mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 08:46:18 +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/nc_album.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/share.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 {
|
||||
static Share fromApi(api.Share share) {
|
||||
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/provider/shared_preferences.dart';
|
||||
import 'package:nc_photos/entity/pref_util.dart' as pref_util;
|
||||
import 'package:nc_photos/entity/recognize_face/data_source.dart';
|
||||
import 'package:nc_photos/entity/recognize_face/repo.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
import 'package:nc_photos/entity/search/data_source.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
|
@ -228,6 +230,12 @@ Future<void> _initDiContainer(InitIsolateType isolateType) async {
|
|||
FaceRecognitionPersonRemoteDataSource());
|
||||
c.faceRecognitionPersonRepoLocal = BasicFaceRecognitionPersonRepo(
|
||||
FaceRecognitionPersonSqliteDbDataSource(c.sqliteDb));
|
||||
c.recognizeFaceRepo =
|
||||
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
|
||||
c.recognizeFaceRepoRemote =
|
||||
const BasicRecognizeFaceRepo(RecognizeFaceRemoteDataSource());
|
||||
c.recognizeFaceRepoLocal =
|
||||
BasicRecognizeFaceRepo(RecognizeFaceSqliteDbDataSource(c.sqliteDb));
|
||||
|
||||
c.touchManager = TouchManager(c);
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import 'package:nc_photos/entity/file.dart';
|
|||
import 'package:nc_photos/entity/local_file.dart';
|
||||
import 'package:nc_photos/entity/nc_album/repo.dart';
|
||||
import 'package:nc_photos/entity/pref.dart';
|
||||
import 'package:nc_photos/entity/recognize_face/repo.dart';
|
||||
import 'package:nc_photos/entity/search.dart';
|
||||
import 'package:nc_photos/entity/share.dart';
|
||||
import 'package:nc_photos/entity/sharee.dart';
|
||||
|
@ -40,6 +41,9 @@ enum DiType {
|
|||
faceRecognitionPersonRepo,
|
||||
faceRecognitionPersonRepoRemote,
|
||||
faceRecognitionPersonRepoLocal,
|
||||
recognizeFaceRepo,
|
||||
recognizeFaceRepoRemote,
|
||||
recognizeFaceRepoLocal,
|
||||
pref,
|
||||
sqliteDb,
|
||||
touchManager,
|
||||
|
@ -71,6 +75,9 @@ class DiContainer {
|
|||
FaceRecognitionPersonRepo? faceRecognitionPersonRepo,
|
||||
FaceRecognitionPersonRepo? faceRecognitionPersonRepoRemote,
|
||||
FaceRecognitionPersonRepo? faceRecognitionPersonRepoLocal,
|
||||
RecognizeFaceRepo? recognizeFaceRepo,
|
||||
RecognizeFaceRepo? recognizeFaceRepoRemote,
|
||||
RecognizeFaceRepo? recognizeFaceRepoLocal,
|
||||
Pref? pref,
|
||||
sql.SqliteDb? sqliteDb,
|
||||
TouchManager? touchManager,
|
||||
|
@ -98,6 +105,9 @@ class DiContainer {
|
|||
_faceRecognitionPersonRepo = faceRecognitionPersonRepo,
|
||||
_faceRecognitionPersonRepoRemote = faceRecognitionPersonRepoRemote,
|
||||
_faceRecognitionPersonRepoLocal = faceRecognitionPersonRepoLocal,
|
||||
_recognizeFaceRepo = recognizeFaceRepo,
|
||||
_recognizeFaceRepoRemote = recognizeFaceRepoRemote,
|
||||
_recognizeFaceRepoLocal = recognizeFaceRepoLocal,
|
||||
_pref = pref,
|
||||
_sqliteDb = sqliteDb,
|
||||
_touchManager = touchManager;
|
||||
|
@ -154,6 +164,12 @@ class DiContainer {
|
|||
return contianer._faceRecognitionPersonRepoRemote != null;
|
||||
case DiType.faceRecognitionPersonRepoLocal:
|
||||
return contianer._faceRecognitionPersonRepoLocal != null;
|
||||
case DiType.recognizeFaceRepo:
|
||||
return contianer._recognizeFaceRepo != null;
|
||||
case DiType.recognizeFaceRepoRemote:
|
||||
return contianer._recognizeFaceRepoRemote != null;
|
||||
case DiType.recognizeFaceRepoLocal:
|
||||
return contianer._recognizeFaceRepoLocal != null;
|
||||
case DiType.pref:
|
||||
return contianer._pref != null;
|
||||
case DiType.sqliteDb:
|
||||
|
@ -176,6 +192,7 @@ class DiContainer {
|
|||
OrNull<SearchRepo>? searchRepo,
|
||||
OrNull<NcAlbumRepo>? ncAlbumRepo,
|
||||
OrNull<FaceRecognitionPersonRepo>? faceRecognitionPersonRepo,
|
||||
OrNull<RecognizeFaceRepo>? recognizeFaceRepo,
|
||||
OrNull<Pref>? pref,
|
||||
OrNull<sql.SqliteDb>? sqliteDb,
|
||||
OrNull<TouchManager>? touchManager,
|
||||
|
@ -196,6 +213,9 @@ class DiContainer {
|
|||
faceRecognitionPersonRepo: faceRecognitionPersonRepo == null
|
||||
? _faceRecognitionPersonRepo
|
||||
: faceRecognitionPersonRepo.obj,
|
||||
recognizeFaceRepo: recognizeFaceRepo == null
|
||||
? _recognizeFaceRepo
|
||||
: recognizeFaceRepo.obj,
|
||||
pref: pref == null ? _pref : pref.obj,
|
||||
sqliteDb: sqliteDb == null ? _sqliteDb : sqliteDb.obj,
|
||||
touchManager: touchManager == null ? _touchManager : touchManager.obj,
|
||||
|
@ -229,6 +249,9 @@ class DiContainer {
|
|||
_faceRecognitionPersonRepoRemote!;
|
||||
FaceRecognitionPersonRepo get faceRecognitionPersonRepoLocal =>
|
||||
_faceRecognitionPersonRepoLocal!;
|
||||
RecognizeFaceRepo get recognizeFaceRepo => _recognizeFaceRepo!;
|
||||
RecognizeFaceRepo get recognizeFaceRepoRemote => _recognizeFaceRepoRemote!;
|
||||
RecognizeFaceRepo get recognizeFaceRepoLocal => _recognizeFaceRepoLocal!;
|
||||
|
||||
sql.SqliteDb get sqliteDb => _sqliteDb!;
|
||||
Pref get pref => _pref!;
|
||||
|
@ -354,6 +377,21 @@ class DiContainer {
|
|||
_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) {
|
||||
assert(_sqliteDb == null);
|
||||
_sqliteDb = v;
|
||||
|
@ -396,6 +434,9 @@ class DiContainer {
|
|||
FaceRecognitionPersonRepo? _faceRecognitionPersonRepo;
|
||||
FaceRecognitionPersonRepo? _faceRecognitionPersonRepoRemote;
|
||||
FaceRecognitionPersonRepo? _faceRecognitionPersonRepoLocal;
|
||||
RecognizeFaceRepo? _recognizeFaceRepo;
|
||||
RecognizeFaceRepo? _recognizeFaceRepoRemote;
|
||||
RecognizeFaceRepo? _recognizeFaceRepoLocal;
|
||||
|
||||
sql.SqliteDb? _sqliteDb;
|
||||
Pref? _pref;
|
||||
|
@ -413,6 +454,7 @@ extension DiContainerExtension on DiContainer {
|
|||
tagRepo: OrNull(_tagRepoRemote),
|
||||
ncAlbumRepo: OrNull(_ncAlbumRepoRemote),
|
||||
faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoRemote),
|
||||
recognizeFaceRepo: OrNull(_recognizeFaceRepoRemote),
|
||||
);
|
||||
|
||||
/// Uses local repo if available
|
||||
|
@ -425,6 +467,7 @@ extension DiContainerExtension on DiContainer {
|
|||
tagRepo: OrNull(_tagRepoLocal),
|
||||
ncAlbumRepo: OrNull(_ncAlbumRepoLocal),
|
||||
faceRecognitionPersonRepo: OrNull(_faceRecognitionPersonRepoLocal),
|
||||
recognizeFaceRepo: OrNull(_recognizeFaceRepoLocal),
|
||||
);
|
||||
|
||||
DiContainer withLocalAlbumRepo() =>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:copy_with/copy_with.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:to_string/to_string.dart';
|
||||
|
||||
part 'person.g.dart';
|
||||
|
@ -40,6 +41,10 @@ class Person with EquatableMixin {
|
|||
isKeepAspectRatio: isKeepAspectRatio,
|
||||
);
|
||||
|
||||
/// See [PersonContentProvider.getCoverTransform]
|
||||
Matrix4? getCoverTransform(int viewportSize, int width, int height) =>
|
||||
contentProvider.getCoverTransform(viewportSize, width, height);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
|
@ -73,4 +78,9 @@ abstract class PersonContentProvider with EquatableMixin {
|
|||
int height, {
|
||||
bool? isKeepAspectRatio,
|
||||
});
|
||||
|
||||
/// Return the transformation matrix to focus the face
|
||||
///
|
||||
/// Only viewport in square is supported
|
||||
Matrix4? getCoverTransform(int viewportSize, int width, int height);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,9 @@ import 'package:nc_photos/account.dart';
|
|||
import 'package:nc_photos/di_container.dart';
|
||||
import 'package:nc_photos/entity/person.dart';
|
||||
import 'package:nc_photos/entity/person/adapter/face_recognition.dart';
|
||||
import 'package:nc_photos/entity/person/adapter/recognize.dart';
|
||||
import 'package:nc_photos/entity/person/content_provider/face_recognition.dart';
|
||||
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
|
||||
import 'package:nc_photos/entity/person_face.dart';
|
||||
|
||||
abstract class PersonAdapter {
|
||||
|
@ -12,6 +14,8 @@ abstract class PersonAdapter {
|
|||
switch (person.contentProvider.runtimeType) {
|
||||
case PersonFaceRecognitionProvider:
|
||||
return PersonFaceRecognitionAdapter(c, account, person);
|
||||
case PersonRecognizeProvider:
|
||||
return PersonRecognizeAdapter(c, account, person);
|
||||
default:
|
||||
throw UnsupportedError(
|
||||
"Unknown type: ${person.contentProvider.runtimeType}");
|
||||
|
|
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/person.dart';
|
||||
import 'package:nc_photos/entity/person/content_provider/face_recognition.dart';
|
||||
import 'package:nc_photos/entity/person/content_provider/recognize.dart';
|
||||
import 'package:nc_photos/entity/recognize_face.dart';
|
||||
import 'package:nc_photos/entity/recognize_face_item.dart';
|
||||
|
||||
class PersonBuilder {
|
||||
static Person byFaceRecognitionPerson(
|
||||
|
@ -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 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/entity/face_recognition_person.dart';
|
||||
|
@ -43,6 +44,9 @@ class PersonFaceRecognitionProvider
|
|||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Matrix4? getCoverTransform(int viewportSize, int width, int height) => null;
|
||||
|
||||
@override
|
||||
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,
|
||||
NcAlbums,
|
||||
NcAlbumItems,
|
||||
RecognizeFaces,
|
||||
RecognizeFaceItems,
|
||||
],
|
||||
)
|
||||
class SqliteDb extends _$SqliteDb {
|
||||
|
@ -104,6 +106,8 @@ class SqliteDb extends _$SqliteDb {
|
|||
if (from >= 2) {
|
||||
await m.renameTable(faceRecognitionPersons, "persons");
|
||||
}
|
||||
await m.createTable(recognizeFaces);
|
||||
await m.createTable(recognizeFaceItems);
|
||||
}
|
||||
});
|
||||
} 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 {
|
||||
_$SqliteDb(QueryExecutor e) : super(e);
|
||||
late final $ServersTable servers = $ServersTable(this);
|
||||
|
@ -5305,6 +6219,9 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
$FaceRecognitionPersonsTable(this);
|
||||
late final $NcAlbumsTable ncAlbums = $NcAlbumsTable(this);
|
||||
late final $NcAlbumItemsTable ncAlbumItems = $NcAlbumItemsTable(this);
|
||||
late final $RecognizeFacesTable recognizeFaces = $RecognizeFacesTable(this);
|
||||
late final $RecognizeFaceItemsTable recognizeFaceItems =
|
||||
$RecognizeFaceItemsTable(this);
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
|
@ -5323,7 +6240,9 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
tags,
|
||||
faceRecognitionPersons,
|
||||
ncAlbums,
|
||||
ncAlbumItems
|
||||
ncAlbumItems,
|
||||
recognizeFaces,
|
||||
recognizeFaceItems
|
||||
];
|
||||
@override
|
||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
|
||||
|
@ -5433,6 +6352,20 @@ abstract class _$SqliteDb extends GeneratedDatabase {
|
|||
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({
|
||||
Account? sqlAccount,
|
||||
app.Account? appAccount,
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:nc_photos/entity/sqlite/database.dart';
|
||||
import 'package:np_codegen/np_codegen.dart';
|
||||
|
||||
part 'table.g.dart';
|
||||
|
||||
class Servers extends Table {
|
||||
IntColumn get rowId => integer().autoIncrement()();
|
||||
|
@ -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> {
|
||||
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/nc_album.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/tag.dart';
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
|
@ -18,6 +20,7 @@ import 'package:nc_photos/object_extension.dart';
|
|||
import 'package:nc_photos/or_null.dart';
|
||||
import 'package:np_api/np_api.dart' as api;
|
||||
import 'package:np_common/ci_string.dart';
|
||||
import 'package:np_common/type.dart';
|
||||
|
||||
extension SqlTagListExtension on List<sql.Tag> {
|
||||
Future<List<Tag>> convertToAppTag() {
|
||||
|
@ -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 {
|
||||
static Album fromSql(
|
||||
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) {
|
||||
final account = map["account"] as sql.Account?;
|
||||
final tag = map["tag"] as Tag;
|
||||
|
@ -337,3 +412,9 @@ sql.FaceRecognitionPersonsCompanion _convertAppFaceRecognitionPerson(Map map) {
|
|||
final person = map["person"] as FaceRecognitionPerson;
|
||||
return SqliteFaceRecognitionPersonConverter.toSql(account, person);
|
||||
}
|
||||
|
||||
sql.RecognizeFacesCompanion _convertAppRecognizeFace(Map map) {
|
||||
final account = map["account"] as sql.Account?;
|
||||
final face = map["face"] as RecognizeFace;
|
||||
return SqliteRecognizeFaceConverter.toSql(account, face);
|
||||
}
|
||||
|
|
|
@ -23,10 +23,13 @@ class SyncFaceRecognitionPerson {
|
|||
/// Return if any people were updated
|
||||
Future<bool> call(Account account) async {
|
||||
_log.info("[call] Sync people with remote");
|
||||
int personSorter(FaceRecognitionPerson a, FaceRecognitionPerson b) =>
|
||||
a.name.compareTo(b.name);
|
||||
late final List<FaceRecognitionPerson> remote;
|
||||
try {
|
||||
remote =
|
||||
await ListFaceRecognitionPerson(_c.withRemoteRepo())(account).last;
|
||||
remote = (await ListFaceRecognitionPerson(_c.withRemoteRepo())(account)
|
||||
.last)
|
||||
..sort(personSorter);
|
||||
} catch (e) {
|
||||
if (e is ApiException && e.response.statusCode == 404) {
|
||||
// face recognition app probably not installed, ignore
|
||||
|
@ -35,10 +38,9 @@ class SyncFaceRecognitionPerson {
|
|||
}
|
||||
rethrow;
|
||||
}
|
||||
final cache =
|
||||
await ListFaceRecognitionPerson(_c.withLocalRepo())(account).last;
|
||||
int personSorter(FaceRecognitionPerson a, FaceRecognitionPerson b) =>
|
||||
a.name.compareTo(b.name);
|
||||
final cache = (await ListFaceRecognitionPerson(_c.withLocalRepo())(account)
|
||||
.last)
|
||||
..sort(personSorter);
|
||||
final diff = list_util.diffWith(cache, remote, personSorter);
|
||||
final inserts = diff.onlyInB;
|
||||
_log.info("[call] New people: ${inserts.toReadableString()}");
|
||||
|
|
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:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api_util.dart' as api_util;
|
||||
import 'package:nc_photos/cache_manager_util.dart';
|
||||
import 'package:nc_photos/entity/file_descriptor.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/np_api_util.dart';
|
||||
import 'package:nc_photos/object_extension.dart';
|
||||
import 'package:nc_photos/widget/cached_network_image_mod.dart' as mod;
|
||||
|
||||
/// A square thumbnail widget for a file
|
||||
class NetworkRectThumbnail extends StatelessWidget {
|
||||
|
@ -16,6 +18,7 @@ class NetworkRectThumbnail extends StatelessWidget {
|
|||
required this.imageUrl,
|
||||
this.dimension,
|
||||
required this.errorBuilder,
|
||||
this.onSize,
|
||||
});
|
||||
|
||||
static String imageUrlForFile(Account account, FileDescriptor file) =>
|
||||
|
@ -41,10 +44,9 @@ class NetworkRectThumbnail extends StatelessWidget {
|
|||
final child = FittedBox(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
fit: BoxFit.cover,
|
||||
child: CachedNetworkImage(
|
||||
child: mod.CachedNetworkImage(
|
||||
cacheManager: ThumbnailCacheManager.inst,
|
||||
imageUrl: imageUrl,
|
||||
// imageUrl: "",
|
||||
httpHeaders: {
|
||||
"Authorization": AuthUtil.fromAccount(account).toHeaderValue(),
|
||||
},
|
||||
|
@ -55,6 +57,12 @@ class NetworkRectThumbnail extends StatelessWidget {
|
|||
dimension: dimension,
|
||||
child: errorBuilder(context),
|
||||
),
|
||||
imageBuilder: (_, child, __) {
|
||||
return _SizeObserver(
|
||||
onSize: onSize,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
if (dimension != null) {
|
||||
|
@ -74,4 +82,48 @@ class NetworkRectThumbnail extends StatelessWidget {
|
|||
final String imageUrl;
|
||||
final double? dimension;
|
||||
final Widget Function(BuildContext context) errorBuilder;
|
||||
final ValueChanged<Size>? onSize;
|
||||
}
|
||||
|
||||
class _SizeObserver extends SingleChildRenderObjectWidget {
|
||||
const _SizeObserver({
|
||||
super.child,
|
||||
this.onSize,
|
||||
});
|
||||
|
||||
@override
|
||||
RenderObject createRenderObject(BuildContext context) {
|
||||
return _RenderSizeChangedWithCallback(
|
||||
onLayoutChangedCallback: () {
|
||||
if (onSize != null) {
|
||||
final size = context.findRenderObject()?.as<RenderBox>()?.size;
|
||||
if (size != null) {
|
||||
onSize?.call(size);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final ValueChanged<Size>? onSize;
|
||||
}
|
||||
|
||||
class _RenderSizeChangedWithCallback extends RenderProxyBox {
|
||||
_RenderSizeChangedWithCallback({
|
||||
RenderBox? child,
|
||||
required this.onLayoutChangedCallback,
|
||||
}) : super(child);
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
super.performLayout();
|
||||
if (size != _oldSize) {
|
||||
onLayoutChangedCallback();
|
||||
}
|
||||
_oldSize = size;
|
||||
}
|
||||
|
||||
final VoidCallback onLayoutChangedCallback;
|
||||
|
||||
Size? _oldSize;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.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';
|
||||
|
||||
part 'search_landing.g.dart';
|
||||
part 'search_landing/type.dart';
|
||||
part 'search_landing/view.dart';
|
||||
|
||||
class SearchLanding extends StatefulWidget {
|
||||
const SearchLanding({
|
||||
|
@ -263,8 +267,7 @@ class _SearchLandingState extends State<SearchLanding> {
|
|||
.take(10)
|
||||
.map((e) => _LandingPersonItem(
|
||||
account: widget.account,
|
||||
name: e.name,
|
||||
faceUrl: e.getCoverUrl(k.faceThumbSize, k.faceThumbSize),
|
||||
person: e,
|
||||
onTap: () => _onPersonItemTap(e),
|
||||
))
|
||||
.toList();
|
||||
|
@ -303,92 +306,32 @@ class _SearchLandingState extends State<SearchLanding> {
|
|||
var _locationItems = <_LandingLocationItem>[];
|
||||
}
|
||||
|
||||
class _LandingPersonItem {
|
||||
_LandingPersonItem({
|
||||
required this.account,
|
||||
required this.name,
|
||||
required this.faceUrl,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
Widget buildWidget(BuildContext context) => _LandingItemWidget(
|
||||
account: account,
|
||||
label: name,
|
||||
coverUrl: faceUrl,
|
||||
onTap: onTap,
|
||||
fallbackBuilder: (_) => Icon(
|
||||
Icons.person,
|
||||
color: Theme.of(context).listPlaceholderForegroundColor,
|
||||
),
|
||||
);
|
||||
|
||||
final Account account;
|
||||
final String name;
|
||||
final String? faceUrl;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _LandingLocationItem {
|
||||
const _LandingLocationItem({
|
||||
required this.account,
|
||||
required this.name,
|
||||
required this.thumbUrl,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
Widget buildWidget(BuildContext context) => _LandingItemWidget(
|
||||
account: account,
|
||||
label: name,
|
||||
coverUrl: thumbUrl,
|
||||
onTap: onTap,
|
||||
fallbackBuilder: (_) => Icon(
|
||||
Icons.location_on,
|
||||
color: Theme.of(context).listPlaceholderForegroundColor,
|
||||
),
|
||||
);
|
||||
|
||||
final Account account;
|
||||
final String name;
|
||||
final String thumbUrl;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
class _LandingItemWidget extends StatelessWidget {
|
||||
const _LandingItemWidget({
|
||||
Key? key,
|
||||
class _LandingPersonWidget extends StatelessWidget {
|
||||
const _LandingPersonWidget({
|
||||
required this.account,
|
||||
required this.person,
|
||||
required this.label,
|
||||
required this.coverUrl,
|
||||
required this.fallbackBuilder,
|
||||
this.onTap,
|
||||
}) : super(key: key);
|
||||
});
|
||||
|
||||
@override
|
||||
build(BuildContext context) {
|
||||
Widget build(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1,
|
||||
child: _buildCoverImage(context),
|
||||
),
|
||||
Center(
|
||||
child: _PersonCoverImage(
|
||||
dimension: 72,
|
||||
account: account,
|
||||
person: person,
|
||||
coverUrl: coverUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
width: 88,
|
||||
child: Text(
|
||||
label + "\n",
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Expanded(child: _Label(label: label)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -402,37 +345,74 @@ class _LandingItemWidget extends StatelessWidget {
|
|||
}
|
||||
}
|
||||
|
||||
Widget _buildCoverImage(BuildContext context) {
|
||||
Widget cover;
|
||||
Widget buildPlaceholder() => Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: fallbackBuilder(context),
|
||||
);
|
||||
try {
|
||||
cover = NetworkRectThumbnail(
|
||||
account: account,
|
||||
imageUrl: coverUrl!,
|
||||
errorBuilder: (_) => buildPlaceholder(),
|
||||
);
|
||||
} catch (_) {
|
||||
cover = FittedBox(
|
||||
child: buildPlaceholder(),
|
||||
);
|
||||
}
|
||||
final Account account;
|
||||
final Person person;
|
||||
final String label;
|
||||
final String? coverUrl;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(128),
|
||||
child: Container(
|
||||
color: Theme.of(context).listPlaceholderBackgroundColor,
|
||||
constraints: const BoxConstraints.expand(),
|
||||
child: cover,
|
||||
class _LandingLocationWidget extends StatelessWidget {
|
||||
const _LandingLocationWidget({
|
||||
required this.account,
|
||||
required this.label,
|
||||
required this.coverUrl,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final content = Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Center(
|
||||
child: _LocationCoverImage(
|
||||
dimension: 72,
|
||||
account: account,
|
||||
coverUrl: coverUrl,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(child: _Label(label: label)),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: content,
|
||||
);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
final Account account;
|
||||
final String label;
|
||||
final String? coverUrl;
|
||||
final Widget Function(BuildContext context) fallbackBuilder;
|
||||
final 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/entity/file.dart';
|
||||
import 'package:nc_photos/entity/recognize_face_item.dart';
|
||||
import 'package:np_api/np_api.dart' as api;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
@ -15,6 +16,12 @@ void main() {
|
|||
test("nextcloud hosted in subdir", _filesServerHostedInSubdir);
|
||||
});
|
||||
});
|
||||
group("ApiRecognizeFaceItemConverter", () {
|
||||
group("fromApi", () {
|
||||
test("minimum", _recognizeFaceItemMinimum);
|
||||
test("size", _recognizeFaceItemSize);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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/nc_album_item_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/share_parser.dart';
|
||||
export 'src/entity/sharee_parser.dart';
|
||||
|
|
|
@ -269,8 +269,7 @@ class RecognizeFaceItem with EquatableMixin {
|
|||
final String? etag;
|
||||
final DateTime? lastModified;
|
||||
final List<JsonObj>? faceDetections;
|
||||
// format currently unknown
|
||||
final dynamic fileMetadataSize;
|
||||
final JsonObj? fileMetadataSize;
|
||||
final bool? hasPreview;
|
||||
final String? realPath;
|
||||
final bool? favorite;
|
||||
|
|
|
@ -23,8 +23,7 @@ class RecognizeFaceItemParser extends XmlResponseParser {
|
|||
String? etag;
|
||||
DateTime? lastModified;
|
||||
List<JsonObj>? faceDetections;
|
||||
// format currently unknown
|
||||
dynamic fileMetadataSize;
|
||||
Object? fileMetadataSize;
|
||||
bool? hasPreview;
|
||||
String? realPath;
|
||||
bool? favorite;
|
||||
|
@ -69,7 +68,9 @@ class RecognizeFaceItemParser extends XmlResponseParser {
|
|||
etag: etag,
|
||||
lastModified: lastModified,
|
||||
faceDetections: faceDetections,
|
||||
fileMetadataSize: fileMetadataSize,
|
||||
fileMetadataSize: fileMetadataSize is Map
|
||||
? fileMetadataSize.cast<String, dynamic>()
|
||||
: null,
|
||||
hasPreview: hasPreview,
|
||||
realPath: realPath,
|
||||
favorite: favorite,
|
||||
|
@ -105,7 +106,8 @@ class _PropParser {
|
|||
: (jsonDecode(child.innerText) as List).cast<JsonObj>();
|
||||
} else if (child.matchQualifiedName("file-metadata-size",
|
||||
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
|
||||
_fileMetadataSize = child.innerText;
|
||||
_fileMetadataSize =
|
||||
child.innerText.isEmpty ? null : jsonDecode(child.innerText);
|
||||
} else if (child.matchQualifiedName("has-preview",
|
||||
prefix: "http://nextcloud.org/ns", namespaces: namespaces)) {
|
||||
_hasPreview = child.innerText == "true";
|
||||
|
@ -127,7 +129,7 @@ class _PropParser {
|
|||
String? get etag => _etag;
|
||||
DateTime? get lastModified => _lastModified;
|
||||
List<JsonObj>? get faceDetections => _faceDetections;
|
||||
dynamic get fileMetadataSize => _fileMetadataSize;
|
||||
Object? get fileMetadataSize => _fileMetadataSize;
|
||||
bool? get hasPreview => _hasPreview;
|
||||
String? get realPath => _realPath;
|
||||
bool? get favorite => _favorite;
|
||||
|
@ -140,7 +142,8 @@ class _PropParser {
|
|||
String? _etag;
|
||||
DateTime? _lastModified;
|
||||
List<JsonObj>? _faceDetections;
|
||||
dynamic _fileMetadataSize;
|
||||
// size can be a map or a list if the size is not known (well...)
|
||||
Object? _fileMetadataSize;
|
||||
bool? _hasPreview;
|
||||
String? _realPath;
|
||||
bool? _favorite;
|
||||
|
|
|
@ -6,7 +6,9 @@ class ApiRecognize {
|
|||
ApiRecognizeFaces faces() => ApiRecognizeFaces(this);
|
||||
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 String userId;
|
||||
|
@ -17,7 +19,7 @@ class ApiRecognizeFaces {
|
|||
const ApiRecognizeFaces(this.recognize);
|
||||
|
||||
Future<Response> propfind() async {
|
||||
final endpoint = "${recognize._path}/faces";
|
||||
final endpoint = "${recognize._userPath}/faces";
|
||||
try {
|
||||
return await api.request("PROPFIND", endpoint);
|
||||
} 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;
|
||||
final ApiRecognize recognize;
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:np_api/np_api.dart';
|
||||
import 'package:np_api/src/entity/recognize_face_item_parser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
|
@ -7,6 +6,7 @@ void main() {
|
|||
group("parse", () {
|
||||
test("empty", _empty);
|
||||
test("image", _image);
|
||||
test("imageWithSize", _imageWithSize);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ Future<void> _image() async {
|
|||
"title": "test",
|
||||
},
|
||||
],
|
||||
fileMetadataSize: "[]",
|
||||
fileMetadataSize: null,
|
||||
hasPreview: true,
|
||||
realPath: "/admin/files/test1.jpg",
|
||||
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