mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +01:00
Support face provided by Recognize app
This commit is contained in:
parent
c920a6bc36
commit
738883387a
40 changed files with 2839 additions and 121 deletions
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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() =>
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}");
|
||||||
|
|
47
app/lib/entity/person/adapter/recognize.dart
Normal file
47
app/lib/entity/person/adapter/recognize.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/person.dart';
|
||||||
|
import 'package:nc_photos/entity/person/adapter.dart';
|
||||||
|
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
|
||||||
|
import 'package:nc_photos/entity/person_face.dart';
|
||||||
|
import 'package:nc_photos/object_extension.dart';
|
||||||
|
import 'package:nc_photos/use_case/find_file_descriptor.dart';
|
||||||
|
import 'package:nc_photos/use_case/recognize_face/list_recognize_face_item.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
||||||
|
part 'recognize.g.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class PersonRecognizeAdapter implements PersonAdapter {
|
||||||
|
PersonRecognizeAdapter(this._c, this.account, this.person)
|
||||||
|
: _provider = person.contentProvider as PersonRecognizeProvider;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<PersonFace>> listFace() {
|
||||||
|
return ListRecognizeFaceItem(_c)(account, _provider.face)
|
||||||
|
.asyncMap((faces) async {
|
||||||
|
final found = await FindFileDescriptor(_c)(
|
||||||
|
account,
|
||||||
|
faces.map((e) => e.fileId).toList(),
|
||||||
|
onFileNotFound: (fileId) {
|
||||||
|
_log.warning("[listFace] File not found: $fileId");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return faces
|
||||||
|
.map((i) {
|
||||||
|
final f = found.firstWhereOrNull((e) => e.fdId == i.fileId);
|
||||||
|
return f?.run(BasicPersonFace.new);
|
||||||
|
})
|
||||||
|
.whereNotNull()
|
||||||
|
.toList();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
final Account account;
|
||||||
|
final Person person;
|
||||||
|
|
||||||
|
final PersonRecognizeProvider _provider;
|
||||||
|
}
|
15
app/lib/entity/person/adapter/recognize.g.dart
Normal file
15
app/lib/entity/person/adapter/recognize.g.dart
Normal 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");
|
||||||
|
}
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
111
app/lib/entity/person/content_provider/recognize.dart
Normal file
111
app/lib/entity/person/content_provider/recognize.dart
Normal 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;
|
||||||
|
}
|
14
app/lib/entity/person/content_provider/recognize.g.dart
Normal file
14
app/lib/entity/person/content_provider/recognize.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'recognize.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$PersonRecognizeProviderToString on PersonRecognizeProvider {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "PersonRecognizeProvider {account: $account, face: $face, items: ${items == null ? null : "[length: ${items!.length}]"}}";
|
||||||
|
}
|
||||||
|
}
|
26
app/lib/entity/recognize_face.dart
Normal file
26
app/lib/entity/recognize_face.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
part 'recognize_face.g.dart';
|
||||||
|
|
||||||
|
/// A person's face recognized by the Recognize app
|
||||||
|
///
|
||||||
|
/// Beware that the terminology used in Recognize is different to
|
||||||
|
/// FaceRecognition, which is also followed by this app. A face in Recognize is
|
||||||
|
/// a person in FaceRecognition and this app
|
||||||
|
@toString
|
||||||
|
class RecognizeFace with EquatableMixin {
|
||||||
|
const RecognizeFace({
|
||||||
|
required this.label,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool get isNamed => int.tryParse(label) == null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [label];
|
||||||
|
|
||||||
|
final String label;
|
||||||
|
}
|
14
app/lib/entity/recognize_face.g.dart
Normal file
14
app/lib/entity/recognize_face.g.dart
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
212
app/lib/entity/recognize_face/data_source.dart
Normal file
212
app/lib/entity/recognize_face/data_source.dart
Normal file
|
@ -0,0 +1,212 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/entity_converter.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face/repo.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face_item.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||||
|
import 'package:nc_photos/entity/sqlite/table.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite/type_converter.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/map_extension.dart';
|
||||||
|
import 'package:nc_photos/np_api_util.dart';
|
||||||
|
import 'package:np_api/np_api.dart' as api;
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
|
part 'data_source.g.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class RecognizeFaceRemoteDataSource implements RecognizeFaceDataSource {
|
||||||
|
const RecognizeFaceRemoteDataSource();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RecognizeFace>> getFaces(Account account) async {
|
||||||
|
_log.info("[getFaces] account: ${account.userId}");
|
||||||
|
final response = await ApiUtil.fromAccount(account)
|
||||||
|
.recognize(account.userId.raw)
|
||||||
|
.faces()
|
||||||
|
.propfind();
|
||||||
|
if (!response.isGood) {
|
||||||
|
_log.severe("[getFaces] Failed requesting server: $response");
|
||||||
|
throw ApiException(
|
||||||
|
response: response,
|
||||||
|
message: "Server responed with an error: HTTP ${response.statusCode}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final apiFaces = await api.RecognizeFaceParser().parse(response.body);
|
||||||
|
return apiFaces
|
||||||
|
.map(ApiRecognizeFaceConverter.fromApi)
|
||||||
|
.where((e) => e.label.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RecognizeFaceItem>> getItems(
|
||||||
|
Account account, RecognizeFace face) async {
|
||||||
|
_log.info("[getItems] account: ${account.userId}, face: ${face.label}");
|
||||||
|
final response = await ApiUtil.fromAccount(account)
|
||||||
|
.recognize(account.userId.raw)
|
||||||
|
.face(face.label)
|
||||||
|
.propfind(
|
||||||
|
getcontentlength: 1,
|
||||||
|
getcontenttype: 1,
|
||||||
|
getetag: 1,
|
||||||
|
getlastmodified: 1,
|
||||||
|
faceDetections: 1,
|
||||||
|
fileMetadataSize: 1,
|
||||||
|
hasPreview: 1,
|
||||||
|
realpath: 1,
|
||||||
|
favorite: 1,
|
||||||
|
fileid: 1,
|
||||||
|
);
|
||||||
|
if (!response.isGood) {
|
||||||
|
_log.severe("[getItems] Failed requesting server: $response");
|
||||||
|
throw ApiException(
|
||||||
|
response: response,
|
||||||
|
message: "Server responed with an error: HTTP ${response.statusCode}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final apiItems = await api.RecognizeFaceItemParser().parse(response.body);
|
||||||
|
return apiItems
|
||||||
|
.where((f) => f.fileId != null)
|
||||||
|
.map(ApiRecognizeFaceItemConverter.fromApi)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
}) async {
|
||||||
|
final results = await Future.wait(faces.map((f) async {
|
||||||
|
try {
|
||||||
|
return MapEntry(f, await getItems(account, f));
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe("[getMultiFaceItems] Failed while querying face: $f", e,
|
||||||
|
stackTrace);
|
||||||
|
onError?.call(f, e, stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return results.whereNotNull().toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<RecognizeFace, RecognizeFaceItem>> getMultiFaceLastItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
}) async {
|
||||||
|
final results = await getMultiFaceItems(account, faces, onError: onError);
|
||||||
|
return results
|
||||||
|
.map((key, value) => MapEntry(key, maxBy(value, (e) => e.fileId)!));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class RecognizeFaceSqliteDbDataSource implements RecognizeFaceDataSource {
|
||||||
|
const RecognizeFaceSqliteDbDataSource(this.sqliteDb);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RecognizeFace>> getFaces(Account account) async {
|
||||||
|
_log.info("[getFaces] $account");
|
||||||
|
final dbFaces = await sqliteDb.use((db) async {
|
||||||
|
return await db.allRecognizeFaces(
|
||||||
|
account: sql.ByAccount.app(account),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
return dbFaces
|
||||||
|
.map((f) {
|
||||||
|
try {
|
||||||
|
return SqliteRecognizeFaceConverter.fromSql(f);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.severe(
|
||||||
|
"[getFaces] Failed while converting DB entry", e, stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whereNotNull()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<RecognizeFaceItem>> getItems(
|
||||||
|
Account account, RecognizeFace face) async {
|
||||||
|
_log.info("[getItems] $face");
|
||||||
|
final results = await getMultiFaceItems(account, [face]);
|
||||||
|
return results[face]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
List<RecognizeFaceItemSort>? orderBy,
|
||||||
|
int? limit,
|
||||||
|
}) async {
|
||||||
|
_log.info("[getMultiFaceItems] ${faces.toReadableString()}");
|
||||||
|
final dbItems = await sqliteDb.use((db) async {
|
||||||
|
final results = await Future.wait(faces.map((f) async {
|
||||||
|
try {
|
||||||
|
return MapEntry(
|
||||||
|
f,
|
||||||
|
await db.recognizeFaceItemsByParentLabel(
|
||||||
|
account: sql.ByAccount.app(account),
|
||||||
|
label: f.label,
|
||||||
|
orderBy: orderBy?.toOrderingItem(db).toList(),
|
||||||
|
limit: limit,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
onError?.call(f, e, stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
return results.whereNotNull().toMap();
|
||||||
|
});
|
||||||
|
return dbItems.entries
|
||||||
|
.map((entry) {
|
||||||
|
final face = entry.key;
|
||||||
|
try {
|
||||||
|
return MapEntry(
|
||||||
|
face,
|
||||||
|
entry.value
|
||||||
|
.map((i) => SqliteRecognizeFaceItemConverter.fromSql(
|
||||||
|
account.userId.raw, face.label, i))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
onError?.call(face, e, stackTrace);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whereNotNull()
|
||||||
|
.toMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<RecognizeFace, RecognizeFaceItem>> getMultiFaceLastItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
}) async {
|
||||||
|
final results = await getMultiFaceItems(
|
||||||
|
account,
|
||||||
|
faces,
|
||||||
|
onError: onError,
|
||||||
|
orderBy: [RecognizeFaceItemSort.fileIdDesc],
|
||||||
|
limit: 1,
|
||||||
|
);
|
||||||
|
return (results..removeWhere((key, value) => value.isEmpty))
|
||||||
|
.map((key, value) => MapEntry(key, value.first));
|
||||||
|
}
|
||||||
|
|
||||||
|
final sql.SqliteDb sqliteDb;
|
||||||
|
}
|
25
app/lib/entity/recognize_face/data_source.g.dart
Normal file
25
app/lib/entity/recognize_face/data_source.g.dart
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'data_source.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// NpLogGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$RecognizeFaceRemoteDataSourceNpLog
|
||||||
|
on RecognizeFaceRemoteDataSource {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log =
|
||||||
|
Logger("entity.recognize_face.data_source.RecognizeFaceRemoteDataSource");
|
||||||
|
}
|
||||||
|
|
||||||
|
extension _$RecognizeFaceSqliteDbDataSourceNpLog
|
||||||
|
on RecognizeFaceSqliteDbDataSource {
|
||||||
|
// ignore: unused_element
|
||||||
|
Logger get _log => log;
|
||||||
|
|
||||||
|
static final log = Logger(
|
||||||
|
"entity.recognize_face.data_source.RecognizeFaceSqliteDbDataSource");
|
||||||
|
}
|
75
app/lib/entity/recognize_face/repo.dart
Normal file
75
app/lib/entity/recognize_face/repo.dart
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face_item.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
|
part 'repo.g.dart';
|
||||||
|
|
||||||
|
abstract class RecognizeFaceRepo {
|
||||||
|
/// Query all [RecognizeFace]s belonging to [account]
|
||||||
|
Stream<List<RecognizeFace>> getFaces(Account account);
|
||||||
|
|
||||||
|
/// Query all items belonging to [face]
|
||||||
|
Stream<List<RecognizeFaceItem>> getItems(Account account, RecognizeFace face);
|
||||||
|
|
||||||
|
/// Query all items belonging to each face
|
||||||
|
Stream<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A repo that simply relay the call to the backed [NcAlbumDataSource]
|
||||||
|
@npLog
|
||||||
|
class BasicRecognizeFaceRepo implements RecognizeFaceRepo {
|
||||||
|
const BasicRecognizeFaceRepo(this.dataSrc);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<RecognizeFace>> getFaces(Account account) async* {
|
||||||
|
yield await dataSrc.getFaces(account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<RecognizeFaceItem>> getItems(
|
||||||
|
Account account, RecognizeFace face) async* {
|
||||||
|
yield await dataSrc.getItems(account, face);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
}) async* {
|
||||||
|
yield await dataSrc.getMultiFaceItems(account, faces, onError: onError);
|
||||||
|
}
|
||||||
|
|
||||||
|
final RecognizeFaceDataSource dataSrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class RecognizeFaceDataSource {
|
||||||
|
/// Query all [RecognizeFace]s belonging to [account]
|
||||||
|
Future<List<RecognizeFace>> getFaces(Account account);
|
||||||
|
|
||||||
|
/// Query all items belonging to [face]
|
||||||
|
Future<List<RecognizeFaceItem>> getItems(Account account, RecognizeFace face);
|
||||||
|
|
||||||
|
/// Query all items belonging to each face
|
||||||
|
Future<Map<RecognizeFace, List<RecognizeFaceItem>>> getMultiFaceItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Query the last items belonging to each face
|
||||||
|
Future<Map<RecognizeFace, RecognizeFaceItem>> getMultiFaceLastItems(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
});
|
||||||
|
}
|
15
app/lib/entity/recognize_face/repo.g.dart
Normal file
15
app/lib/entity/recognize_face/repo.g.dart
Normal 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");
|
||||||
|
}
|
113
app/lib/entity/recognize_face_item.dart
Normal file
113
app/lib/entity/recognize_face_item.dart
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:nc_photos/entity/file.dart';
|
||||||
|
import 'package:np_api/np_api.dart' as api;
|
||||||
|
import 'package:np_common/string_extension.dart';
|
||||||
|
import 'package:to_string/to_string.dart';
|
||||||
|
|
||||||
|
part 'recognize_face_item.g.dart';
|
||||||
|
|
||||||
|
@ToString(ignoreNull: true)
|
||||||
|
class RecognizeFaceItem with EquatableMixin {
|
||||||
|
const RecognizeFaceItem({
|
||||||
|
required this.path,
|
||||||
|
required this.fileId,
|
||||||
|
this.contentLength,
|
||||||
|
this.contentType,
|
||||||
|
this.etag,
|
||||||
|
this.lastModified,
|
||||||
|
this.hasPreview,
|
||||||
|
this.realPath,
|
||||||
|
this.isFavorite,
|
||||||
|
this.fileMetadataWidth,
|
||||||
|
this.fileMetadataHeight,
|
||||||
|
this.faceDetections,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => _$toString();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
path,
|
||||||
|
fileId,
|
||||||
|
contentLength,
|
||||||
|
contentType,
|
||||||
|
etag,
|
||||||
|
lastModified,
|
||||||
|
hasPreview,
|
||||||
|
realPath,
|
||||||
|
isFavorite,
|
||||||
|
fileMetadataWidth,
|
||||||
|
fileMetadataHeight,
|
||||||
|
faceDetections,
|
||||||
|
];
|
||||||
|
|
||||||
|
final String path;
|
||||||
|
final int fileId;
|
||||||
|
final int? contentLength;
|
||||||
|
final String? contentType;
|
||||||
|
final String? etag;
|
||||||
|
final DateTime? lastModified;
|
||||||
|
final bool? hasPreview;
|
||||||
|
final String? realPath;
|
||||||
|
final bool? isFavorite;
|
||||||
|
final int? fileMetadataWidth;
|
||||||
|
final int? fileMetadataHeight;
|
||||||
|
final List<Map<String, dynamic>>? faceDetections;
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RecognizeFaceItemExtension on RecognizeFaceItem {
|
||||||
|
/// Return the path of this item with the DAV part stripped
|
||||||
|
///
|
||||||
|
/// WebDAV file path: remote.php/dav/recognize/{userId}/faces/{face}/{strippedPath}.
|
||||||
|
/// If this path points to the user's root album path, return "."
|
||||||
|
String get strippedPath {
|
||||||
|
if (!path.startsWith("${api.ApiRecognize.path}/")) {
|
||||||
|
throw ArgumentError("Unsupported path: $path");
|
||||||
|
}
|
||||||
|
var begin = "${api.ApiRecognize.path}/".length;
|
||||||
|
begin = path.indexOf("/", begin);
|
||||||
|
if (begin == -1) {
|
||||||
|
throw ArgumentError("Unsupported path: $path");
|
||||||
|
}
|
||||||
|
// /faces/{face}/{strippedPath}
|
||||||
|
if (path.slice(begin, begin + 6) != "/faces") {
|
||||||
|
throw ArgumentError("Unsupported path: $path");
|
||||||
|
}
|
||||||
|
begin += 7;
|
||||||
|
// {face}/{strippedPath}
|
||||||
|
begin = path.indexOf("/", begin);
|
||||||
|
if (begin == -1) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
return path.slice(begin + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool compareIdentity(RecognizeFaceItem other) => fileId == other.fileId;
|
||||||
|
|
||||||
|
int get identityHashCode => fileId.hashCode;
|
||||||
|
|
||||||
|
static int identityComparator(RecognizeFaceItem a, RecognizeFaceItem b) =>
|
||||||
|
a.fileId.compareTo(b.fileId);
|
||||||
|
|
||||||
|
File toFile() {
|
||||||
|
Metadata? metadata;
|
||||||
|
if (fileMetadataWidth != null && fileMetadataHeight != null) {
|
||||||
|
metadata = Metadata(
|
||||||
|
imageWidth: fileMetadataWidth,
|
||||||
|
imageHeight: fileMetadataHeight,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return File(
|
||||||
|
path: realPath ?? path,
|
||||||
|
fileId: fileId,
|
||||||
|
contentLength: contentLength,
|
||||||
|
contentType: contentType,
|
||||||
|
etag: etag,
|
||||||
|
lastModified: lastModified,
|
||||||
|
hasPreview: hasPreview,
|
||||||
|
isFavorite: isFavorite,
|
||||||
|
metadata: metadata,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
14
app/lib/entity/recognize_face_item.g.dart
Normal file
14
app/lib/entity/recognize_face_item.g.dart
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'recognize_face_item.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// ToStringGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
extension _$RecognizeFaceItemToString on RecognizeFaceItem {
|
||||||
|
String _$toString() {
|
||||||
|
// ignore: unnecessary_string_interpolations
|
||||||
|
return "RecognizeFaceItem {path: $path, fileId: $fileId, ${contentLength == null ? "" : "contentLength: $contentLength, "}${contentType == null ? "" : "contentType: $contentType, "}${etag == null ? "" : "etag: $etag, "}${lastModified == null ? "" : "lastModified: $lastModified, "}${hasPreview == null ? "" : "hasPreview: $hasPreview, "}${realPath == null ? "" : "realPath: $realPath, "}${isFavorite == null ? "" : "isFavorite: $isFavorite, "}${fileMetadataWidth == null ? "" : "fileMetadataWidth: $fileMetadataWidth, "}${fileMetadataHeight == null ? "" : "fileMetadataHeight: $fileMetadataHeight, "}${faceDetections == null ? "" : "faceDetections: [length: ${faceDetections!.length}]"}}";
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
104
app/lib/entity/sqlite/table.g.dart
Normal file
104
app/lib/entity/sqlite/table.g.dart
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'table.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// DriftTableSortGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
enum RecognizeFaceItemSort {
|
||||||
|
rowIdAsc,
|
||||||
|
rowIdDesc,
|
||||||
|
parentAsc,
|
||||||
|
parentDesc,
|
||||||
|
relativePathAsc,
|
||||||
|
relativePathDesc,
|
||||||
|
fileIdAsc,
|
||||||
|
fileIdDesc,
|
||||||
|
contentLengthAsc,
|
||||||
|
contentLengthDesc,
|
||||||
|
contentTypeAsc,
|
||||||
|
contentTypeDesc,
|
||||||
|
etagAsc,
|
||||||
|
etagDesc,
|
||||||
|
lastModifiedAsc,
|
||||||
|
lastModifiedDesc,
|
||||||
|
hasPreviewAsc,
|
||||||
|
hasPreviewDesc,
|
||||||
|
realPathAsc,
|
||||||
|
realPathDesc,
|
||||||
|
isFavoriteAsc,
|
||||||
|
isFavoriteDesc,
|
||||||
|
fileMetadataWidthAsc,
|
||||||
|
fileMetadataWidthDesc,
|
||||||
|
fileMetadataHeightAsc,
|
||||||
|
fileMetadataHeightDesc,
|
||||||
|
faceDetectionsAsc,
|
||||||
|
faceDetectionsDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RecognizeFaceItemSortIterableExtension
|
||||||
|
on Iterable<RecognizeFaceItemSort> {
|
||||||
|
Iterable<OrderingTerm> toOrderingItem(SqliteDb db) {
|
||||||
|
return map((s) {
|
||||||
|
switch (s) {
|
||||||
|
case RecognizeFaceItemSort.rowIdAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.rowId);
|
||||||
|
case RecognizeFaceItemSort.rowIdDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.rowId);
|
||||||
|
case RecognizeFaceItemSort.parentAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.parent);
|
||||||
|
case RecognizeFaceItemSort.parentDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.parent);
|
||||||
|
case RecognizeFaceItemSort.relativePathAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.relativePath);
|
||||||
|
case RecognizeFaceItemSort.relativePathDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.relativePath);
|
||||||
|
case RecognizeFaceItemSort.fileIdAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.fileId);
|
||||||
|
case RecognizeFaceItemSort.fileIdDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.fileId);
|
||||||
|
case RecognizeFaceItemSort.contentLengthAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.contentLength);
|
||||||
|
case RecognizeFaceItemSort.contentLengthDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.contentLength);
|
||||||
|
case RecognizeFaceItemSort.contentTypeAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.contentType);
|
||||||
|
case RecognizeFaceItemSort.contentTypeDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.contentType);
|
||||||
|
case RecognizeFaceItemSort.etagAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.etag);
|
||||||
|
case RecognizeFaceItemSort.etagDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.etag);
|
||||||
|
case RecognizeFaceItemSort.lastModifiedAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.lastModified);
|
||||||
|
case RecognizeFaceItemSort.lastModifiedDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.lastModified);
|
||||||
|
case RecognizeFaceItemSort.hasPreviewAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.hasPreview);
|
||||||
|
case RecognizeFaceItemSort.hasPreviewDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.hasPreview);
|
||||||
|
case RecognizeFaceItemSort.realPathAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.realPath);
|
||||||
|
case RecognizeFaceItemSort.realPathDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.realPath);
|
||||||
|
case RecognizeFaceItemSort.isFavoriteAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.isFavorite);
|
||||||
|
case RecognizeFaceItemSort.isFavoriteDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.isFavorite);
|
||||||
|
case RecognizeFaceItemSort.fileMetadataWidthAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.fileMetadataWidth);
|
||||||
|
case RecognizeFaceItemSort.fileMetadataWidthDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.fileMetadataWidth);
|
||||||
|
case RecognizeFaceItemSort.fileMetadataHeightAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.fileMetadataHeight);
|
||||||
|
case RecognizeFaceItemSort.fileMetadataHeightDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.fileMetadataHeight);
|
||||||
|
case RecognizeFaceItemSort.faceDetectionsAsc:
|
||||||
|
return OrderingTerm.asc(db.recognizeFaceItems.faceDetections);
|
||||||
|
case RecognizeFaceItemSort.faceDetectionsDesc:
|
||||||
|
return OrderingTerm.desc(db.recognizeFaceItems.faceDetections);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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()}");
|
||||||
|
|
13
app/lib/use_case/recognize_face/list_recognize_face.dart
Normal file
13
app/lib/use_case/recognize_face/list_recognize_face.dart
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face.dart';
|
||||||
|
|
||||||
|
class ListRecognizeFace {
|
||||||
|
const ListRecognizeFace(this._c);
|
||||||
|
|
||||||
|
/// List all [RecognizeFace]s belonging to [account]
|
||||||
|
Stream<List<RecognizeFace>> call(Account account) =>
|
||||||
|
_c.recognizeFaceRepo.getFaces(account);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face_item.dart';
|
||||||
|
import 'package:np_common/type.dart';
|
||||||
|
|
||||||
|
class ListRecognizeFaceItem {
|
||||||
|
const ListRecognizeFaceItem(this._c);
|
||||||
|
|
||||||
|
/// List all [RecognizeFaceItem]s belonging to [face]
|
||||||
|
Stream<List<RecognizeFaceItem>> call(Account account, RecognizeFace face) =>
|
||||||
|
_c.recognizeFaceRepo.getItems(account, face);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListMultipleRecognizeFaceItem {
|
||||||
|
const ListMultipleRecognizeFaceItem(this._c);
|
||||||
|
|
||||||
|
/// List all [RecognizeFaceItem]s belonging to each face
|
||||||
|
Stream<Map<RecognizeFace, List<RecognizeFaceItem>>> call(
|
||||||
|
Account account,
|
||||||
|
List<RecognizeFace> faces, {
|
||||||
|
ErrorWithValueHandler<RecognizeFace>? onError,
|
||||||
|
}) =>
|
||||||
|
_c.recognizeFaceRepo.getMultiFaceItems(account, faces, onError: onError);
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
288
app/lib/use_case/recognize_face/sync_recognize_face.dart
Normal file
288
app/lib/use_case/recognize_face/sync_recognize_face.dart
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:drift/drift.dart' as sql;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/di_container.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face.dart';
|
||||||
|
import 'package:nc_photos/entity/recognize_face_item.dart';
|
||||||
|
import 'package:nc_photos/entity/sqlite/database.dart' as sql;
|
||||||
|
import 'package:nc_photos/entity/sqlite/type_converter.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/iterable_extension.dart';
|
||||||
|
import 'package:nc_photos/list_util.dart' as list_util;
|
||||||
|
import 'package:nc_photos/map_extension.dart';
|
||||||
|
import 'package:nc_photos/use_case/recognize_face/list_recognize_face.dart';
|
||||||
|
import 'package:nc_photos/use_case/recognize_face/list_recognize_face_item.dart';
|
||||||
|
import 'package:np_codegen/np_codegen.dart';
|
||||||
|
|
||||||
|
part 'sync_recognize_face.g.dart';
|
||||||
|
|
||||||
|
@npLog
|
||||||
|
class SyncRecognizeFace {
|
||||||
|
const SyncRecognizeFace(this._c);
|
||||||
|
|
||||||
|
/// Sync people in cache db with remote server
|
||||||
|
///
|
||||||
|
/// Return if any people were updated
|
||||||
|
Future<bool> call(Account account) async {
|
||||||
|
_log.info("[call] Sync people with remote");
|
||||||
|
final faces = await _getFaceResults(account);
|
||||||
|
if (faces == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var shouldUpdate = faces.inserts.isNotEmpty ||
|
||||||
|
faces.deletes.isNotEmpty ||
|
||||||
|
faces.updates.isNotEmpty;
|
||||||
|
final items =
|
||||||
|
await _getFaceItemResults(account, faces.results.values.toList());
|
||||||
|
shouldUpdate = shouldUpdate ||
|
||||||
|
items.values.any((e) =>
|
||||||
|
e.inserts.isNotEmpty ||
|
||||||
|
e.deletes.isNotEmpty ||
|
||||||
|
e.updates.isNotEmpty);
|
||||||
|
if (!shouldUpdate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _c.sqliteDb.use((db) async {
|
||||||
|
final dbAccount = await db.accountOf(account);
|
||||||
|
await db.batch((batch) {
|
||||||
|
for (final d in faces.deletes) {
|
||||||
|
batch.deleteWhere(
|
||||||
|
db.recognizeFaces,
|
||||||
|
(sql.$RecognizeFacesTable t) =>
|
||||||
|
t.account.equals(dbAccount.rowId) &
|
||||||
|
t.label.equals(faces.results[d]!.label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final u in faces.updates) {
|
||||||
|
batch.update(
|
||||||
|
db.recognizeFaces,
|
||||||
|
sql.RecognizeFacesCompanion(
|
||||||
|
label: sql.Value(faces.results[u]!.label),
|
||||||
|
),
|
||||||
|
where: (sql.$RecognizeFacesTable t) =>
|
||||||
|
t.account.equals(dbAccount.rowId) &
|
||||||
|
t.label.equals(faces.results[u]!.label),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final i in faces.inserts) {
|
||||||
|
batch.insert(
|
||||||
|
db.recognizeFaces,
|
||||||
|
SqliteRecognizeFaceConverter.toSql(dbAccount, faces.results[i]!),
|
||||||
|
mode: sql.InsertMode.insertOrIgnore,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// update each item
|
||||||
|
for (final f in faces.results.values) {
|
||||||
|
try {
|
||||||
|
await _syncDbForFaceItem(db, dbAccount, f, items[f]!);
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
_log.shout("[call] Failed to update db for face: $f", e, stackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_FaceResult?> _getFaceResults(Account account) async {
|
||||||
|
int faceSorter(RecognizeFace a, RecognizeFace b) =>
|
||||||
|
a.label.compareTo(b.label);
|
||||||
|
late final List<RecognizeFace> remote;
|
||||||
|
try {
|
||||||
|
remote = (await ListRecognizeFace(_c.withRemoteRepo())(account).last)
|
||||||
|
..sort(faceSorter);
|
||||||
|
} catch (e) {
|
||||||
|
if (e is ApiException && e.response.statusCode == 404) {
|
||||||
|
// recognize app probably not installed, ignore
|
||||||
|
_log.info("[_getFaceResults] Recognize app not installed");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
final cache = (await ListRecognizeFace(_c.withLocalRepo())(account).last)
|
||||||
|
..sort(faceSorter);
|
||||||
|
final diff = list_util.diffWith(cache, remote, faceSorter);
|
||||||
|
final inserts = diff.onlyInB;
|
||||||
|
_log.info("[_getFaceResults] New face: ${inserts.toReadableString()}");
|
||||||
|
final deletes = diff.onlyInA;
|
||||||
|
_log.info("[_getFaceResults] Removed face: ${deletes.toReadableString()}");
|
||||||
|
final updates = remote.where((r) {
|
||||||
|
final c = cache.firstWhereOrNull((c) => c.label == r.label);
|
||||||
|
return c != null && c != r;
|
||||||
|
}).toList();
|
||||||
|
_log.info("[_getFaceResults] Updated face: ${updates.toReadableString()}");
|
||||||
|
return _FaceResult(
|
||||||
|
results: remote.map((e) => MapEntry(e.label, e)).toMap(),
|
||||||
|
inserts: inserts.map((e) => e.label).toList(),
|
||||||
|
updates: updates.map((e) => e.label).toList(),
|
||||||
|
deletes: deletes.map((e) => e.label).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<RecognizeFace, _FaceItemResult>> _getFaceItemResults(
|
||||||
|
Account account, List<RecognizeFace> faces) async {
|
||||||
|
Object? firstError;
|
||||||
|
StackTrace? firstStackTrace;
|
||||||
|
final remote = await ListMultipleRecognizeFaceItem(_c.withRemoteRepo())(
|
||||||
|
account,
|
||||||
|
faces,
|
||||||
|
onError: (f, e, stackTrace) {
|
||||||
|
_log.severe(
|
||||||
|
"[_getFaceItemResults] Failed while listing remote face: $f",
|
||||||
|
e,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
if (firstError == null) {
|
||||||
|
firstError = e;
|
||||||
|
firstStackTrace = stackTrace;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
).last;
|
||||||
|
if (firstError != null) {
|
||||||
|
Error.throwWithStackTrace(
|
||||||
|
firstError!, firstStackTrace ?? StackTrace.current);
|
||||||
|
}
|
||||||
|
final cache = await ListMultipleRecognizeFaceItem(_c.withLocalRepo())(
|
||||||
|
account,
|
||||||
|
faces,
|
||||||
|
onError: (f, e, stackTrace) {
|
||||||
|
_log.severe("[_getFaceItemResults] Failed while listing cache face: $f",
|
||||||
|
e, stackTrace);
|
||||||
|
},
|
||||||
|
).last;
|
||||||
|
|
||||||
|
int itemSorter(RecognizeFaceItem a, RecognizeFaceItem b) =>
|
||||||
|
a.fileId.compareTo(b.fileId);
|
||||||
|
final results = <RecognizeFace, _FaceItemResult>{};
|
||||||
|
for (final f in faces) {
|
||||||
|
final thisCache = (cache[f] ?? [])..sort(itemSorter);
|
||||||
|
final thisRemote = (remote[f] ?? [])..sort(itemSorter);
|
||||||
|
final diff = list_util.diffWith<RecognizeFaceItem>(
|
||||||
|
thisCache, thisRemote, itemSorter);
|
||||||
|
final inserts = diff.onlyInB;
|
||||||
|
_log.info(
|
||||||
|
"[_getFaceItemResults] New item: ${inserts.toReadableString()}");
|
||||||
|
final deletes = diff.onlyInA;
|
||||||
|
_log.info(
|
||||||
|
"[_getFaceItemResults] Removed item: ${deletes.toReadableString()}");
|
||||||
|
final updates = thisRemote.where((r) {
|
||||||
|
final c = thisCache.firstWhereOrNull((c) => c.fileId == r.fileId);
|
||||||
|
return c != null && c != r;
|
||||||
|
}).toList();
|
||||||
|
_log.info(
|
||||||
|
"[_getFaceItemResults] Updated item: ${updates.toReadableString()}");
|
||||||
|
results[f] = _FaceItemResult(
|
||||||
|
results: thisRemote.map((e) => MapEntry(e.fileId, e)).toMap(),
|
||||||
|
inserts: inserts.map((e) => e.fileId).toList(),
|
||||||
|
updates: updates.map((e) => e.fileId).toList(),
|
||||||
|
deletes: deletes.map((e) => e.fileId).toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future<_FaceItemResult?> _getFaceItemResults(
|
||||||
|
// Account account, RecognizeFace face) async {
|
||||||
|
// late final List<RecognizeFaceItem> remote;
|
||||||
|
// try {
|
||||||
|
// remote =
|
||||||
|
// await ListRecognizeFaceItem(_c.withRemoteRepo())(account, face).last;
|
||||||
|
// } catch (e) {
|
||||||
|
// if (e is ApiException && e.response.statusCode == 404) {
|
||||||
|
// // recognize app probably not installed, ignore
|
||||||
|
// _log.info("[_getFaceItemResults] Recognize app not installed");
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// rethrow;
|
||||||
|
// }
|
||||||
|
// final cache =
|
||||||
|
// await ListRecognizeFaceItem(_c.withLocalRepo())(account, face).last;
|
||||||
|
// int itemSorter(RecognizeFaceItem a, RecognizeFaceItem b) =>
|
||||||
|
// a.fileId.compareTo(b.fileId);
|
||||||
|
// final diff = list_util.diffWith(cache, remote, itemSorter);
|
||||||
|
// final inserts = diff.onlyInB;
|
||||||
|
// _log.info("[_getFaceItemResults] New face: ${inserts.toReadableString()}");
|
||||||
|
// final deletes = diff.onlyInA;
|
||||||
|
// _log.info(
|
||||||
|
// "[_getFaceItemResults] Removed face: ${deletes.toReadableString()}");
|
||||||
|
// final updates = remote.where((r) {
|
||||||
|
// final c = cache.firstWhereOrNull((c) => c.fileId == r.fileId);
|
||||||
|
// return c != null && c != r;
|
||||||
|
// }).toList();
|
||||||
|
// _log.info(
|
||||||
|
// "[_getFaceItemResults] Updated face: ${updates.toReadableString()}");
|
||||||
|
// return _FaceItemResult(
|
||||||
|
// results: remote.map((e) => MapEntry(e.fileId, e)).toMap(),
|
||||||
|
// inserts: inserts.map((e) => e.fileId).toList(),
|
||||||
|
// updates: updates.map((e) => e.fileId).toList(),
|
||||||
|
// deletes: deletes.map((e) => e.fileId).toList(),
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
Future<void> _syncDbForFaceItem(sql.SqliteDb db, sql.Account dbAccount,
|
||||||
|
RecognizeFace face, _FaceItemResult item) async {
|
||||||
|
await db.transaction(() async {
|
||||||
|
final dbFace = await db.recognizeFaceByLabel(
|
||||||
|
account: sql.ByAccount.sql(dbAccount),
|
||||||
|
label: face.label,
|
||||||
|
);
|
||||||
|
await db.batch((batch) {
|
||||||
|
for (final d in item.deletes) {
|
||||||
|
batch.deleteWhere(
|
||||||
|
db.recognizeFaceItems,
|
||||||
|
(sql.$RecognizeFaceItemsTable t) =>
|
||||||
|
t.parent.equals(dbFace.rowId) & t.fileId.equals(d),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final u in item.updates) {
|
||||||
|
batch.update(
|
||||||
|
db.recognizeFaceItems,
|
||||||
|
SqliteRecognizeFaceItemConverter.toSql(dbFace, item.results[u]!),
|
||||||
|
where: (sql.$RecognizeFaceItemsTable t) =>
|
||||||
|
t.parent.equals(dbFace.rowId) & t.fileId.equals(u),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (final i in item.inserts) {
|
||||||
|
batch.insert(
|
||||||
|
db.recognizeFaceItems,
|
||||||
|
SqliteRecognizeFaceItemConverter.toSql(dbFace, item.results[i]!),
|
||||||
|
mode: sql.InsertMode.insertOrIgnore,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final DiContainer _c;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FaceResult {
|
||||||
|
const _FaceResult({
|
||||||
|
required this.results,
|
||||||
|
required this.inserts,
|
||||||
|
required this.updates,
|
||||||
|
required this.deletes,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<String, RecognizeFace> results;
|
||||||
|
final List<String> inserts;
|
||||||
|
final List<String> updates;
|
||||||
|
final List<String> deletes;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FaceItemResult {
|
||||||
|
const _FaceItemResult({
|
||||||
|
required this.results,
|
||||||
|
required this.inserts,
|
||||||
|
required this.updates,
|
||||||
|
required this.deletes,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Map<int, RecognizeFaceItem> results;
|
||||||
|
final List<int> inserts;
|
||||||
|
final List<int> updates;
|
||||||
|
final List<int> deletes;
|
||||||
|
}
|
15
app/lib/use_case/recognize_face/sync_recognize_face.g.dart
Normal file
15
app/lib/use_case/recognize_face/sync_recognize_face.g.dart
Normal 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");
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
49
app/lib/widget/search_landing/type.dart
Normal file
49
app/lib/widget/search_landing/type.dart
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
part of '../search_landing.dart';
|
||||||
|
|
||||||
|
class _LandingPersonItem {
|
||||||
|
_LandingPersonItem({
|
||||||
|
required this.account,
|
||||||
|
required this.person,
|
||||||
|
this.onTap,
|
||||||
|
}) : name = person.name,
|
||||||
|
faceUrl = person.getCoverUrl(
|
||||||
|
k.photoLargeSize,
|
||||||
|
k.photoLargeSize,
|
||||||
|
isKeepAspectRatio: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget buildWidget(BuildContext context) => _LandingPersonWidget(
|
||||||
|
account: account,
|
||||||
|
person: person,
|
||||||
|
label: name,
|
||||||
|
coverUrl: faceUrl,
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final Person person;
|
||||||
|
final String name;
|
||||||
|
final String? faceUrl;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LandingLocationItem {
|
||||||
|
const _LandingLocationItem({
|
||||||
|
required this.account,
|
||||||
|
required this.name,
|
||||||
|
required this.thumbUrl,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
Widget buildWidget(BuildContext context) => _LandingLocationWidget(
|
||||||
|
account: account,
|
||||||
|
label: name,
|
||||||
|
coverUrl: thumbUrl,
|
||||||
|
onTap: onTap,
|
||||||
|
);
|
||||||
|
|
||||||
|
final Account account;
|
||||||
|
final String name;
|
||||||
|
final String thumbUrl;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
}
|
143
app/lib/widget/search_landing/view.dart
Normal file
143
app/lib/widget/search_landing/view.dart
Normal 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;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>{"width":1024,"height":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,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue