Support face provided by Recognize app

This commit is contained in:
Ming Ming 2023-07-16 19:30:23 +08:00
parent c920a6bc36
commit 738883387a
40 changed files with 2839 additions and 121 deletions

View file

@ -7,6 +7,8 @@ import 'package:nc_photos/entity/favorite.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/nc_album.dart'; import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album_item.dart'; import 'package:nc_photos/entity/nc_album_item.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/server_status.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart'; import 'package:nc_photos/entity/sharee.dart';
@ -131,6 +133,50 @@ class ApiNcAlbumItemConverter {
} }
} }
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,
);
}
}
class ApiShareConverter { class ApiShareConverter {
static Share fromApi(api.Share share) { static Share fromApi(api.Share share) {
final shareType = ShareTypeExtension.fromValue(share.shareType); final shareType = ShareTypeExtension.fromValue(share.shareType);

View file

@ -23,6 +23,8 @@ import 'package:nc_photos/entity/nc_album/repo.dart';
import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/entity/pref.dart';
import 'package:nc_photos/entity/pref/provider/shared_preferences.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/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.dart';
import 'package:nc_photos/entity/search/data_source.dart'; import 'package:nc_photos/entity/search/data_source.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
@ -228,6 +230,12 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
FaceRecognitionPersonRemoteDataSource()); FaceRecognitionPersonRemoteDataSource());
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo( c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb)); FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
c.recognizeFaceRepo =
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
c.recognizeFaceRepoRemote =
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
c.recognizeFaceRepoLocal =
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.sqliteDb));
c.touchManager = TouchManager(c); c.touchManager = TouchManager(c);

View file

@ -6,6 +6,7 @@ import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/local_file.dart'; import 'package:nc_photos/entity/local_file.dart';
import 'package:nc_photos/entity/nc_album/repo.dart'; import 'package:nc_photos/entity/nc_album/repo.dart';
import 'package:nc_photos/entity/pref.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/search.dart';
import 'package:nc_photos/entity/share.dart'; import 'package:nc_photos/entity/share.dart';
import 'package:nc_photos/entity/sharee.dart'; import 'package:nc_photos/entity/sharee.dart';
@ -40,6 +41,9 @@ enum DiType {
faceRecognitionPersonRepo, faceRecognitionPersonRepo,
faceRecognitionPersonRepoRemote, faceRecognitionPersonRepoRemote,
faceRecognitionPersonRepoLocal, faceRecognitionPersonRepoLocal,
recognizeFaceRepo,
recognizeFaceRepoRemote,
recognizeFaceRepoLocal,
pref, pref,
sqliteDb, sqliteDb,
touchManager, touchManager,
@ -71,6 +75,9 @@ class DiContainer {
FaceRecognitionPersonRepo? faceRecognitionPersonRepo, FaceRecognitionPersonRepo? faceRecognitionPersonRepo,
FaceRecognitionPersonRepo? faceRecognitionPersonRepoRemote, FaceRecognitionPersonRepo? faceRecognitionPersonRepoRemote,
FaceRecognitionPersonRepo? faceRecognitionPersonRepoLocal, FaceRecognitionPersonRepo? faceRecognitionPersonRepoLocal,
RecognizeFaceRepo? recognizeFaceRepo,
RecognizeFaceRepo? recognizeFaceRepoRemote,
RecognizeFaceRepo? recognizeFaceRepoLocal,
Pref? pref, Pref? pref,
sql.SqliteDb? sqliteDb, sql.SqliteDb? sqliteDb,
TouchManager? touchManager, TouchManager? touchManager,
@ -98,6 +105,9 @@ class DiContainer {
_faceRecognitionPersonRepo = faceRecognitionPersonRepo, _faceRecognitionPersonRepo = faceRecognitionPersonRepo,
_faceRecognitionPersonRepoRemote = faceRecognitionPersonRepoRemote, _faceRecognitionPersonRepoRemote = faceRecognitionPersonRepoRemote,
_faceRecognitionPersonRepoLocal = faceRecognitionPersonRepoLocal, _faceRecognitionPersonRepoLocal = faceRecognitionPersonRepoLocal,
_recognizeFaceRepo = recognizeFaceRepo,
_recognizeFaceRepoRemote = recognizeFaceRepoRemote,
_recognizeFaceRepoLocal = recognizeFaceRepoLocal,
_pref = pref, _pref = pref,
_sqliteDb = sqliteDb, _sqliteDb = sqliteDb,
_touchManager = touchManager; _touchManager = touchManager;
@ -154,6 +164,12 @@ class DiContainer {
return contianer._faceRecognitionPersonRepoRemote != null; return contianer._faceRecognitionPersonRepoRemote != null;
case DiType.faceRecognitionPersonRepoLocal: case DiType.faceRecognitionPersonRepoLocal:
return contianer._faceRecognitionPersonRepoLocal != null; 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: case DiType.pref:
return contianer._pref != null; return contianer._pref != null;
case DiType.sqliteDb: case DiType.sqliteDb:
@ -176,6 +192,7 @@ class DiContainer {
OrNull<SearchRepo>? searchRepo, OrNull<SearchRepo>? searchRepo,
OrNull<NcAlbumRepo>? ncAlbumRepo, OrNull<NcAlbumRepo>? ncAlbumRepo,
OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo, OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo,
OrNull<RecognizeFaceRepo>? recognizeFaceRepo,
OrNull<Pref>? pref, OrNull<Pref>? pref,
OrNull<sql.SqliteDb>? sqliteDb, OrNull<sql.SqliteDb>? sqliteDb,
OrNull<TouchManager>? touchManager, OrNull<TouchManager>? touchManager,
@ -196,6 +213,9 @@ class DiContainer {
faceRecognitionPersonRepo: faceRecognitionPersonRepo == null faceRecognitionPersonRepo: faceRecognitionPersonRepo == null
? _faceRecognitionPersonRepo ? _faceRecognitionPersonRepo
: faceRecognitionPersonRepo.obj, : faceRecognitionPersonRepo.obj,
recognizeFaceRepo: recognizeFaceRepo == null
? _recognizeFaceRepo
: recognizeFaceRepo.obj,
pref: pref == null ? _pref : pref.obj, pref: pref == null ? _pref : pref.obj,
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj, sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
touchManager: touchManager == null ? _touchManager : touchManager.obj, touchManager: touchManager == null ? _touchManager : touchManager.obj,
@ -229,6 +249,9 @@ class DiContainer {
_faceRecognitionPersonRepoRemote!; _faceRecognitionPersonRepoRemote!;
FaceRecognitionPersonRepo get faceRecognitionPersonRepoLocal => FaceRecognitionPersonRepo get faceRecognitionPersonRepoLocal =>
_faceRecognitionPersonRepoLocal!; _faceRecognitionPersonRepoLocal!;
RecognizeFaceRepo get recognizeFaceRepo => _recognizeFaceRepo!;
RecognizeFaceRepo get recognizeFaceRepoRemote => _recognizeFaceRepoRemote!;
RecognizeFaceRepo get recognizeFaceRepoLocal => _recognizeFaceRepoLocal!;
sql.SqliteDb get sqliteDb => _sqliteDb!; sql.SqliteDb get sqliteDb => _sqliteDb!;
Pref get pref => _pref!; Pref get pref => _pref!;
@ -354,6 +377,21 @@ class DiContainer {
_faceRecognitionPersonRepoLocal = v; _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) { set sqliteDb(sql.SqliteDb v) {
assert(_sqliteDb == null); assert(_sqliteDb == null);
_sqliteDb = v; _sqliteDb = v;
@ -396,6 +434,9 @@ class DiContainer {
FaceRecognitionPersonRepo? _faceRecognitionPersonRepo; FaceRecognitionPersonRepo? _faceRecognitionPersonRepo;
FaceRecognitionPersonRepo? _faceRecognitionPersonRepoRemote; FaceRecognitionPersonRepo? _faceRecognitionPersonRepoRemote;
FaceRecognitionPersonRepo? _faceRecognitionPersonRepoLocal; FaceRecognitionPersonRepo? _faceRecognitionPersonRepoLocal;
RecognizeFaceRepo? _recognizeFaceRepo;
RecognizeFaceRepo? _recognizeFaceRepoRemote;
RecognizeFaceRepo? _recognizeFaceRepoLocal;
sql.SqliteDb? _sqliteDb; sql.SqliteDb? _sqliteDb;
Pref? _pref; Pref? _pref;
@ -413,6 +454,7 @@ extension DiContainerExtension on DiContainer {
tagRepo: OrNull(_tagRepoRemote), tagRepo: OrNull(_tagRepoRemote),
ncAlbumRepo: OrNull(_ncAlbumRepoRemote), ncAlbumRepo: OrNull(_ncAlbumRepoRemote),
faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoRemote), faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoRemote),
recognizeFaceRepo: OrNull(_recognizeFaceRepoRemote),
); );
/// Uses local repo if available /// Uses local repo if available
@ -425,6 +467,7 @@ extension DiContainerExtension on DiContainer {
tagRepo: OrNull(_tagRepoLocal), tagRepo: OrNull(_tagRepoLocal),
ncAlbumRepo: OrNull(_ncAlbumRepoLocal), ncAlbumRepo: OrNull(_ncAlbumRepoLocal),
faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoLocal), faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoLocal),
recognizeFaceRepo: OrNull(_recognizeFaceRepoLocal),
); );
DiContainer withLocalAlbumRepo() => DiContainer withLocalAlbumRepo() =>

View file

@ -1,5 +1,6 @@
import 'package:copy_with/copy_with.dart'; import 'package:copy_with/copy_with.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:to_string/to_string.dart'; import 'package:to_string/to_string.dart';
part 'person.g.dart'; part 'person.g.dart';
@ -40,6 +41,10 @@ class Person with EquatableMixin {
isKeepAspectRatio: isKeepAspectRatio, isKeepAspectRatio: isKeepAspectRatio,
); );
/// See [PersonContentProvider.getCoverTransform]
Matrix4? getCoverTransform(int viewportSize, int width, int height) =>
contentProvider.getCoverTransform(viewportSize, width, height);
@override @override
List<Object?> get props => [ List<Object?> get props => [
name, name,
@ -73,4 +78,9 @@ abstract class PersonContentProvider with EquatableMixin {
int height, { int height, {
bool? isKeepAspectRatio, bool? isKeepAspectRatio,
}); });
/// Return the transformation matrix to focus the face
///
/// Only viewport in square is supported
Matrix4? getCoverTransform(int viewportSize, int width, int height);
} }

View file

@ -2,7 +2,9 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/person.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/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/face_recognition.dart';
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
import 'package:nc_photos/entity/person_face.dart'; import 'package:nc_photos/entity/person_face.dart';
abstract class PersonAdapter { abstract class PersonAdapter {
@ -12,6 +14,8 @@ abstract class PersonAdapter {
switch (person.contentProvider.runtimeType) { switch (person.contentProvider.runtimeType) {
case PersonFaceRecognitionProvider: case PersonFaceRecognitionProvider:
return PersonFaceRecognitionAdapter(c, account, person); return PersonFaceRecognitionAdapter(c, account, person);
case PersonRecognizeProvider:
return PersonRecognizeAdapter(c, account, person);
default: default:
throw UnsupportedError( throw UnsupportedError(
"Unknown type: ${person.contentProvider.runtimeType}"); "Unknown type: ${person.contentProvider.runtimeType}");

View file

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

View file

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

View file

@ -2,6 +2,9 @@ import 'package:nc_photos/account.dart';
import 'package:nc_photos/entity/face_recognition_person.dart'; import 'package:nc_photos/entity/face_recognition_person.dart';
import 'package:nc_photos/entity/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/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 { class PersonBuilder {
static Person byFaceRecognitionPerson( static Person byFaceRecognitionPerson(
@ -14,4 +17,16 @@ class PersonBuilder {
), ),
); );
} }
static Person byRecognizeFace(
Account account, RecognizeFace face, List<RecognizeFaceItem>? items) {
return Person(
name: face.isNamed ? face.label : "",
contentProvider: PersonRecognizeProvider(
account: account,
face: face,
items: items,
),
);
}
} }

View file

@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:nc_photos/account.dart'; import 'package:nc_photos/account.dart';
import 'package:nc_photos/api/api_util.dart' as api_util; 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/face_recognition_person.dart';
@ -43,6 +44,9 @@ class PersonFaceRecognitionProvider
); );
} }
@override
Matrix4? getCoverTransform(int viewportSize, int width, int height) => null;
@override @override
List<Object?> get props => [account, person]; List<Object?> get props => [account, person];

View file

@ -0,0 +1,111 @@
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))
.reversed
.toList();
@override
String toString() => _$toString();
@override
String get fourCc => "RCNZ";
@override
String get id => face.label;
@override
int? get count => items?.length;
@override
String? getCoverUrl(
int width,
int height, {
bool? isKeepAspectRatio,
}) =>
items?.firstOrNull?.run((i) => api_util.getFilePreviewUrl(
account,
i.toFile(),
width: width,
height: height,
isKeepAspectRatio: isKeepAspectRatio ?? false,
));
@override
Matrix4? getCoverTransform(int viewportSize, int imgW, int imgH) {
final detection = items?.firstOrNull?.faceDetections
?.firstWhereOrNull((e) => e["title"] == face.label);
if (detection == null) {
return null;
}
final faceXNorm = (detection["x"] as Object?).as<double>();
final faceYNorm = (detection["y"] as Object?).as<double>();
final faceHNorm = (detection["height"] as Object?).as<double>();
final faceWNorm = (detection["width"] as Object?).as<double>();
if (faceXNorm == null ||
faceYNorm == null ||
faceHNorm == null ||
faceWNorm == null) {
return null;
}
// move image to the face
double mx = imgW * -faceXNorm;
double my = imgH * -faceYNorm;
// add offset in case image is not a square
if (imgW > imgH) {
mx += (imgW - imgH) / 2;
} else if (imgH > imgW) {
my += (imgH - imgW) / 2;
}
// scale image to focus on the face
final faceW = imgW * faceWNorm;
final faceH = imgH * faceHNorm;
double ms;
if (faceW > faceH) {
ms = viewportSize / faceW;
} else {
ms = viewportSize / faceH;
}
// slightly scale down to include pixels around the face
ms *= .75;
// center the scaled image
final resultFaceW = faceW * ms;
final resultFaceH = faceH * ms;
final cx = (viewportSize - resultFaceW) / 2;
final cy = (viewportSize - resultFaceH) / 2;
return Matrix4.identity()
..translate(cx, cy)
..scale(ms)
..translate(mx, my);
}
@override
List<Object?> get props => [account, face, items];
final Account account;
final RecognizeFace face;
final List<RecognizeFaceItem>? items;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -39,6 +39,8 @@ part 'database_extension.dart';
FaceRecognitionPersons, FaceRecognitionPersons,
NcAlbums, NcAlbums,
NcAlbumItems, NcAlbumItems,
RecognizeFaces,
RecognizeFaceItems,
], ],
) )
class SqliteDb extends _$SqliteDb { class SqliteDb extends _$SqliteDb {
@ -104,6 +106,8 @@ class SqliteDb extends _$SqliteDb {
if (from >= 2) { if (from >= 2) {
await m.renameTable(faceRecognitionPersons, "persons"); await m.renameTable(faceRecognitionPersons, "persons");
} }
await m.createTable(recognizeFaces);
await m.createTable(recognizeFaceItems);
} }
}); });
} catch (e, stackTrace) { } catch (e, stackTrace) {

View file

@ -5288,6 +5288,920 @@ class NcAlbumItemsCompanion extends UpdateCompanion<NcAlbumItem> {
} }
} }
class $RecognizeFacesTable extends RecognizeFaces
with TableInfo<$RecognizeFacesTable, RecognizeFace> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$RecognizeFacesTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _rowIdMeta = const VerificationMeta('rowId');
@override
late final GeneratedColumn<int> rowId = GeneratedColumn<int>(
'row_id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _accountMeta =
const VerificationMeta('account');
@override
late final GeneratedColumn<int> account = GeneratedColumn<int>(
'account', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES accounts (row_id) ON DELETE CASCADE'));
static const VerificationMeta _labelMeta = const VerificationMeta('label');
@override
late final GeneratedColumn<String> label = GeneratedColumn<String>(
'label', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
@override
List<GeneratedColumn> get $columns => [rowId, account, label];
@override
String get aliasedName => _alias ?? 'recognize_faces';
@override
String get actualTableName => 'recognize_faces';
@override
VerificationContext validateIntegrity(Insertable<RecognizeFace> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('row_id')) {
context.handle(
_rowIdMeta, rowId.isAcceptableOrUnknown(data['row_id']!, _rowIdMeta));
}
if (data.containsKey('account')) {
context.handle(_accountMeta,
account.isAcceptableOrUnknown(data['account']!, _accountMeta));
} else if (isInserting) {
context.missing(_accountMeta);
}
if (data.containsKey('label')) {
context.handle(
_labelMeta, label.isAcceptableOrUnknown(data['label']!, _labelMeta));
} else if (isInserting) {
context.missing(_labelMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {rowId};
@override
List<Set<GeneratedColumn>> get uniqueKeys => [
{account, label},
];
@override
RecognizeFace map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return RecognizeFace(
rowId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}row_id'])!,
account: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}account'])!,
label: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}label'])!,
);
}
@override
$RecognizeFacesTable createAlias(String alias) {
return $RecognizeFacesTable(attachedDatabase, alias);
}
}
class RecognizeFace extends DataClass implements Insertable<RecognizeFace> {
final int rowId;
final int account;
final String label;
const RecognizeFace(
{required this.rowId, required this.account, required this.label});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['row_id'] = Variable<int>(rowId);
map['account'] = Variable<int>(account);
map['label'] = Variable<String>(label);
return map;
}
RecognizeFacesCompanion toCompanion(bool nullToAbsent) {
return RecognizeFacesCompanion(
rowId: Value(rowId),
account: Value(account),
label: Value(label),
);
}
factory RecognizeFace.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return RecognizeFace(
rowId: serializer.fromJson<int>(json['rowId']),
account: serializer.fromJson<int>(json['account']),
label: serializer.fromJson<String>(json['label']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'rowId': serializer.toJson<int>(rowId),
'account': serializer.toJson<int>(account),
'label': serializer.toJson<String>(label),
};
}
RecognizeFace copyWith({int? rowId, int? account, String? label}) =>
RecognizeFace(
rowId: rowId ?? this.rowId,
account: account ?? this.account,
label: label ?? this.label,
);
@override
String toString() {
return (StringBuffer('RecognizeFace(')
..write('rowId: $rowId, ')
..write('account: $account, ')
..write('label: $label')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(rowId, account, label);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is RecognizeFace &&
other.rowId == this.rowId &&
other.account == this.account &&
other.label == this.label);
}
class RecognizeFacesCompanion extends UpdateCompanion<RecognizeFace> {
final Value<int> rowId;
final Value<int> account;
final Value<String> label;
const RecognizeFacesCompanion({
this.rowId = const Value.absent(),
this.account = const Value.absent(),
this.label = const Value.absent(),
});
RecognizeFacesCompanion.insert({
this.rowId = const Value.absent(),
required int account,
required String label,
}) : account = Value(account),
label = Value(label);
static Insertable<RecognizeFace> custom({
Expression<int>? rowId,
Expression<int>? account,
Expression<String>? label,
}) {
return RawValuesInsertable({
if (rowId != null) 'row_id': rowId,
if (account != null) 'account': account,
if (label != null) 'label': label,
});
}
RecognizeFacesCompanion copyWith(
{Value<int>? rowId, Value<int>? account, Value<String>? label}) {
return RecognizeFacesCompanion(
rowId: rowId ?? this.rowId,
account: account ?? this.account,
label: label ?? this.label,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (rowId.present) {
map['row_id'] = Variable<int>(rowId.value);
}
if (account.present) {
map['account'] = Variable<int>(account.value);
}
if (label.present) {
map['label'] = Variable<String>(label.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('RecognizeFacesCompanion(')
..write('rowId: $rowId, ')
..write('account: $account, ')
..write('label: $label')
..write(')'))
.toString();
}
}
class $RecognizeFaceItemsTable extends RecognizeFaceItems
with TableInfo<$RecognizeFaceItemsTable, RecognizeFaceItem> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$RecognizeFaceItemsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _rowIdMeta = const VerificationMeta('rowId');
@override
late final GeneratedColumn<int> rowId = GeneratedColumn<int>(
'row_id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _parentMeta = const VerificationMeta('parent');
@override
late final GeneratedColumn<int> parent = GeneratedColumn<int>(
'parent', aliasedName, false,
type: DriftSqlType.int,
requiredDuringInsert: true,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'REFERENCES recognize_faces (row_id) ON DELETE CASCADE'));
static const VerificationMeta _relativePathMeta =
const VerificationMeta('relativePath');
@override
late final GeneratedColumn<String> relativePath = GeneratedColumn<String>(
'relative_path', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true);
static const VerificationMeta _fileIdMeta = const VerificationMeta('fileId');
@override
late final GeneratedColumn<int> fileId = GeneratedColumn<int>(
'file_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
static const VerificationMeta _contentLengthMeta =
const VerificationMeta('contentLength');
@override
late final GeneratedColumn<int> contentLength = GeneratedColumn<int>(
'content_length', aliasedName, true,
type: DriftSqlType.int, requiredDuringInsert: false);
static const VerificationMeta _contentTypeMeta =
const VerificationMeta('contentType');
@override
late final GeneratedColumn<String> contentType = GeneratedColumn<String>(
'content_type', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _etagMeta = const VerificationMeta('etag');
@override
late final GeneratedColumn<String> etag = GeneratedColumn<String>(
'etag', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _lastModifiedMeta =
const VerificationMeta('lastModified');
@override
late final GeneratedColumnWithTypeConverter<DateTime?, DateTime>
lastModified = GeneratedColumn<DateTime>(
'last_modified', aliasedName, true,
type: DriftSqlType.dateTime, requiredDuringInsert: false)
.withConverter<DateTime?>(
$RecognizeFaceItemsTable.$converterlastModifiedn);
static const VerificationMeta _hasPreviewMeta =
const VerificationMeta('hasPreview');
@override
late final GeneratedColumn<bool> hasPreview =
GeneratedColumn<bool>('has_preview', aliasedName, true,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
SqlDialect.sqlite: 'CHECK ("has_preview" IN (0, 1))',
SqlDialect.mysql: '',
SqlDialect.postgres: '',
}));
static const VerificationMeta _realPathMeta =
const VerificationMeta('realPath');
@override
late final GeneratedColumn<String> realPath = GeneratedColumn<String>(
'real_path', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
static const VerificationMeta _isFavoriteMeta =
const VerificationMeta('isFavorite');
@override
late final GeneratedColumn<bool> isFavorite =
GeneratedColumn<bool>('is_favorite', aliasedName, true,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintsDependsOnDialect({
SqlDialect.sqlite: 'CHECK ("is_favorite" IN (0, 1))',
SqlDialect.mysql: '',
SqlDialect.postgres: '',
}));
static const VerificationMeta _fileMetadataWidthMeta =
const VerificationMeta('fileMetadataWidth');
@override
late final GeneratedColumn<int> fileMetadataWidth = GeneratedColumn<int>(
'file_metadata_width', aliasedName, true,
type: DriftSqlType.int, requiredDuringInsert: false);
static const VerificationMeta _fileMetadataHeightMeta =
const VerificationMeta('fileMetadataHeight');
@override
late final GeneratedColumn<int> fileMetadataHeight = GeneratedColumn<int>(
'file_metadata_height', aliasedName, true,
type: DriftSqlType.int, requiredDuringInsert: false);
static const VerificationMeta _faceDetectionsMeta =
const VerificationMeta('faceDetections');
@override
late final GeneratedColumn<String> faceDetections = GeneratedColumn<String>(
'face_detections', aliasedName, true,
type: DriftSqlType.string, requiredDuringInsert: false);
@override
List<GeneratedColumn> get $columns => [
rowId,
parent,
relativePath,
fileId,
contentLength,
contentType,
etag,
lastModified,
hasPreview,
realPath,
isFavorite,
fileMetadataWidth,
fileMetadataHeight,
faceDetections
];
@override
String get aliasedName => _alias ?? 'recognize_face_items';
@override
String get actualTableName => 'recognize_face_items';
@override
VerificationContext validateIntegrity(Insertable<RecognizeFaceItem> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('row_id')) {
context.handle(
_rowIdMeta, rowId.isAcceptableOrUnknown(data['row_id']!, _rowIdMeta));
}
if (data.containsKey('parent')) {
context.handle(_parentMeta,
parent.isAcceptableOrUnknown(data['parent']!, _parentMeta));
} else if (isInserting) {
context.missing(_parentMeta);
}
if (data.containsKey('relative_path')) {
context.handle(
_relativePathMeta,
relativePath.isAcceptableOrUnknown(
data['relative_path']!, _relativePathMeta));
} else if (isInserting) {
context.missing(_relativePathMeta);
}
if (data.containsKey('file_id')) {
context.handle(_fileIdMeta,
fileId.isAcceptableOrUnknown(data['file_id']!, _fileIdMeta));
} else if (isInserting) {
context.missing(_fileIdMeta);
}
if (data.containsKey('content_length')) {
context.handle(
_contentLengthMeta,
contentLength.isAcceptableOrUnknown(
data['content_length']!, _contentLengthMeta));
}
if (data.containsKey('content_type')) {
context.handle(
_contentTypeMeta,
contentType.isAcceptableOrUnknown(
data['content_type']!, _contentTypeMeta));
}
if (data.containsKey('etag')) {
context.handle(
_etagMeta, etag.isAcceptableOrUnknown(data['etag']!, _etagMeta));
}
context.handle(_lastModifiedMeta, const VerificationResult.success());
if (data.containsKey('has_preview')) {
context.handle(
_hasPreviewMeta,
hasPreview.isAcceptableOrUnknown(
data['has_preview']!, _hasPreviewMeta));
}
if (data.containsKey('real_path')) {
context.handle(_realPathMeta,
realPath.isAcceptableOrUnknown(data['real_path']!, _realPathMeta));
}
if (data.containsKey('is_favorite')) {
context.handle(
_isFavoriteMeta,
isFavorite.isAcceptableOrUnknown(
data['is_favorite']!, _isFavoriteMeta));
}
if (data.containsKey('file_metadata_width')) {
context.handle(
_fileMetadataWidthMeta,
fileMetadataWidth.isAcceptableOrUnknown(
data['file_metadata_width']!, _fileMetadataWidthMeta));
}
if (data.containsKey('file_metadata_height')) {
context.handle(
_fileMetadataHeightMeta,
fileMetadataHeight.isAcceptableOrUnknown(
data['file_metadata_height']!, _fileMetadataHeightMeta));
}
if (data.containsKey('face_detections')) {
context.handle(
_faceDetectionsMeta,
faceDetections.isAcceptableOrUnknown(
data['face_detections']!, _faceDetectionsMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {rowId};
@override
List<Set<GeneratedColumn>> get uniqueKeys => [
{parent, fileId},
];
@override
RecognizeFaceItem map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return RecognizeFaceItem(
rowId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}row_id'])!,
parent: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}parent'])!,
relativePath: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}relative_path'])!,
fileId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}file_id'])!,
contentLength: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}content_length']),
contentType: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}content_type']),
etag: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}etag']),
lastModified: $RecognizeFaceItemsTable.$converterlastModifiedn.fromSql(
attachedDatabase.typeMapping.read(
DriftSqlType.dateTime, data['${effectivePrefix}last_modified'])),
hasPreview: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}has_preview']),
realPath: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}real_path']),
isFavorite: attachedDatabase.typeMapping
.read(DriftSqlType.bool, data['${effectivePrefix}is_favorite']),
fileMetadataWidth: attachedDatabase.typeMapping.read(
DriftSqlType.int, data['${effectivePrefix}file_metadata_width']),
fileMetadataHeight: attachedDatabase.typeMapping.read(
DriftSqlType.int, data['${effectivePrefix}file_metadata_height']),
faceDetections: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}face_detections']),
);
}
@override
$RecognizeFaceItemsTable createAlias(String alias) {
return $RecognizeFaceItemsTable(attachedDatabase, alias);
}
static TypeConverter<DateTime, DateTime> $converterlastModified =
const SqliteDateTimeConverter();
static TypeConverter<DateTime?, DateTime?> $converterlastModifiedn =
NullAwareTypeConverter.wrap($converterlastModified);
}
class RecognizeFaceItem extends DataClass
implements Insertable<RecognizeFaceItem> {
final int rowId;
final int parent;
final String relativePath;
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 String? faceDetections;
const RecognizeFaceItem(
{required this.rowId,
required this.parent,
required this.relativePath,
required this.fileId,
this.contentLength,
this.contentType,
this.etag,
this.lastModified,
this.hasPreview,
this.realPath,
this.isFavorite,
this.fileMetadataWidth,
this.fileMetadataHeight,
this.faceDetections});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['row_id'] = Variable<int>(rowId);
map['parent'] = Variable<int>(parent);
map['relative_path'] = Variable<String>(relativePath);
map['file_id'] = Variable<int>(fileId);
if (!nullToAbsent || contentLength != null) {
map['content_length'] = Variable<int>(contentLength);
}
if (!nullToAbsent || contentType != null) {
map['content_type'] = Variable<String>(contentType);
}
if (!nullToAbsent || etag != null) {
map['etag'] = Variable<String>(etag);
}
if (!nullToAbsent || lastModified != null) {
final converter = $RecognizeFaceItemsTable.$converterlastModifiedn;
map['last_modified'] = Variable<DateTime>(converter.toSql(lastModified));
}
if (!nullToAbsent || hasPreview != null) {
map['has_preview'] = Variable<bool>(hasPreview);
}
if (!nullToAbsent || realPath != null) {
map['real_path'] = Variable<String>(realPath);
}
if (!nullToAbsent || isFavorite != null) {
map['is_favorite'] = Variable<bool>(isFavorite);
}
if (!nullToAbsent || fileMetadataWidth != null) {
map['file_metadata_width'] = Variable<int>(fileMetadataWidth);
}
if (!nullToAbsent || fileMetadataHeight != null) {
map['file_metadata_height'] = Variable<int>(fileMetadataHeight);
}
if (!nullToAbsent || faceDetections != null) {
map['face_detections'] = Variable<String>(faceDetections);
}
return map;
}
RecognizeFaceItemsCompanion toCompanion(bool nullToAbsent) {
return RecognizeFaceItemsCompanion(
rowId: Value(rowId),
parent: Value(parent),
relativePath: Value(relativePath),
fileId: Value(fileId),
contentLength: contentLength == null && nullToAbsent
? const Value.absent()
: Value(contentLength),
contentType: contentType == null && nullToAbsent
? const Value.absent()
: Value(contentType),
etag: etag == null && nullToAbsent ? const Value.absent() : Value(etag),
lastModified: lastModified == null && nullToAbsent
? const Value.absent()
: Value(lastModified),
hasPreview: hasPreview == null && nullToAbsent
? const Value.absent()
: Value(hasPreview),
realPath: realPath == null && nullToAbsent
? const Value.absent()
: Value(realPath),
isFavorite: isFavorite == null && nullToAbsent
? const Value.absent()
: Value(isFavorite),
fileMetadataWidth: fileMetadataWidth == null && nullToAbsent
? const Value.absent()
: Value(fileMetadataWidth),
fileMetadataHeight: fileMetadataHeight == null && nullToAbsent
? const Value.absent()
: Value(fileMetadataHeight),
faceDetections: faceDetections == null && nullToAbsent
? const Value.absent()
: Value(faceDetections),
);
}
factory RecognizeFaceItem.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return RecognizeFaceItem(
rowId: serializer.fromJson<int>(json['rowId']),
parent: serializer.fromJson<int>(json['parent']),
relativePath: serializer.fromJson<String>(json['relativePath']),
fileId: serializer.fromJson<int>(json['fileId']),
contentLength: serializer.fromJson<int?>(json['contentLength']),
contentType: serializer.fromJson<String?>(json['contentType']),
etag: serializer.fromJson<String?>(json['etag']),
lastModified: serializer.fromJson<DateTime?>(json['lastModified']),
hasPreview: serializer.fromJson<bool?>(json['hasPreview']),
realPath: serializer.fromJson<String?>(json['realPath']),
isFavorite: serializer.fromJson<bool?>(json['isFavorite']),
fileMetadataWidth: serializer.fromJson<int?>(json['fileMetadataWidth']),
fileMetadataHeight: serializer.fromJson<int?>(json['fileMetadataHeight']),
faceDetections: serializer.fromJson<String?>(json['faceDetections']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'rowId': serializer.toJson<int>(rowId),
'parent': serializer.toJson<int>(parent),
'relativePath': serializer.toJson<String>(relativePath),
'fileId': serializer.toJson<int>(fileId),
'contentLength': serializer.toJson<int?>(contentLength),
'contentType': serializer.toJson<String?>(contentType),
'etag': serializer.toJson<String?>(etag),
'lastModified': serializer.toJson<DateTime?>(lastModified),
'hasPreview': serializer.toJson<bool?>(hasPreview),
'realPath': serializer.toJson<String?>(realPath),
'isFavorite': serializer.toJson<bool?>(isFavorite),
'fileMetadataWidth': serializer.toJson<int?>(fileMetadataWidth),
'fileMetadataHeight': serializer.toJson<int?>(fileMetadataHeight),
'faceDetections': serializer.toJson<String?>(faceDetections),
};
}
RecognizeFaceItem copyWith(
{int? rowId,
int? parent,
String? relativePath,
int? fileId,
Value<int?> contentLength = const Value.absent(),
Value<String?> contentType = const Value.absent(),
Value<String?> etag = const Value.absent(),
Value<DateTime?> lastModified = const Value.absent(),
Value<bool?> hasPreview = const Value.absent(),
Value<String?> realPath = const Value.absent(),
Value<bool?> isFavorite = const Value.absent(),
Value<int?> fileMetadataWidth = const Value.absent(),
Value<int?> fileMetadataHeight = const Value.absent(),
Value<String?> faceDetections = const Value.absent()}) =>
RecognizeFaceItem(
rowId: rowId ?? this.rowId,
parent: parent ?? this.parent,
relativePath: relativePath ?? this.relativePath,
fileId: fileId ?? this.fileId,
contentLength:
contentLength.present ? contentLength.value : this.contentLength,
contentType: contentType.present ? contentType.value : this.contentType,
etag: etag.present ? etag.value : this.etag,
lastModified:
lastModified.present ? lastModified.value : this.lastModified,
hasPreview: hasPreview.present ? hasPreview.value : this.hasPreview,
realPath: realPath.present ? realPath.value : this.realPath,
isFavorite: isFavorite.present ? isFavorite.value : this.isFavorite,
fileMetadataWidth: fileMetadataWidth.present
? fileMetadataWidth.value
: this.fileMetadataWidth,
fileMetadataHeight: fileMetadataHeight.present
? fileMetadataHeight.value
: this.fileMetadataHeight,
faceDetections:
faceDetections.present ? faceDetections.value : this.faceDetections,
);
@override
String toString() {
return (StringBuffer('RecognizeFaceItem(')
..write('rowId: $rowId, ')
..write('parent: $parent, ')
..write('relativePath: $relativePath, ')
..write('fileId: $fileId, ')
..write('contentLength: $contentLength, ')
..write('contentType: $contentType, ')
..write('etag: $etag, ')
..write('lastModified: $lastModified, ')
..write('hasPreview: $hasPreview, ')
..write('realPath: $realPath, ')
..write('isFavorite: $isFavorite, ')
..write('fileMetadataWidth: $fileMetadataWidth, ')
..write('fileMetadataHeight: $fileMetadataHeight, ')
..write('faceDetections: $faceDetections')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(
rowId,
parent,
relativePath,
fileId,
contentLength,
contentType,
etag,
lastModified,
hasPreview,
realPath,
isFavorite,
fileMetadataWidth,
fileMetadataHeight,
faceDetections);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is RecognizeFaceItem &&
other.rowId == this.rowId &&
other.parent == this.parent &&
other.relativePath == this.relativePath &&
other.fileId == this.fileId &&
other.contentLength == this.contentLength &&
other.contentType == this.contentType &&
other.etag == this.etag &&
other.lastModified == this.lastModified &&
other.hasPreview == this.hasPreview &&
other.realPath == this.realPath &&
other.isFavorite == this.isFavorite &&
other.fileMetadataWidth == this.fileMetadataWidth &&
other.fileMetadataHeight == this.fileMetadataHeight &&
other.faceDetections == this.faceDetections);
}
class RecognizeFaceItemsCompanion extends UpdateCompanion<RecognizeFaceItem> {
final Value<int> rowId;
final Value<int> parent;
final Value<String> relativePath;
final Value<int> fileId;
final Value<int?> contentLength;
final Value<String?> contentType;
final Value<String?> etag;
final Value<DateTime?> lastModified;
final Value<bool?> hasPreview;
final Value<String?> realPath;
final Value<bool?> isFavorite;
final Value<int?> fileMetadataWidth;
final Value<int?> fileMetadataHeight;
final Value<String?> faceDetections;
const RecognizeFaceItemsCompanion({
this.rowId = const Value.absent(),
this.parent = const Value.absent(),
this.relativePath = const Value.absent(),
this.fileId = const Value.absent(),
this.contentLength = const Value.absent(),
this.contentType = const Value.absent(),
this.etag = const Value.absent(),
this.lastModified = const Value.absent(),
this.hasPreview = const Value.absent(),
this.realPath = const Value.absent(),
this.isFavorite = const Value.absent(),
this.fileMetadataWidth = const Value.absent(),
this.fileMetadataHeight = const Value.absent(),
this.faceDetections = const Value.absent(),
});
RecognizeFaceItemsCompanion.insert({
this.rowId = const Value.absent(),
required int parent,
required String relativePath,
required int fileId,
this.contentLength = const Value.absent(),
this.contentType = const Value.absent(),
this.etag = const Value.absent(),
this.lastModified = const Value.absent(),
this.hasPreview = const Value.absent(),
this.realPath = const Value.absent(),
this.isFavorite = const Value.absent(),
this.fileMetadataWidth = const Value.absent(),
this.fileMetadataHeight = const Value.absent(),
this.faceDetections = const Value.absent(),
}) : parent = Value(parent),
relativePath = Value(relativePath),
fileId = Value(fileId);
static Insertable<RecognizeFaceItem> custom({
Expression<int>? rowId,
Expression<int>? parent,
Expression<String>? relativePath,
Expression<int>? fileId,
Expression<int>? contentLength,
Expression<String>? contentType,
Expression<String>? etag,
Expression<DateTime>? lastModified,
Expression<bool>? hasPreview,
Expression<String>? realPath,
Expression<bool>? isFavorite,
Expression<int>? fileMetadataWidth,
Expression<int>? fileMetadataHeight,
Expression<String>? faceDetections,
}) {
return RawValuesInsertable({
if (rowId != null) 'row_id': rowId,
if (parent != null) 'parent': parent,
if (relativePath != null) 'relative_path': relativePath,
if (fileId != null) 'file_id': fileId,
if (contentLength != null) 'content_length': contentLength,
if (contentType != null) 'content_type': contentType,
if (etag != null) 'etag': etag,
if (lastModified != null) 'last_modified': lastModified,
if (hasPreview != null) 'has_preview': hasPreview,
if (realPath != null) 'real_path': realPath,
if (isFavorite != null) 'is_favorite': isFavorite,
if (fileMetadataWidth != null) 'file_metadata_width': fileMetadataWidth,
if (fileMetadataHeight != null)
'file_metadata_height': fileMetadataHeight,
if (faceDetections != null) 'face_detections': faceDetections,
});
}
RecognizeFaceItemsCompanion copyWith(
{Value<int>? rowId,
Value<int>? parent,
Value<String>? relativePath,
Value<int>? fileId,
Value<int?>? contentLength,
Value<String?>? contentType,
Value<String?>? etag,
Value<DateTime?>? lastModified,
Value<bool?>? hasPreview,
Value<String?>? realPath,
Value<bool?>? isFavorite,
Value<int?>? fileMetadataWidth,
Value<int?>? fileMetadataHeight,
Value<String?>? faceDetections}) {
return RecognizeFaceItemsCompanion(
rowId: rowId ?? this.rowId,
parent: parent ?? this.parent,
relativePath: relativePath ?? this.relativePath,
fileId: fileId ?? this.fileId,
contentLength: contentLength ?? this.contentLength,
contentType: contentType ?? this.contentType,
etag: etag ?? this.etag,
lastModified: lastModified ?? this.lastModified,
hasPreview: hasPreview ?? this.hasPreview,
realPath: realPath ?? this.realPath,
isFavorite: isFavorite ?? this.isFavorite,
fileMetadataWidth: fileMetadataWidth ?? this.fileMetadataWidth,
fileMetadataHeight: fileMetadataHeight ?? this.fileMetadataHeight,
faceDetections: faceDetections ?? this.faceDetections,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (rowId.present) {
map['row_id'] = Variable<int>(rowId.value);
}
if (parent.present) {
map['parent'] = Variable<int>(parent.value);
}
if (relativePath.present) {
map['relative_path'] = Variable<String>(relativePath.value);
}
if (fileId.present) {
map['file_id'] = Variable<int>(fileId.value);
}
if (contentLength.present) {
map['content_length'] = Variable<int>(contentLength.value);
}
if (contentType.present) {
map['content_type'] = Variable<String>(contentType.value);
}
if (etag.present) {
map['etag'] = Variable<String>(etag.value);
}
if (lastModified.present) {
final converter = $RecognizeFaceItemsTable.$converterlastModifiedn;
map['last_modified'] =
Variable<DateTime>(converter.toSql(lastModified.value));
}
if (hasPreview.present) {
map['has_preview'] = Variable<bool>(hasPreview.value);
}
if (realPath.present) {
map['real_path'] = Variable<String>(realPath.value);
}
if (isFavorite.present) {
map['is_favorite'] = Variable<bool>(isFavorite.value);
}
if (fileMetadataWidth.present) {
map['file_metadata_width'] = Variable<int>(fileMetadataWidth.value);
}
if (fileMetadataHeight.present) {
map['file_metadata_height'] = Variable<int>(fileMetadataHeight.value);
}
if (faceDetections.present) {
map['face_detections'] = Variable<String>(faceDetections.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('RecognizeFaceItemsCompanion(')
..write('rowId: $rowId, ')
..write('parent: $parent, ')
..write('relativePath: $relativePath, ')
..write('fileId: $fileId, ')
..write('contentLength: $contentLength, ')
..write('contentType: $contentType, ')
..write('etag: $etag, ')
..write('lastModified: $lastModified, ')
..write('hasPreview: $hasPreview, ')
..write('realPath: $realPath, ')
..write('isFavorite: $isFavorite, ')
..write('fileMetadataWidth: $fileMetadataWidth, ')
..write('fileMetadataHeight: $fileMetadataHeight, ')
..write('faceDetections: $faceDetections')
..write(')'))
.toString();
}
}
abstract class _$SqliteDb extends GeneratedDatabase { abstract class _$SqliteDb extends GeneratedDatabase {
_$SqliteDb(QueryExecutor e) : super(e); _$SqliteDb(QueryExecutor e) : super(e);
late final $ServersTable servers = $ServersTable(this); late final $ServersTable servers = $ServersTable(this);
@ -5305,6 +6219,9 @@ abstract class _$SqliteDb extends GeneratedDatabase {
$FaceRecognitionPersonsTable(this); $FaceRecognitionPersonsTable(this);
late final $NcAlbumsTable ncAlbums = $NcAlbumsTable(this); late final $NcAlbumsTable ncAlbums = $NcAlbumsTable(this);
late final $NcAlbumItemsTable ncAlbumItems = $NcAlbumItemsTable(this); late final $NcAlbumItemsTable ncAlbumItems = $NcAlbumItemsTable(this);
late final $RecognizeFacesTable recognizeFaces = $RecognizeFacesTable(this);
late final $RecognizeFaceItemsTable recognizeFaceItems =
$RecognizeFaceItemsTable(this);
@override @override
Iterable<TableInfo<Table, Object?>> get allTables => Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>(); allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@ -5323,7 +6240,9 @@ abstract class _$SqliteDb extends GeneratedDatabase {
tags, tags,
faceRecognitionPersons, faceRecognitionPersons,
ncAlbums, ncAlbums,
ncAlbumItems ncAlbumItems,
recognizeFaces,
recognizeFaceItems
]; ];
@override @override
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules( StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
@ -5433,6 +6352,20 @@ abstract class _$SqliteDb extends GeneratedDatabase {
TableUpdate('nc_album_items', kind: UpdateKind.delete), TableUpdate('nc_album_items', kind: UpdateKind.delete),
], ],
), ),
WritePropagation(
on: TableUpdateQuery.onTableName('accounts',
limitUpdateKind: UpdateKind.delete),
result: [
TableUpdate('recognize_faces', kind: UpdateKind.delete),
],
),
WritePropagation(
on: TableUpdateQuery.onTableName('recognize_faces',
limitUpdateKind: UpdateKind.delete),
result: [
TableUpdate('recognize_face_items', kind: UpdateKind.delete),
],
),
], ],
); );
} }

View file

@ -581,6 +581,92 @@ extension SqliteDbExtension on SqliteDb {
} }
} }
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({ Future<int> countMissingMetadataByFileIds({
Account? sqlAccount, Account? sqlAccount,
app.Account? appAccount, app.Account? appAccount,

View file

@ -1,4 +1,8 @@
import 'package:drift/drift.dart'; 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 { class Servers extends Table {
IntColumn get rowId => integer().autoIncrement()(); IntColumn get rowId => integer().autoIncrement()();
@ -233,6 +237,43 @@ class FaceRecognitionPersons 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> { class SqliteDateTimeConverter extends TypeConverter<DateTime, DateTime> {
const SqliteDateTimeConverter(); const SqliteDateTimeConverter();

View file

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

View file

@ -11,6 +11,8 @@ import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart'; import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/nc_album.dart'; import 'package:nc_photos/entity/nc_album.dart';
import 'package:nc_photos/entity/nc_album_item.dart'; import 'package:nc_photos/entity/nc_album_item.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/database.dart' as sql;
import 'package:nc_photos/entity/tag.dart'; import 'package:nc_photos/entity/tag.dart';
import 'package:nc_photos/iterable_extension.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:nc_photos/or_null.dart';
import 'package:np_api/np_api.dart' as api; import 'package:np_api/np_api.dart' as api;
import 'package:np_common/ci_string.dart'; import 'package:np_common/ci_string.dart';
import 'package:np_common/type.dart';
extension SqlTagListExtension on List<sql.Tag> { extension SqlTagListExtension on List<sql.Tag> {
Future<List<Tag>> convertToAppTag() { Future<List<Tag>> convertToAppTag() {
@ -52,6 +55,22 @@ extension AppFaceRecognitionPersonListExtension on List<FaceRecognitionPerson> {
} }
} }
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);
}
}
class SqliteAlbumConverter { class SqliteAlbumConverter {
static Album fromSql( static Album fromSql(
sql.Album album, File albumFile, List<sql.AlbumShare> shares) { sql.Album album, File albumFile, List<sql.AlbumShare> shares) {
@ -326,6 +345,62 @@ 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) { sql.TagsCompanion _convertAppTag(Map map) {
final account = map["account"] as sql.Account?; final account = map["account"] as sql.Account?;
final tag = map["tag"] as Tag; final tag = map["tag"] as Tag;
@ -337,3 +412,9 @@ sql.FaceRecognitionPersonsCompanion _convertAppFaceRecognitionPerson(Map map) {
final person = map["person"] as FaceRecognitionPerson; final person = map["person"] as FaceRecognitionPerson;
return SqliteFaceRecognitionPersonConverter.toSql(account, person); return SqliteFaceRecognitionPersonConverter.toSql(account, person);
} }
sql.RecognizeFacesCompanion _convertAppRecognizeFace(Map map) {
final account = map["account"] as sql.Account?;
final face = map["face"] as RecognizeFace;
return SqliteRecognizeFaceConverter.toSql(account, face);
}

View file

@ -23,10 +23,13 @@ class SyncFaceRecognitionPerson {
/// Return if any people were updated /// Return if any people were updated
Future<bool> call(Account account) async { Future<bool> call(Account account) async {
_log.info("[call] Sync people with remote"); _log.info("[call] Sync people with remote");
int personSorter(FaceRecognitionPerson a, FaceRecognitionPerson b) =>
a.name.compareTo(b.name);
late final List<FaceRecognitionPerson> remote; late final List<FaceRecognitionPerson> remote;
try { try {
remote = remote = (await ListFaceRecognitionPerson(_c.withRemoteRepo())(account)
await ListFaceRecognitionPerson(_c.withRemoteRepo())(account).last; .last)
..sort(personSorter);
} catch (e) { } catch (e) {
if (e is ApiException && e.response.statusCode == 404) { if (e is ApiException && e.response.statusCode == 404) {
// face recognition app probably not installed, ignore // face recognition app probably not installed, ignore
@ -35,10 +38,9 @@ class SyncFaceRecognitionPerson {
} }
rethrow; rethrow;
} }
final cache = final cache = (await ListFaceRecognitionPerson(_c.withLocalRepo())(account)
await ListFaceRecognitionPerson(_c.withLocalRepo())(account).last; .last)
int personSorter(FaceRecognitionPerson a, FaceRecognitionPerson b) => ..sort(personSorter);
a.name.compareTo(b.name);
final diff = list_util.diffWith(cache, remote, personSorter); final diff = list_util.diffWith(cache, remote, personSorter);
final inserts = diff.onlyInB; final inserts = diff.onlyInB;
_log.info("[call] New people: ${inserts.toReadableString()}"); _log.info("[call] New people: ${inserts.toReadableString()}");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,5 @@
import 'dart:math' as math;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -25,6 +27,8 @@ import 'package:nc_photos/widget/places_browser.dart';
import 'package:np_codegen/np_codegen.dart'; import 'package:np_codegen/np_codegen.dart';
part 'search_landing.g.dart'; part 'search_landing.g.dart';
part 'search_landing/type.dart';
part 'search_landing/view.dart';
class SearchLanding extends StatefulWidget { class SearchLanding extends StatefulWidget {
const SearchLanding({ const SearchLanding({
@ -263,8 +267,7 @@ class _SearchLandingState extends State<SearchLanding> {
.take(10) .take(10)
.map((e) => _LandingPersonItem( .map((e) => _LandingPersonItem(
account: widget.account, account: widget.account,
name: e.name, person: e,
faceUrl: e.getCoverUrl(k.faceThumbSize, k.faceThumbSize),
onTap: () => _onPersonItemTap(e), onTap: () => _onPersonItemTap(e),
)) ))
.toList(); .toList();
@ -303,92 +306,32 @@ class _SearchLandingState extends State<SearchLanding> {
var _locationItems = <_LandingLocationItem>[]; var _locationItems = <_LandingLocationItem>[];
} }
class _LandingPersonItem { class _LandingPersonWidget extends StatelessWidget {
_LandingPersonItem({ const _LandingPersonWidget({
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,
required this.account, required this.account,
required this.person,
required this.label, required this.label,
required this.coverUrl, required this.coverUrl,
required this.fallbackBuilder,
this.onTap, this.onTap,
}) : super(key: key); });
@override @override
build(BuildContext context) { Widget build(BuildContext context) {
final content = Padding( final content = Padding(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Expanded( Center(
child: Center( child: _PersonCoverImage(
child: AspectRatio( dimension: 72,
aspectRatio: 1, account: account,
child: _buildCoverImage(context), person: person,
), coverUrl: coverUrl,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( Expanded(child: _Label(label: label)),
width: 88,
child: Text(
label + "\n",
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
], ],
), ),
); );
@ -402,37 +345,74 @@ class _LandingItemWidget extends StatelessWidget {
} }
} }
Widget _buildCoverImage(BuildContext context) { final Account account;
Widget cover; final Person person;
Widget buildPlaceholder() => Padding( final String label;
padding: const EdgeInsets.all(8), final String? coverUrl;
child: fallbackBuilder(context), final VoidCallback? onTap;
);
try {
cover = NetworkRectThumbnail(
account: account,
imageUrl: coverUrl!,
errorBuilder: (_) => buildPlaceholder(),
);
} catch (_) {
cover = FittedBox(
child: buildPlaceholder(),
);
} }
return ClipRRect( class _LandingLocationWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(128), const _LandingLocationWidget({
child: Container( required this.account,
color: Theme.of(context).listPlaceholderBackgroundColor, required this.label,
constraints: const BoxConstraints.expand(), required this.coverUrl,
child: cover, 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 Account account;
final String label; final String label;
final String? coverUrl; final String? coverUrl;
final Widget Function(BuildContext context) fallbackBuilder;
final VoidCallback? onTap; final VoidCallback? onTap;
} }
class _Label extends StatelessWidget {
const _Label({
required this.label,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 88,
child: Text(
label + "\n",
style: Theme.of(context).textTheme.bodyMedium,
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
);
}
final String label;
}

View file

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

View file

@ -0,0 +1,143 @@
part of '../search_landing.dart';
class _PersonCoverPlaceholder extends StatelessWidget {
const _PersonCoverPlaceholder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.person,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
}
}
class _PersonCoverImage extends StatefulWidget {
const _PersonCoverImage({
required this.dimension,
required this.account,
required this.coverUrl,
required this.person,
});
@override
State<StatefulWidget> createState() => _PersonCoverImageState();
final double dimension;
final Account account;
final String? coverUrl;
final Person person;
}
class _PersonCoverImageState extends State<_PersonCoverImage> {
@override
Widget build(BuildContext context) {
Widget cover;
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;
}
}
cover = Transform(
transform: m,
child: NetworkRectThumbnail(
account: widget.account,
imageUrl: widget.coverUrl!,
errorBuilder: (_) => const _PersonCoverPlaceholder(),
onSize: (size) {
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
_layoutSize = size;
});
});
},
),
);
} catch (_) {
cover = const FittedBox(
child: _PersonCoverPlaceholder(),
);
}
return SizedBox.square(
dimension: widget.dimension,
child: ClipRRect(
borderRadius: BorderRadius.circular(widget.dimension / 2),
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: cover,
),
),
);
}
Size? _layoutSize;
}
class _LocationCoverPlaceholder extends StatelessWidget {
const _LocationCoverPlaceholder();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.location_on,
color: Theme.of(context).listPlaceholderForegroundColor,
),
);
}
}
class _LocationCoverImage extends StatelessWidget {
const _LocationCoverImage({
required this.dimension,
required this.account,
required this.coverUrl,
});
@override
Widget build(BuildContext context) {
Widget cover;
try {
cover = NetworkRectThumbnail(
account: account,
imageUrl: coverUrl!,
errorBuilder: (_) => const _LocationCoverPlaceholder(),
);
} catch (_) {
cover = const FittedBox(
child: _LocationCoverPlaceholder(),
);
}
return SizedBox.square(
dimension: dimension,
child: ClipRRect(
borderRadius: BorderRadius.circular(dimension / 2),
child: Container(
color: Theme.of(context).listPlaceholderBackgroundColor,
constraints: const BoxConstraints.expand(),
child: cover,
),
),
);
}
final double dimension;
final Account account;
final String? coverUrl;
}

View file

@ -1,5 +1,6 @@
import 'package:nc_photos/api/entity_converter.dart'; import 'package:nc_photos/api/entity_converter.dart';
import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/recognize_face_item.dart';
import 'package:np_api/np_api.dart' as api; import 'package:np_api/np_api.dart' as api;
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -15,6 +16,12 @@ void main() {
test("nextcloud hosted in subdir", _filesServerHostedInSubdir); test("nextcloud hosted in subdir", _filesServerHostedInSubdir);
}); });
}); });
group("ApiRecognizeFaceItemConverter", () {
group("fromApi", () {
test("minimum", _recognizeFaceItemMinimum);
test("size", _recognizeFaceItemSize);
});
});
} }
Future<void> _files() async { Future<void> _files() async {
@ -242,3 +249,37 @@ Future<void> _filesServerHostedInSubdir() async {
), ),
); );
} }
void _recognizeFaceItemMinimum() {
const apiItem = api.RecognizeFaceItem(
href: "/remote.php/dav/recognize/admin/faces/test/test1.jpg",
fileId: 123,
);
expect(
ApiRecognizeFaceItemConverter.fromApi(apiItem),
const RecognizeFaceItem(
path: "remote.php/dav/recognize/admin/faces/test/test1.jpg",
fileId: 123,
),
);
}
void _recognizeFaceItemSize() {
const apiItem = api.RecognizeFaceItem(
href: "/remote.php/dav/recognize/admin/faces/test/test1.jpg",
fileId: 123,
fileMetadataSize: {
"width": 1024,
"height": 768,
},
);
expect(
ApiRecognizeFaceItemConverter.fromApi(apiItem),
const RecognizeFaceItem(
path: "remote.php/dav/recognize/admin/faces/test/test1.jpg",
fileId: 123,
fileMetadataWidth: 1024,
fileMetadataHeight: 768,
),
);
}

View file

@ -6,6 +6,7 @@ export 'src/entity/favorite_parser.dart';
export 'src/entity/file_parser.dart'; export 'src/entity/file_parser.dart';
export 'src/entity/nc_album_item_parser.dart'; export 'src/entity/nc_album_item_parser.dart';
export 'src/entity/nc_album_parser.dart'; export 'src/entity/nc_album_parser.dart';
export 'src/entity/recognize_face_item_parser.dart';
export 'src/entity/recognize_face_parser.dart'; export 'src/entity/recognize_face_parser.dart';
export 'src/entity/share_parser.dart'; export 'src/entity/share_parser.dart';
export 'src/entity/sharee_parser.dart'; export 'src/entity/sharee_parser.dart';

View file

@ -269,8 +269,7 @@ class RecognizeFaceItem with EquatableMixin {
final String? etag; final String? etag;
final DateTime? lastModified; final DateTime? lastModified;
final List<JsonObj>? faceDetections; final List<JsonObj>? faceDetections;
// format currently unknown final JsonObj? fileMetadataSize;
final dynamic fileMetadataSize;
final bool? hasPreview; final bool? hasPreview;
final String? realPath; final String? realPath;
final bool? favorite; final bool? favorite;

View file

@ -23,8 +23,7 @@ class RecognizeFaceItemParser extends XmlResponseParser {
String? etag; String? etag;
DateTime? lastModified; DateTime? lastModified;
List<JsonObj>? faceDetections; List<JsonObj>? faceDetections;
// format currently unknown Object? fileMetadataSize;
dynamic fileMetadataSize;
bool? hasPreview; bool? hasPreview;
String? realPath; String? realPath;
bool? favorite; bool? favorite;
@ -69,7 +68,9 @@ class RecognizeFaceItemParser extends XmlResponseParser {
etag: etag, etag: etag,
lastModified: lastModified, lastModified: lastModified,
faceDetections: faceDetections, faceDetections: faceDetections,
fileMetadataSize: fileMetadataSize, fileMetadataSize: fileMetadataSize is Map
? fileMetadataSize.cast<String, dynamic>()
: null,
hasPreview: hasPreview, hasPreview: hasPreview,
realPath: realPath, realPath: realPath,
favorite: favorite, favorite: favorite,
@ -105,7 +106,8 @@ class _PropParser {
: (jsonDecode(child.innerText) as List).cast<JsonObj>(); : (jsonDecode(child.innerText) as List).cast<JsonObj>();
} else if (child.matchQualifiedName("file-metadata-size", } else if (child.matchQualifiedName("file-metadata-size",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) { prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_fileMetadataSize = child.innerText; _fileMetadataSize =
child.innerText.isEmpty ? null : jsonDecode(child.innerText);
} else if (child.matchQualifiedName("has-preview", } else if (child.matchQualifiedName("has-preview",
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) { prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
_hasPreview = child.innerText == "true"; _hasPreview = child.innerText == "true";
@ -127,7 +129,7 @@ class _PropParser {
String? get etag => _etag; String? get etag => _etag;
DateTime? get lastModified => _lastModified; DateTime? get lastModified => _lastModified;
List<JsonObj>? get faceDetections => _faceDetections; List<JsonObj>? get faceDetections => _faceDetections;
dynamic get fileMetadataSize => _fileMetadataSize; Object? get fileMetadataSize => _fileMetadataSize;
bool? get hasPreview => _hasPreview; bool? get hasPreview => _hasPreview;
String? get realPath => _realPath; String? get realPath => _realPath;
bool? get favorite => _favorite; bool? get favorite => _favorite;
@ -140,7 +142,8 @@ class _PropParser {
String? _etag; String? _etag;
DateTime? _lastModified; DateTime? _lastModified;
List<JsonObj>? _faceDetections; List<JsonObj>? _faceDetections;
dynamic _fileMetadataSize; // size can be a map or a list if the size is not known (well...)
Object? _fileMetadataSize;
bool? _hasPreview; bool? _hasPreview;
String? _realPath; String? _realPath;
bool? _favorite; bool? _favorite;

View file

@ -6,7 +6,9 @@ class ApiRecognize {
ApiRecognizeFaces faces() => ApiRecognizeFaces(this); ApiRecognizeFaces faces() => ApiRecognizeFaces(this);
ApiRecognizeFace face(String name) => ApiRecognizeFace(this, name); ApiRecognizeFace face(String name) => ApiRecognizeFace(this, name);
String get _path => "remote.php/dav/recognize/$userId"; String get _userPath => "$path/$userId";
static String get path => "remote.php/dav/recognize";
final Api api; final Api api;
final String userId; final String userId;
@ -17,7 +19,7 @@ class ApiRecognizeFaces {
const ApiRecognizeFaces(this.recognize); const ApiRecognizeFaces(this.recognize);
Future<Response> propfind() async { Future<Response> propfind() async {
final endpoint = "${recognize._path}/faces"; final endpoint = "${recognize._userPath}/faces";
try { try {
return await api.request("PROPFIND", endpoint); return await api.request("PROPFIND", endpoint);
} catch (e) { } catch (e) {
@ -116,7 +118,7 @@ class ApiRecognizeFace {
} }
} }
String get _path => "${recognize._path}/faces/$name"; String get _path => "${recognize._userPath}/faces/$name";
Api get api => recognize.api; Api get api => recognize.api;
final ApiRecognize recognize; final ApiRecognize recognize;

View file

@ -1,5 +1,4 @@
import 'package:np_api/np_api.dart'; import 'package:np_api/np_api.dart';
import 'package:np_api/src/entity/recognize_face_item_parser.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
void main() { void main() {
@ -7,6 +6,7 @@ void main() {
group("parse", () { group("parse", () {
test("empty", _empty); test("empty", _empty);
test("image", _image); test("image", _image);
test("imageWithSize", _imageWithSize);
}); });
}); });
} }
@ -143,7 +143,7 @@ Future<void> _image() async {
"title": "test", "title": "test",
}, },
], ],
fileMetadataSize: "[]", fileMetadataSize: null,
hasPreview: true, hasPreview: true,
realPath: "/admin/files/test1.jpg", realPath: "/admin/files/test1.jpg",
favorite: false, favorite: false,
@ -152,3 +152,44 @@ Future<void> _image() async {
], ],
); );
} }
Future<void> _imageWithSize() async {
const xml = """
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/test/</d:href>
<d:propstat>
<d:prop>
<nc:file-metadata-size/>
</d:prop>
<d:status>HTTP/1.1 404 Not Found</d:status>
</d:propstat>
</d:response>
<d:response>
<d:href>/remote.php/dav/recognize/admin/faces/test/test1.jpg</d:href>
<d:propstat>
<d:prop>
<nc:file-metadata-size>{&quot;width&quot;:1024,&quot;height&quot;:768}</nc:file-metadata-size>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
""";
final results = await RecognizeFaceItemParser().parse(xml);
expect(
results,
[
const RecognizeFaceItem(
href: "/remote.php/dav/recognize/admin/faces/test/"),
const RecognizeFaceItem(
href: "/remote.php/dav/recognize/admin/faces/test/test1.jpg",
fileMetadataSize: {
"width": 1024,
"height": 768,
},
),
],
);
}