feat(web): re-assign person faces (2) (#4949)
* feat: unassign person faces * multiple improvements * chore: regenerate api * feat: improve face interactions in photos * fix: tests * fix: tests * optimize * fix: wrong assignment on complex-multiple re-assignments * fix: thumbnails with large photos * fix: complex reassign * fix: don't send people with faces * fix: person thumbnail generation * chore: regenerate api * add tess * feat: face box even when zoomed * fix: change feature photo * feat: make the blue icon hoverable * chore: regenerate api * feat: use websocket * fix: loading spinner when clicking on the done button * fix: use the svelte way * fix: tests * simplify * fix: unused vars * fix: remove unused code * fix: add migration * chore: regenerate api * ci: add unit tests * chore: regenerate api * feat: if a new person is created for a face and the server takes more than 15 seconds to generate the person thumbnail, don't wait for it * reorganize * chore: regenerate api * feat: global edit * pr feedback * pr feedback * simplify * revert test * fix: face generation * fix: tests * fix: face generation * fix merge * feat: search names in unmerge face selector modal * fix: merge face selector * simplify feature photo generation * fix: change endpoint * pr feedback * chore: fix merge * chore: fix merge * fix: tests * fix: edit & hide buttons * fix: tests * feat: show if person is hidden * feat: rename face to person * feat: split in new panel * copy-paste-error * pr feedback * fix: feature photo * do not leak faces * fix: unmerge modal * fix: merge modal event * feat(server): remove duplicates * fix: title for image thumbnails * fix: disable side panel when there's no face until next PR --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
982183600d
commit
7702560b12
74 changed files with 4882 additions and 283 deletions
588
cli/src/api/open-api/api.ts
generated
588
cli/src/api/open-api/api.ts
generated
|
@ -586,6 +586,142 @@ export const AssetBulkUploadCheckResultReasonEnum = {
|
|||
|
||||
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceResponseDto
|
||||
*/
|
||||
export interface AssetFaceResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxX1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxX2': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxY1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxY2': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'imageHeight': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'imageWidth': number;
|
||||
/**
|
||||
*
|
||||
* @type {PersonResponseDto}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'person': PersonResponseDto | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceUpdateDto
|
||||
*/
|
||||
export interface AssetFaceUpdateDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetFaceUpdateItem>}
|
||||
* @memberof AssetFaceUpdateDto
|
||||
*/
|
||||
'data': Array<AssetFaceUpdateItem>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceUpdateItem
|
||||
*/
|
||||
export interface AssetFaceUpdateItem {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceUpdateItem
|
||||
*/
|
||||
'assetId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceUpdateItem
|
||||
*/
|
||||
'personId': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
export interface AssetFaceWithoutPersonResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxX1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxX2': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxY1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxY2': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'imageHeight': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'imageWidth': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -842,10 +978,10 @@ export interface AssetResponseDto {
|
|||
'ownerId': string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<PersonResponseDto>}
|
||||
* @type {Array<PersonWithFacesResponseDto>}
|
||||
* @memberof AssetResponseDto
|
||||
*/
|
||||
'people'?: Array<PersonResponseDto>;
|
||||
'people'?: Array<PersonWithFacesResponseDto>;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
|
@ -1672,6 +1808,19 @@ export interface ExifResponseDto {
|
|||
*/
|
||||
'timeZone'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface FaceDto
|
||||
*/
|
||||
export interface FaceDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceDto
|
||||
*/
|
||||
'id': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -2564,6 +2713,49 @@ export interface PersonUpdateDto {
|
|||
*/
|
||||
'name'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PersonWithFacesResponseDto
|
||||
*/
|
||||
export interface PersonWithFacesResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'birthDate': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetFaceWithoutPersonResponseDto>}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'faces': Array<AssetFaceWithoutPersonResponseDto>;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'isHidden': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'thumbnailPath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -11349,6 +11541,233 @@ export class AuthenticationApi extends BaseAPI {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* FaceApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getFaces', 'id', id)
|
||||
const localVarPath = `/face`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (id !== undefined) {
|
||||
localVarQueryParameter['id'] = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {FaceDto} faceDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('reassignFacesById', 'id', id)
|
||||
// verify required parameter 'faceDto' is not null or undefined
|
||||
assertParamExists('reassignFacesById', 'faceDto', faceDto)
|
||||
const localVarPath = `/face/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FaceApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetFaceResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {FaceDto} faceDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FaceApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = FaceApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetFaceResponseDto>> {
|
||||
return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||
return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for getFaces operation in FaceApi.
|
||||
* @export
|
||||
* @interface FaceApiGetFacesRequest
|
||||
*/
|
||||
export interface FaceApiGetFacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceApiGetFaces
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for reassignFacesById operation in FaceApi.
|
||||
* @export
|
||||
* @interface FaceApiReassignFacesByIdRequest
|
||||
*/
|
||||
export interface FaceApiReassignFacesByIdRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceApiReassignFacesById
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {FaceDto}
|
||||
* @memberof FaceApiReassignFacesById
|
||||
*/
|
||||
readonly faceDto: FaceDto
|
||||
}
|
||||
|
||||
/**
|
||||
* FaceApi - object-oriented interface
|
||||
* @export
|
||||
* @class FaceApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class FaceApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof FaceApi
|
||||
*/
|
||||
public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) {
|
||||
return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof FaceApi
|
||||
*/
|
||||
public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) {
|
||||
return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* JobApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -13180,6 +13599,44 @@ export class PartnerApi extends BaseAPI {
|
|||
*/
|
||||
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createPerson: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/person`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [withHidden]
|
||||
|
@ -13439,6 +13896,54 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('reassignFaces', 'id', id)
|
||||
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
|
||||
assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
|
||||
const localVarPath = `/person/{id}/reassign`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
|
@ -13541,6 +14046,15 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
export const PersonApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [withHidden]
|
||||
|
@ -13602,6 +14116,17 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
|
@ -13633,6 +14158,14 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
|||
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = PersonApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createPerson(options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||
return localVarFp.createPerson(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13687,6 +14220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
|
|||
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||
return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13799,6 +14341,27 @@ export interface PersonApiMergePersonRequest {
|
|||
readonly mergePersonDto: MergePersonDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for reassignFaces operation in PersonApi.
|
||||
* @export
|
||||
* @interface PersonApiReassignFacesRequest
|
||||
*/
|
||||
export interface PersonApiReassignFacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonApiReassignFaces
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {AssetFaceUpdateDto}
|
||||
* @memberof PersonApiReassignFaces
|
||||
*/
|
||||
readonly assetFaceUpdateDto: AssetFaceUpdateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePeople operation in PersonApi.
|
||||
* @export
|
||||
|
@ -13841,6 +14404,16 @@ export interface PersonApiUpdatePersonRequest {
|
|||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class PersonApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public createPerson(options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13907,6 +14480,17 @@ export class PersonApi extends BaseAPI {
|
|||
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
|
|
21
mobile/openapi/.openapi-generator/FILES
generated
21
mobile/openapi/.openapi-generator/FILES
generated
|
@ -24,6 +24,10 @@ doc/AssetBulkUploadCheckDto.md
|
|||
doc/AssetBulkUploadCheckItem.md
|
||||
doc/AssetBulkUploadCheckResponseDto.md
|
||||
doc/AssetBulkUploadCheckResult.md
|
||||
doc/AssetFaceResponseDto.md
|
||||
doc/AssetFaceUpdateDto.md
|
||||
doc/AssetFaceUpdateItem.md
|
||||
doc/AssetFaceWithoutPersonResponseDto.md
|
||||
doc/AssetFileUploadResponseDto.md
|
||||
doc/AssetIdsDto.md
|
||||
doc/AssetIdsResponseDto.md
|
||||
|
@ -60,6 +64,8 @@ doc/DownloadInfoDto.md
|
|||
doc/DownloadResponseDto.md
|
||||
doc/EntityType.md
|
||||
doc/ExifResponseDto.md
|
||||
doc/FaceApi.md
|
||||
doc/FaceDto.md
|
||||
doc/FileChecksumDto.md
|
||||
doc/FileChecksumResponseDto.md
|
||||
doc/FileReportDto.md
|
||||
|
@ -100,6 +106,7 @@ doc/PersonApi.md
|
|||
doc/PersonResponseDto.md
|
||||
doc/PersonStatisticsResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
doc/PersonWithFacesResponseDto.md
|
||||
doc/QueueStatusDto.md
|
||||
doc/ReactionLevel.md
|
||||
doc/ReactionType.md
|
||||
|
@ -177,6 +184,7 @@ lib/api/api_key_api.dart
|
|||
lib/api/asset_api.dart
|
||||
lib/api/audit_api.dart
|
||||
lib/api/authentication_api.dart
|
||||
lib/api/face_api.dart
|
||||
lib/api/job_api.dart
|
||||
lib/api/library_api.dart
|
||||
lib/api/o_auth_api.dart
|
||||
|
@ -213,6 +221,10 @@ lib/model/asset_bulk_upload_check_dto.dart
|
|||
lib/model/asset_bulk_upload_check_item.dart
|
||||
lib/model/asset_bulk_upload_check_response_dto.dart
|
||||
lib/model/asset_bulk_upload_check_result.dart
|
||||
lib/model/asset_face_response_dto.dart
|
||||
lib/model/asset_face_update_dto.dart
|
||||
lib/model/asset_face_update_item.dart
|
||||
lib/model/asset_face_without_person_response_dto.dart
|
||||
lib/model/asset_file_upload_response_dto.dart
|
||||
lib/model/asset_ids_dto.dart
|
||||
lib/model/asset_ids_response_dto.dart
|
||||
|
@ -247,6 +259,7 @@ lib/model/download_info_dto.dart
|
|||
lib/model/download_response_dto.dart
|
||||
lib/model/entity_type.dart
|
||||
lib/model/exif_response_dto.dart
|
||||
lib/model/face_dto.dart
|
||||
lib/model/file_checksum_dto.dart
|
||||
lib/model/file_checksum_response_dto.dart
|
||||
lib/model/file_report_dto.dart
|
||||
|
@ -282,6 +295,7 @@ lib/model/people_update_item.dart
|
|||
lib/model/person_response_dto.dart
|
||||
lib/model/person_statistics_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/person_with_faces_response_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
lib/model/reaction_level.dart
|
||||
lib/model/reaction_type.dart
|
||||
|
@ -367,6 +381,10 @@ test/asset_bulk_upload_check_dto_test.dart
|
|||
test/asset_bulk_upload_check_item_test.dart
|
||||
test/asset_bulk_upload_check_response_dto_test.dart
|
||||
test/asset_bulk_upload_check_result_test.dart
|
||||
test/asset_face_response_dto_test.dart
|
||||
test/asset_face_update_dto_test.dart
|
||||
test/asset_face_update_item_test.dart
|
||||
test/asset_face_without_person_response_dto_test.dart
|
||||
test/asset_file_upload_response_dto_test.dart
|
||||
test/asset_ids_dto_test.dart
|
||||
test/asset_ids_response_dto_test.dart
|
||||
|
@ -403,6 +421,8 @@ test/download_info_dto_test.dart
|
|||
test/download_response_dto_test.dart
|
||||
test/entity_type_test.dart
|
||||
test/exif_response_dto_test.dart
|
||||
test/face_api_test.dart
|
||||
test/face_dto_test.dart
|
||||
test/file_checksum_dto_test.dart
|
||||
test/file_checksum_response_dto_test.dart
|
||||
test/file_report_dto_test.dart
|
||||
|
@ -443,6 +463,7 @@ test/person_api_test.dart
|
|||
test/person_response_dto_test.dart
|
||||
test/person_statistics_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
test/person_with_faces_response_dto_test.dart
|
||||
test/queue_status_dto_test.dart
|
||||
test/reaction_level_test.dart
|
||||
test/reaction_type_test.dart
|
||||
|
|
10
mobile/openapi/README.md
generated
10
mobile/openapi/README.md
generated
|
@ -133,6 +133,8 @@ Class | Method | HTTP request | Description
|
|||
*AuthenticationApi* | [**logoutAuthDevices**](doc//AuthenticationApi.md#logoutauthdevices) | **DELETE** /auth/devices |
|
||||
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
|
||||
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
|
||||
*FaceApi* | [**getFaces**](doc//FaceApi.md#getfaces) | **GET** /face |
|
||||
*FaceApi* | [**reassignFacesById**](doc//FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
|
||||
*JobApi* | [**getAllJobsStatus**](doc//JobApi.md#getalljobsstatus) | **GET** /jobs |
|
||||
*JobApi* | [**sendJobCommand**](doc//JobApi.md#sendjobcommand) | **PUT** /jobs/{id} |
|
||||
*LibraryApi* | [**createLibrary**](doc//LibraryApi.md#createlibrary) | **POST** /library |
|
||||
|
@ -153,12 +155,14 @@ Class | Method | HTTP request | Description
|
|||
*PartnerApi* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner |
|
||||
*PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} |
|
||||
*PartnerApi* | [**updatePartner**](doc//PartnerApi.md#updatepartner) | **PUT** /partner/{id} |
|
||||
*PersonApi* | [**createPerson**](doc//PersonApi.md#createperson) | **POST** /person |
|
||||
*PersonApi* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person |
|
||||
*PersonApi* | [**getPerson**](doc//PersonApi.md#getperson) | **GET** /person/{id} |
|
||||
*PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
|
||||
*PersonApi* | [**getPersonStatistics**](doc//PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
|
||||
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
|
||||
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
|
||||
*PersonApi* | [**reassignFaces**](doc//PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
||||
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
|
||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||
|
@ -224,6 +228,10 @@ Class | Method | HTTP request | Description
|
|||
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
|
||||
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
|
||||
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
|
||||
- [AssetFaceResponseDto](doc//AssetFaceResponseDto.md)
|
||||
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
|
||||
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
|
||||
- [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md)
|
||||
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
|
||||
- [AssetIdsDto](doc//AssetIdsDto.md)
|
||||
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
|
||||
|
@ -258,6 +266,7 @@ Class | Method | HTTP request | Description
|
|||
- [DownloadResponseDto](doc//DownloadResponseDto.md)
|
||||
- [EntityType](doc//EntityType.md)
|
||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||
- [FaceDto](doc//FaceDto.md)
|
||||
- [FileChecksumDto](doc//FileChecksumDto.md)
|
||||
- [FileChecksumResponseDto](doc//FileChecksumResponseDto.md)
|
||||
- [FileReportDto](doc//FileReportDto.md)
|
||||
|
@ -293,6 +302,7 @@ Class | Method | HTTP request | Description
|
|||
- [PersonResponseDto](doc//PersonResponseDto.md)
|
||||
- [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md)
|
||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||
- [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
- [ReactionLevel](doc//ReactionLevel.md)
|
||||
- [ReactionType](doc//ReactionType.md)
|
||||
|
|
22
mobile/openapi/doc/AssetFaceResponseDto.md
generated
Normal file
22
mobile/openapi/doc/AssetFaceResponseDto.md
generated
Normal file
|
@ -0,0 +1,22 @@
|
|||
# openapi.model.AssetFaceResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**boundingBoxX1** | **int** | |
|
||||
**boundingBoxX2** | **int** | |
|
||||
**boundingBoxY1** | **int** | |
|
||||
**boundingBoxY2** | **int** | |
|
||||
**id** | **String** | |
|
||||
**imageHeight** | **int** | |
|
||||
**imageWidth** | **int** | |
|
||||
**person** | [**PersonResponseDto**](PersonResponseDto.md) | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
15
mobile/openapi/doc/AssetFaceUpdateDto.md
generated
Normal file
15
mobile/openapi/doc/AssetFaceUpdateDto.md
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
# openapi.model.AssetFaceUpdateDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**data** | [**List<AssetFaceUpdateItem>**](AssetFaceUpdateItem.md) | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
16
mobile/openapi/doc/AssetFaceUpdateItem.md
generated
Normal file
16
mobile/openapi/doc/AssetFaceUpdateItem.md
generated
Normal file
|
@ -0,0 +1,16 @@
|
|||
# openapi.model.AssetFaceUpdateItem
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**assetId** | **String** | |
|
||||
**personId** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
21
mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md
generated
Normal file
21
mobile/openapi/doc/AssetFaceWithoutPersonResponseDto.md
generated
Normal file
|
@ -0,0 +1,21 @@
|
|||
# openapi.model.AssetFaceWithoutPersonResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**boundingBoxX1** | **int** | |
|
||||
**boundingBoxX2** | **int** | |
|
||||
**boundingBoxY1** | **int** | |
|
||||
**boundingBoxY2** | **int** | |
|
||||
**id** | **String** | |
|
||||
**imageHeight** | **int** | |
|
||||
**imageWidth** | **int** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
2
mobile/openapi/doc/AssetResponseDto.md
generated
2
mobile/openapi/doc/AssetResponseDto.md
generated
|
@ -30,7 +30,7 @@ Name | Type | Description | Notes
|
|||
**originalPath** | **String** | |
|
||||
**owner** | [**UserResponseDto**](UserResponseDto.md) | | [optional]
|
||||
**ownerId** | **String** | |
|
||||
**people** | [**List<PersonResponseDto>**](PersonResponseDto.md) | | [optional] [default to const []]
|
||||
**people** | [**List<PersonWithFacesResponseDto>**](PersonWithFacesResponseDto.md) | | [optional] [default to const []]
|
||||
**resized** | **bool** | |
|
||||
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
|
||||
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]
|
||||
|
|
127
mobile/openapi/doc/FaceApi.md
generated
Normal file
127
mobile/openapi/doc/FaceApi.md
generated
Normal file
|
@ -0,0 +1,127 @@
|
|||
# openapi.api.FaceApi
|
||||
|
||||
## Load the API package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**getFaces**](FaceApi.md#getfaces) | **GET** /face |
|
||||
[**reassignFacesById**](FaceApi.md#reassignfacesbyid) | **PUT** /face/{id} |
|
||||
|
||||
|
||||
# **getFaces**
|
||||
> List<AssetFaceResponseDto> getFaces(id)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FaceApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getFaces(id);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling FaceApi->getFaces: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<AssetFaceResponseDto>**](AssetFaceResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **reassignFacesById**
|
||||
> PersonResponseDto reassignFacesById(id, faceDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = FaceApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final faceDto = FaceDto(); // FaceDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.reassignFacesById(id, faceDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling FaceApi->reassignFacesById: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**faceDto** | [**FaceDto**](FaceDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**PersonResponseDto**](PersonResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
15
mobile/openapi/doc/FaceDto.md
generated
Normal file
15
mobile/openapi/doc/FaceDto.md
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
# openapi.model.FaceDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
110
mobile/openapi/doc/PersonApi.md
generated
110
mobile/openapi/doc/PersonApi.md
generated
|
@ -9,16 +9,69 @@ All URIs are relative to */api*
|
|||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**createPerson**](PersonApi.md#createperson) | **POST** /person |
|
||||
[**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person |
|
||||
[**getPerson**](PersonApi.md#getperson) | **GET** /person/{id} |
|
||||
[**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
|
||||
[**getPersonStatistics**](PersonApi.md#getpersonstatistics) | **GET** /person/{id}/statistics |
|
||||
[**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
|
||||
[**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
|
||||
[**reassignFaces**](PersonApi.md#reassignfaces) | **PUT** /person/{id}/reassign |
|
||||
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
|
||||
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
|
||||
|
||||
# **createPerson**
|
||||
> PersonResponseDto createPerson()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = PersonApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.createPerson();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling PersonApi->createPerson: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**PersonResponseDto**](PersonResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **getAllPeople**
|
||||
> PeopleResponseDto getAllPeople(withHidden)
|
||||
|
||||
|
@ -351,6 +404,63 @@ Name | Type | Description | Notes
|
|||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **reassignFaces**
|
||||
> List<PersonResponseDto> reassignFaces(id, assetFaceUpdateDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = PersonApi();
|
||||
final id = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |
|
||||
final assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.reassignFaces(id, assetFaceUpdateDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling PersonApi->reassignFaces: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<PersonResponseDto>**](PersonResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **updatePeople**
|
||||
> List<BulkIdResponseDto> updatePeople(peopleUpdateDto)
|
||||
|
||||
|
|
20
mobile/openapi/doc/PersonWithFacesResponseDto.md
generated
Normal file
20
mobile/openapi/doc/PersonWithFacesResponseDto.md
generated
Normal file
|
@ -0,0 +1,20 @@
|
|||
# openapi.model.PersonWithFacesResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**birthDate** | [**DateTime**](DateTime.md) | |
|
||||
**faces** | [**List<AssetFaceWithoutPersonResponseDto>**](AssetFaceWithoutPersonResponseDto.md) | | [default to const []]
|
||||
**id** | **String** | |
|
||||
**isHidden** | **bool** | |
|
||||
**name** | **String** | |
|
||||
**thumbnailPath** | **String** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
7
mobile/openapi/lib/api.dart
generated
7
mobile/openapi/lib/api.dart
generated
|
@ -34,6 +34,7 @@ part 'api/album_api.dart';
|
|||
part 'api/asset_api.dart';
|
||||
part 'api/audit_api.dart';
|
||||
part 'api/authentication_api.dart';
|
||||
part 'api/face_api.dart';
|
||||
part 'api/job_api.dart';
|
||||
part 'api/library_api.dart';
|
||||
part 'api/o_auth_api.dart';
|
||||
|
@ -63,6 +64,10 @@ part 'model/asset_bulk_upload_check_dto.dart';
|
|||
part 'model/asset_bulk_upload_check_item.dart';
|
||||
part 'model/asset_bulk_upload_check_response_dto.dart';
|
||||
part 'model/asset_bulk_upload_check_result.dart';
|
||||
part 'model/asset_face_response_dto.dart';
|
||||
part 'model/asset_face_update_dto.dart';
|
||||
part 'model/asset_face_update_item.dart';
|
||||
part 'model/asset_face_without_person_response_dto.dart';
|
||||
part 'model/asset_file_upload_response_dto.dart';
|
||||
part 'model/asset_ids_dto.dart';
|
||||
part 'model/asset_ids_response_dto.dart';
|
||||
|
@ -97,6 +102,7 @@ part 'model/download_info_dto.dart';
|
|||
part 'model/download_response_dto.dart';
|
||||
part 'model/entity_type.dart';
|
||||
part 'model/exif_response_dto.dart';
|
||||
part 'model/face_dto.dart';
|
||||
part 'model/file_checksum_dto.dart';
|
||||
part 'model/file_checksum_response_dto.dart';
|
||||
part 'model/file_report_dto.dart';
|
||||
|
@ -132,6 +138,7 @@ part 'model/people_update_item.dart';
|
|||
part 'model/person_response_dto.dart';
|
||||
part 'model/person_statistics_response_dto.dart';
|
||||
part 'model/person_update_dto.dart';
|
||||
part 'model/person_with_faces_response_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
part 'model/reaction_level.dart';
|
||||
part 'model/reaction_type.dart';
|
||||
|
|
122
mobile/openapi/lib/api/face_api.dart
generated
Normal file
122
mobile/openapi/lib/api/face_api.dart
generated
Normal file
|
@ -0,0 +1,122 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
|
||||
class FaceApi {
|
||||
FaceApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'GET /face' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getFacesWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/face';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
queryParams.addAll(_queryParams('', 'id', id));
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<List<AssetFaceResponseDto>?> getFaces(String id,) async {
|
||||
final response = await getFacesWithHttpInfo(id,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<AssetFaceResponseDto>') as List)
|
||||
.cast<AssetFaceResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /face/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [FaceDto] faceDto (required):
|
||||
Future<Response> reassignFacesByIdWithHttpInfo(String id, FaceDto faceDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/face/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = faceDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [FaceDto] faceDto (required):
|
||||
Future<PersonResponseDto?> reassignFacesById(String id, FaceDto faceDto,) async {
|
||||
final response = await reassignFacesByIdWithHttpInfo(id, faceDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
96
mobile/openapi/lib/api/person_api.dart
generated
96
mobile/openapi/lib/api/person_api.dart
generated
|
@ -16,6 +16,47 @@ class PersonApi {
|
|||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'POST /person' operation and returns the [Response].
|
||||
Future<Response> createPersonWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/person';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<PersonResponseDto?> createPerson() async {
|
||||
final response = await createPersonWithHttpInfo();
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /person' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
@ -317,6 +358,61 @@ class PersonApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /person/{id}/reassign' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
|
||||
Future<Response> reassignFacesWithHttpInfo(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/person/{id}/reassign'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = assetFaceUpdateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
|
||||
Future<List<PersonResponseDto>?> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto,) async {
|
||||
final response = await reassignFacesWithHttpInfo(id, assetFaceUpdateDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<PersonResponseDto>') as List)
|
||||
.cast<PersonResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /person' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
|
12
mobile/openapi/lib/api_client.dart
generated
12
mobile/openapi/lib/api_client.dart
generated
|
@ -215,6 +215,14 @@ class ApiClient {
|
|||
return AssetBulkUploadCheckResponseDto.fromJson(value);
|
||||
case 'AssetBulkUploadCheckResult':
|
||||
return AssetBulkUploadCheckResult.fromJson(value);
|
||||
case 'AssetFaceResponseDto':
|
||||
return AssetFaceResponseDto.fromJson(value);
|
||||
case 'AssetFaceUpdateDto':
|
||||
return AssetFaceUpdateDto.fromJson(value);
|
||||
case 'AssetFaceUpdateItem':
|
||||
return AssetFaceUpdateItem.fromJson(value);
|
||||
case 'AssetFaceWithoutPersonResponseDto':
|
||||
return AssetFaceWithoutPersonResponseDto.fromJson(value);
|
||||
case 'AssetFileUploadResponseDto':
|
||||
return AssetFileUploadResponseDto.fromJson(value);
|
||||
case 'AssetIdsDto':
|
||||
|
@ -283,6 +291,8 @@ class ApiClient {
|
|||
return EntityTypeTypeTransformer().decode(value);
|
||||
case 'ExifResponseDto':
|
||||
return ExifResponseDto.fromJson(value);
|
||||
case 'FaceDto':
|
||||
return FaceDto.fromJson(value);
|
||||
case 'FileChecksumDto':
|
||||
return FileChecksumDto.fromJson(value);
|
||||
case 'FileChecksumResponseDto':
|
||||
|
@ -353,6 +363,8 @@ class ApiClient {
|
|||
return PersonStatisticsResponseDto.fromJson(value);
|
||||
case 'PersonUpdateDto':
|
||||
return PersonUpdateDto.fromJson(value);
|
||||
case 'PersonWithFacesResponseDto':
|
||||
return PersonWithFacesResponseDto.fromJson(value);
|
||||
case 'QueueStatusDto':
|
||||
return QueueStatusDto.fromJson(value);
|
||||
case 'ReactionLevel':
|
||||
|
|
158
mobile/openapi/lib/model/asset_face_response_dto.dart
generated
Normal file
158
mobile/openapi/lib/model/asset_face_response_dto.dart
generated
Normal file
|
@ -0,0 +1,158 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetFaceResponseDto {
|
||||
/// Returns a new [AssetFaceResponseDto] instance.
|
||||
AssetFaceResponseDto({
|
||||
required this.boundingBoxX1,
|
||||
required this.boundingBoxX2,
|
||||
required this.boundingBoxY1,
|
||||
required this.boundingBoxY2,
|
||||
required this.id,
|
||||
required this.imageHeight,
|
||||
required this.imageWidth,
|
||||
required this.person,
|
||||
});
|
||||
|
||||
int boundingBoxX1;
|
||||
|
||||
int boundingBoxX2;
|
||||
|
||||
int boundingBoxY1;
|
||||
|
||||
int boundingBoxY2;
|
||||
|
||||
String id;
|
||||
|
||||
int imageHeight;
|
||||
|
||||
int imageWidth;
|
||||
|
||||
PersonResponseDto? person;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto &&
|
||||
other.boundingBoxX1 == boundingBoxX1 &&
|
||||
other.boundingBoxX2 == boundingBoxX2 &&
|
||||
other.boundingBoxY1 == boundingBoxY1 &&
|
||||
other.boundingBoxY2 == boundingBoxY2 &&
|
||||
other.id == id &&
|
||||
other.imageHeight == imageHeight &&
|
||||
other.imageWidth == imageWidth &&
|
||||
other.person == person;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(boundingBoxX1.hashCode) +
|
||||
(boundingBoxX2.hashCode) +
|
||||
(boundingBoxY1.hashCode) +
|
||||
(boundingBoxY2.hashCode) +
|
||||
(id.hashCode) +
|
||||
(imageHeight.hashCode) +
|
||||
(imageWidth.hashCode) +
|
||||
(person == null ? 0 : person!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'boundingBoxX1'] = this.boundingBoxX1;
|
||||
json[r'boundingBoxX2'] = this.boundingBoxX2;
|
||||
json[r'boundingBoxY1'] = this.boundingBoxY1;
|
||||
json[r'boundingBoxY2'] = this.boundingBoxY2;
|
||||
json[r'id'] = this.id;
|
||||
json[r'imageHeight'] = this.imageHeight;
|
||||
json[r'imageWidth'] = this.imageWidth;
|
||||
if (this.person != null) {
|
||||
json[r'person'] = this.person;
|
||||
} else {
|
||||
// json[r'person'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceResponseDto(
|
||||
boundingBoxX1: mapValueOfType<int>(json, r'boundingBoxX1')!,
|
||||
boundingBoxX2: mapValueOfType<int>(json, r'boundingBoxX2')!,
|
||||
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
||||
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||
person: PersonResponseDto.fromJson(json[r'person']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'boundingBoxX1',
|
||||
'boundingBoxX2',
|
||||
'boundingBoxY1',
|
||||
'boundingBoxY2',
|
||||
'id',
|
||||
'imageHeight',
|
||||
'imageWidth',
|
||||
'person',
|
||||
};
|
||||
}
|
||||
|
98
mobile/openapi/lib/model/asset_face_update_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/asset_face_update_dto.dart
generated
Normal file
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetFaceUpdateDto {
|
||||
/// Returns a new [AssetFaceUpdateDto] instance.
|
||||
AssetFaceUpdateDto({
|
||||
this.data = const [],
|
||||
});
|
||||
|
||||
List<AssetFaceUpdateItem> data;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceUpdateDto &&
|
||||
other.data == data;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(data.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceUpdateDto[data=$data]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'data'] = this.data;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceUpdateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceUpdateDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceUpdateDto(
|
||||
data: AssetFaceUpdateItem.listFromJson(json[r'data']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceUpdateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceUpdateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceUpdateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceUpdateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceUpdateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceUpdateDto-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceUpdateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'data',
|
||||
};
|
||||
}
|
||||
|
106
mobile/openapi/lib/model/asset_face_update_item.dart
generated
Normal file
106
mobile/openapi/lib/model/asset_face_update_item.dart
generated
Normal file
|
@ -0,0 +1,106 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetFaceUpdateItem {
|
||||
/// Returns a new [AssetFaceUpdateItem] instance.
|
||||
AssetFaceUpdateItem({
|
||||
required this.assetId,
|
||||
required this.personId,
|
||||
});
|
||||
|
||||
String assetId;
|
||||
|
||||
String personId;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceUpdateItem &&
|
||||
other.assetId == assetId &&
|
||||
other.personId == personId;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetId.hashCode) +
|
||||
(personId.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceUpdateItem[assetId=$assetId, personId=$personId]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetId'] = this.assetId;
|
||||
json[r'personId'] = this.personId;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceUpdateItem] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceUpdateItem? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceUpdateItem(
|
||||
assetId: mapValueOfType<String>(json, r'assetId')!,
|
||||
personId: mapValueOfType<String>(json, r'personId')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceUpdateItem> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceUpdateItem>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceUpdateItem.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceUpdateItem> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceUpdateItem>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceUpdateItem.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceUpdateItem-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceUpdateItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceUpdateItem>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceUpdateItem.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetId',
|
||||
'personId',
|
||||
};
|
||||
}
|
||||
|
146
mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
generated
Normal file
146
mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
generated
Normal file
|
@ -0,0 +1,146 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class AssetFaceWithoutPersonResponseDto {
|
||||
/// Returns a new [AssetFaceWithoutPersonResponseDto] instance.
|
||||
AssetFaceWithoutPersonResponseDto({
|
||||
required this.boundingBoxX1,
|
||||
required this.boundingBoxX2,
|
||||
required this.boundingBoxY1,
|
||||
required this.boundingBoxY2,
|
||||
required this.id,
|
||||
required this.imageHeight,
|
||||
required this.imageWidth,
|
||||
});
|
||||
|
||||
int boundingBoxX1;
|
||||
|
||||
int boundingBoxX2;
|
||||
|
||||
int boundingBoxY1;
|
||||
|
||||
int boundingBoxY2;
|
||||
|
||||
String id;
|
||||
|
||||
int imageHeight;
|
||||
|
||||
int imageWidth;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto &&
|
||||
other.boundingBoxX1 == boundingBoxX1 &&
|
||||
other.boundingBoxX2 == boundingBoxX2 &&
|
||||
other.boundingBoxY1 == boundingBoxY1 &&
|
||||
other.boundingBoxY2 == boundingBoxY2 &&
|
||||
other.id == id &&
|
||||
other.imageHeight == imageHeight &&
|
||||
other.imageWidth == imageWidth;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(boundingBoxX1.hashCode) +
|
||||
(boundingBoxX2.hashCode) +
|
||||
(boundingBoxY1.hashCode) +
|
||||
(boundingBoxY2.hashCode) +
|
||||
(id.hashCode) +
|
||||
(imageHeight.hashCode) +
|
||||
(imageWidth.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'boundingBoxX1'] = this.boundingBoxX1;
|
||||
json[r'boundingBoxX2'] = this.boundingBoxX2;
|
||||
json[r'boundingBoxY1'] = this.boundingBoxY1;
|
||||
json[r'boundingBoxY2'] = this.boundingBoxY2;
|
||||
json[r'id'] = this.id;
|
||||
json[r'imageHeight'] = this.imageHeight;
|
||||
json[r'imageWidth'] = this.imageWidth;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [AssetFaceWithoutPersonResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return AssetFaceWithoutPersonResponseDto(
|
||||
boundingBoxX1: mapValueOfType<int>(json, r'boundingBoxX1')!,
|
||||
boundingBoxX2: mapValueOfType<int>(json, r'boundingBoxX2')!,
|
||||
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
|
||||
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<AssetFaceWithoutPersonResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <AssetFaceWithoutPersonResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = AssetFaceWithoutPersonResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, AssetFaceWithoutPersonResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, AssetFaceWithoutPersonResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = AssetFaceWithoutPersonResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of AssetFaceWithoutPersonResponseDto-objects as value to a dart map
|
||||
static Map<String, List<AssetFaceWithoutPersonResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<AssetFaceWithoutPersonResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = AssetFaceWithoutPersonResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'boundingBoxX1',
|
||||
'boundingBoxX2',
|
||||
'boundingBoxY1',
|
||||
'boundingBoxY2',
|
||||
'id',
|
||||
'imageHeight',
|
||||
'imageWidth',
|
||||
};
|
||||
}
|
||||
|
4
mobile/openapi/lib/model/asset_response_dto.dart
generated
4
mobile/openapi/lib/model/asset_response_dto.dart
generated
|
@ -104,7 +104,7 @@ class AssetResponseDto {
|
|||
|
||||
String ownerId;
|
||||
|
||||
List<PersonResponseDto> people;
|
||||
List<PersonWithFacesResponseDto> people;
|
||||
|
||||
bool resized;
|
||||
|
||||
|
@ -299,7 +299,7 @@ class AssetResponseDto {
|
|||
originalPath: mapValueOfType<String>(json, r'originalPath')!,
|
||||
owner: UserResponseDto.fromJson(json[r'owner']),
|
||||
ownerId: mapValueOfType<String>(json, r'ownerId')!,
|
||||
people: PersonResponseDto.listFromJson(json[r'people']),
|
||||
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
|
||||
resized: mapValueOfType<bool>(json, r'resized')!,
|
||||
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
|
||||
stack: AssetResponseDto.listFromJson(json[r'stack']),
|
||||
|
|
98
mobile/openapi/lib/model/face_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/face_dto.dart
generated
Normal file
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class FaceDto {
|
||||
/// Returns a new [FaceDto] instance.
|
||||
FaceDto({
|
||||
required this.id,
|
||||
});
|
||||
|
||||
String id;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is FaceDto &&
|
||||
other.id == id;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(id.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'FaceDto[id=$id]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'id'] = this.id;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [FaceDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static FaceDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return FaceDto(
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<FaceDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <FaceDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = FaceDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, FaceDto> mapFromJson(dynamic json) {
|
||||
final map = <String, FaceDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = FaceDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of FaceDto-objects as value to a dart map
|
||||
static Map<String, List<FaceDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<FaceDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = FaceDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'id',
|
||||
};
|
||||
}
|
||||
|
142
mobile/openapi/lib/model/person_with_faces_response_dto.dart
generated
Normal file
142
mobile/openapi/lib/model/person_with_faces_response_dto.dart
generated
Normal file
|
@ -0,0 +1,142 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PersonWithFacesResponseDto {
|
||||
/// Returns a new [PersonWithFacesResponseDto] instance.
|
||||
PersonWithFacesResponseDto({
|
||||
required this.birthDate,
|
||||
this.faces = const [],
|
||||
required this.id,
|
||||
required this.isHidden,
|
||||
required this.name,
|
||||
required this.thumbnailPath,
|
||||
});
|
||||
|
||||
DateTime? birthDate;
|
||||
|
||||
List<AssetFaceWithoutPersonResponseDto> faces;
|
||||
|
||||
String id;
|
||||
|
||||
bool isHidden;
|
||||
|
||||
String name;
|
||||
|
||||
String thumbnailPath;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PersonWithFacesResponseDto &&
|
||||
other.birthDate == birthDate &&
|
||||
other.faces == faces &&
|
||||
other.id == id &&
|
||||
other.isHidden == isHidden &&
|
||||
other.name == name &&
|
||||
other.thumbnailPath == thumbnailPath;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(birthDate == null ? 0 : birthDate!.hashCode) +
|
||||
(faces.hashCode) +
|
||||
(id.hashCode) +
|
||||
(isHidden.hashCode) +
|
||||
(name.hashCode) +
|
||||
(thumbnailPath.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PersonWithFacesResponseDto[birthDate=$birthDate, faces=$faces, id=$id, isHidden=$isHidden, name=$name, thumbnailPath=$thumbnailPath]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
if (this.birthDate != null) {
|
||||
json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc());
|
||||
} else {
|
||||
// json[r'birthDate'] = null;
|
||||
}
|
||||
json[r'faces'] = this.faces;
|
||||
json[r'id'] = this.id;
|
||||
json[r'isHidden'] = this.isHidden;
|
||||
json[r'name'] = this.name;
|
||||
json[r'thumbnailPath'] = this.thumbnailPath;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PersonWithFacesResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PersonWithFacesResponseDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PersonWithFacesResponseDto(
|
||||
birthDate: mapDateTime(json, r'birthDate', ''),
|
||||
faces: AssetFaceWithoutPersonResponseDto.listFromJson(json[r'faces']),
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden')!,
|
||||
name: mapValueOfType<String>(json, r'name')!,
|
||||
thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PersonWithFacesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PersonWithFacesResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PersonWithFacesResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PersonWithFacesResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PersonWithFacesResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PersonWithFacesResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PersonWithFacesResponseDto-objects as value to a dart map
|
||||
static Map<String, List<PersonWithFacesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PersonWithFacesResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PersonWithFacesResponseDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'birthDate',
|
||||
'faces',
|
||||
'id',
|
||||
'isHidden',
|
||||
'name',
|
||||
'thumbnailPath',
|
||||
};
|
||||
}
|
||||
|
62
mobile/openapi/test/asset_face_response_dto_test.dart
generated
Normal file
62
mobile/openapi/test/asset_face_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,62 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceResponseDto
|
||||
void main() {
|
||||
// final instance = AssetFaceResponseDto();
|
||||
|
||||
group('test AssetFaceResponseDto', () {
|
||||
// int boundingBoxX1
|
||||
test('to test the property `boundingBoxX1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxX2
|
||||
test('to test the property `boundingBoxX2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY1
|
||||
test('to test the property `boundingBoxY1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY2
|
||||
test('to test the property `boundingBoxY2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageHeight
|
||||
test('to test the property `imageHeight`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageWidth
|
||||
test('to test the property `imageWidth`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// PersonResponseDto person
|
||||
test('to test the property `person`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
27
mobile/openapi/test/asset_face_update_dto_test.dart
generated
Normal file
27
mobile/openapi/test/asset_face_update_dto_test.dart
generated
Normal file
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceUpdateDto
|
||||
void main() {
|
||||
// final instance = AssetFaceUpdateDto();
|
||||
|
||||
group('test AssetFaceUpdateDto', () {
|
||||
// List<AssetFaceUpdateItem> data (default value: const [])
|
||||
test('to test the property `data`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
32
mobile/openapi/test/asset_face_update_item_test.dart
generated
Normal file
32
mobile/openapi/test/asset_face_update_item_test.dart
generated
Normal file
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceUpdateItem
|
||||
void main() {
|
||||
// final instance = AssetFaceUpdateItem();
|
||||
|
||||
group('test AssetFaceUpdateItem', () {
|
||||
// String assetId
|
||||
test('to test the property `assetId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String personId
|
||||
test('to test the property `personId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
57
mobile/openapi/test/asset_face_without_person_response_dto_test.dart
generated
Normal file
57
mobile/openapi/test/asset_face_without_person_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,57 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for AssetFaceWithoutPersonResponseDto
|
||||
void main() {
|
||||
// final instance = AssetFaceWithoutPersonResponseDto();
|
||||
|
||||
group('test AssetFaceWithoutPersonResponseDto', () {
|
||||
// int boundingBoxX1
|
||||
test('to test the property `boundingBoxX1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxX2
|
||||
test('to test the property `boundingBoxX2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY1
|
||||
test('to test the property `boundingBoxY1`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int boundingBoxY2
|
||||
test('to test the property `boundingBoxY2`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageHeight
|
||||
test('to test the property `imageHeight`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// int imageWidth
|
||||
test('to test the property `imageWidth`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
2
mobile/openapi/test/asset_response_dto_test.dart
generated
2
mobile/openapi/test/asset_response_dto_test.dart
generated
|
@ -127,7 +127,7 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
// List<PersonResponseDto> people (default value: const [])
|
||||
// List<PersonWithFacesResponseDto> people (default value: const [])
|
||||
test('to test the property `people`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
|
31
mobile/openapi/test/face_api_test.dart
generated
Normal file
31
mobile/openapi/test/face_api_test.dart
generated
Normal file
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
|
||||
/// tests for FaceApi
|
||||
void main() {
|
||||
// final instance = FaceApi();
|
||||
|
||||
group('tests for FaceApi', () {
|
||||
//Future<List<AssetFaceResponseDto>> getFaces(String id) async
|
||||
test('test getFaces', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<PersonResponseDto> reassignFacesById(String id, FaceDto faceDto) async
|
||||
test('test reassignFacesById', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
});
|
||||
}
|
27
mobile/openapi/test/face_dto_test.dart
generated
Normal file
27
mobile/openapi/test/face_dto_test.dart
generated
Normal file
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for FaceDto
|
||||
void main() {
|
||||
// final instance = FaceDto();
|
||||
|
||||
group('test FaceDto', () {
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
10
mobile/openapi/test/person_api_test.dart
generated
10
mobile/openapi/test/person_api_test.dart
generated
|
@ -17,6 +17,11 @@ void main() {
|
|||
// final instance = PersonApi();
|
||||
|
||||
group('tests for PersonApi', () {
|
||||
//Future<PersonResponseDto> createPerson() async
|
||||
test('test createPerson', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<PeopleResponseDto> getAllPeople({ bool withHidden }) async
|
||||
test('test getAllPeople', () async {
|
||||
// TODO
|
||||
|
@ -47,6 +52,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<PersonResponseDto>> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto) async
|
||||
test('test reassignFaces', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
|
||||
test('test updatePeople', () async {
|
||||
// TODO
|
||||
|
|
52
mobile/openapi/test/person_with_faces_response_dto_test.dart
generated
Normal file
52
mobile/openapi/test/person_with_faces_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for PersonWithFacesResponseDto
|
||||
void main() {
|
||||
// final instance = PersonWithFacesResponseDto();
|
||||
|
||||
group('test PersonWithFacesResponseDto', () {
|
||||
// DateTime birthDate
|
||||
test('to test the property `birthDate`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// List<AssetFaceWithoutPersonResponseDto> faces (default value: const [])
|
||||
test('to test the property `faces`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool isHidden
|
||||
test('to test the property `isHidden`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String name
|
||||
test('to test the property `name`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String thumbnailPath
|
||||
test('to test the property `thumbnailPath`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
|
@ -3220,6 +3220,103 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/face": {
|
||||
"get": {
|
||||
"operationId": "getFaces",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Face"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/face/{id}": {
|
||||
"put": {
|
||||
"operationId": "reassignFacesById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FaceDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Face"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/jobs": {
|
||||
"get": {
|
||||
"operationId": "getAllJobsStatus",
|
||||
|
@ -4022,6 +4119,36 @@
|
|||
"Person"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createPerson",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Person"
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updatePeople",
|
||||
"parameters": [],
|
||||
|
@ -4258,6 +4385,61 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/person/{id}/reassign": {
|
||||
"put": {
|
||||
"operationId": "reassignFaces",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/AssetFaceUpdateDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"Person"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/person/{id}/statistics": {
|
||||
"get": {
|
||||
"operationId": "getPersonStatistics",
|
||||
|
@ -6557,6 +6739,118 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceResponseDto": {
|
||||
"properties": {
|
||||
"boundingBoxX1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxX2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"imageHeight": {
|
||||
"type": "integer"
|
||||
},
|
||||
"imageWidth": {
|
||||
"type": "integer"
|
||||
},
|
||||
"person": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
}
|
||||
],
|
||||
"nullable": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"imageHeight",
|
||||
"imageWidth",
|
||||
"boundingBoxX1",
|
||||
"boundingBoxX2",
|
||||
"boundingBoxY1",
|
||||
"boundingBoxY2",
|
||||
"person"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceUpdateDto": {
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceUpdateItem"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceUpdateItem": {
|
||||
"properties": {
|
||||
"assetId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"personId": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"personId",
|
||||
"assetId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFaceWithoutPersonResponseDto": {
|
||||
"properties": {
|
||||
"boundingBoxX1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxX2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY1": {
|
||||
"type": "integer"
|
||||
},
|
||||
"boundingBoxY2": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
},
|
||||
"imageHeight": {
|
||||
"type": "integer"
|
||||
},
|
||||
"imageWidth": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"imageHeight",
|
||||
"imageWidth",
|
||||
"boundingBoxX1",
|
||||
"boundingBoxX2",
|
||||
"boundingBoxY1",
|
||||
"boundingBoxY2"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"AssetFileUploadResponseDto": {
|
||||
"properties": {
|
||||
"duplicate": {
|
||||
|
@ -6719,7 +7013,7 @@
|
|||
},
|
||||
"people": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PersonResponseDto"
|
||||
"$ref": "#/components/schemas/PersonWithFacesResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
@ -7452,6 +7746,18 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FaceDto": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"format": "uuid",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileChecksumDto": {
|
||||
"properties": {
|
||||
"filenames": {
|
||||
|
@ -8147,6 +8453,42 @@
|
|||
},
|
||||
"type": "object"
|
||||
},
|
||||
"PersonWithFacesResponseDto": {
|
||||
"properties": {
|
||||
"birthDate": {
|
||||
"format": "date",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"faces": {
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"isHidden": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnailPath": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"birthDate",
|
||||
"faces",
|
||||
"id",
|
||||
"name",
|
||||
"thumbnailPath",
|
||||
"isHidden"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"QueueStatusDto": {
|
||||
"properties": {
|
||||
"isActive": {
|
||||
|
|
|
@ -41,6 +41,8 @@ export enum Permission {
|
|||
PERSON_READ = 'person.read',
|
||||
PERSON_WRITE = 'person.write',
|
||||
PERSON_MERGE = 'person.merge',
|
||||
PERSON_CREATE = 'person.create',
|
||||
PERSON_REASSIGN = 'person.reassign',
|
||||
|
||||
PARTNER_UPDATE = 'partner.update',
|
||||
}
|
||||
|
@ -247,6 +249,12 @@ export class AccessCore {
|
|||
case Permission.PERSON_MERGE:
|
||||
return await this.repository.person.checkOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_CREATE:
|
||||
return this.repository.person.hasFaceOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PERSON_REASSIGN:
|
||||
return this.repository.person.hasFaceOwnerAccess(authUser.id, ids);
|
||||
|
||||
case Permission.PARTNER_UPDATE:
|
||||
return await this.repository.partner.checkUpdateAccess(authUser.id, ids);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AssetEntity, AssetType } from '@app/infra/entities';
|
||||
import { AssetEntity, AssetFaceEntity, AssetType } from '@app/infra/entities';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { PersonResponseDto, mapFace } from '../../person/person.dto';
|
||||
import { PersonWithFacesResponseDto } from '../../person/person.dto';
|
||||
import { TagResponseDto, mapTag } from '../../tag';
|
||||
import { UserResponseDto, mapUser } from '../../user/response-dto/user-response.dto';
|
||||
import { ExifResponseDto, mapExif } from './exif-response.dto';
|
||||
|
@ -39,7 +39,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
exifInfo?: ExifResponseDto;
|
||||
smartInfo?: SmartInfoResponseDto;
|
||||
tags?: TagResponseDto[];
|
||||
people?: PersonResponseDto[];
|
||||
people?: PersonWithFacesResponseDto[];
|
||||
/**base64 encoded sha1 hash */
|
||||
checksum!: string;
|
||||
stackParentId?: string | null;
|
||||
|
@ -53,6 +53,24 @@ export type AssetMapOptions = {
|
|||
withStack?: boolean;
|
||||
};
|
||||
|
||||
const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => {
|
||||
const result: PersonWithFacesResponseDto[] = [];
|
||||
if (faces) {
|
||||
faces.forEach((face) => {
|
||||
if (face.person) {
|
||||
const existingPersonEntry = result.find((item) => item.id === face.person!.id);
|
||||
if (existingPersonEntry) {
|
||||
existingPersonEntry.faces.push(face);
|
||||
} else {
|
||||
result.push({ ...face.person!, faces: [face] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto {
|
||||
const { stripMetadata = false, withStack = false } = options;
|
||||
|
||||
|
@ -96,16 +114,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
|
|||
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
|
||||
livePhotoVideoId: entity.livePhotoVideoId,
|
||||
tags: entity.tags?.map(mapTag),
|
||||
people: entity.faces
|
||||
?.map(mapFace)
|
||||
.filter((person): person is PersonResponseDto => person !== null)
|
||||
.reduce((people, person) => {
|
||||
const existingPerson = people.find((p) => p.id === person.id);
|
||||
if (!existingPerson) {
|
||||
people.push(person);
|
||||
}
|
||||
return people;
|
||||
}, [] as PersonResponseDto[]),
|
||||
people: peopleWithFaces(entity.faces),
|
||||
checksum: entity.checksum.toString('base64'),
|
||||
stackParentId: entity.stackParentId,
|
||||
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,
|
||||
|
|
|
@ -201,7 +201,7 @@ export class JobService {
|
|||
const { id } = item.data;
|
||||
const person = await this.personRepository.getById(id);
|
||||
if (person) {
|
||||
this.communicationRepository.send(CommunicationEvent.PERSON_THUMBNAIL, person.ownerId, id);
|
||||
this.communicationRepository.send(CommunicationEvent.PERSON_THUMBNAIL, person.ownerId, person.id);
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { AuthUserDto } from '../auth';
|
||||
import { Optional, ValidateUUID, toBoolean } from '../domain.util';
|
||||
|
||||
export class PersonUpdateDto {
|
||||
|
@ -73,6 +74,51 @@ export class PersonResponseDto {
|
|||
isHidden!: boolean;
|
||||
}
|
||||
|
||||
export class PersonWithFacesResponseDto extends PersonResponseDto {
|
||||
faces!: AssetFaceWithoutPersonResponseDto[];
|
||||
}
|
||||
|
||||
export class AssetFaceWithoutPersonResponseDto {
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
imageHeight!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
imageWidth!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxX1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxX2!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY2!: number;
|
||||
}
|
||||
|
||||
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
|
||||
person!: PersonResponseDto | null;
|
||||
}
|
||||
|
||||
export class AssetFaceUpdateDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetFaceUpdateItem)
|
||||
data!: AssetFaceUpdateItem[];
|
||||
}
|
||||
|
||||
export class FaceDto {
|
||||
@ValidateUUID()
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export class AssetFaceUpdateItem {
|
||||
@ValidateUUID()
|
||||
personId!: string;
|
||||
|
||||
@ValidateUUID()
|
||||
assetId!: string;
|
||||
}
|
||||
|
||||
export class PersonStatisticsResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
assets!: number;
|
||||
|
@ -98,10 +144,15 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
|
|||
};
|
||||
}
|
||||
|
||||
export function mapFace(face: AssetFaceEntity): PersonResponseDto | null {
|
||||
if (face.person) {
|
||||
return mapPerson(face.person);
|
||||
}
|
||||
|
||||
return null;
|
||||
export function mapFaces(face: AssetFaceEntity, authUser: AuthUserDto): AssetFaceResponseDto {
|
||||
return {
|
||||
id: face.id,
|
||||
imageHeight: face.imageHeight,
|
||||
imageWidth: face.imageWidth,
|
||||
boundingBoxX1: face.boundingBoxX1,
|
||||
boundingBoxX2: face.boundingBoxX2,
|
||||
boundingBoxY1: face.boundingBoxY1,
|
||||
boundingBoxY2: face.boundingBoxY2,
|
||||
person: face.person?.ownerId === authUser.id ? mapPerson(face.person) : null,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import {
|
|||
ISystemConfigRepository,
|
||||
WithoutProperty,
|
||||
} from '../repositories';
|
||||
import { PersonResponseDto } from './person.dto';
|
||||
import { PersonResponseDto, mapFaces } from './person.dto';
|
||||
import { PersonService } from './person.service';
|
||||
|
||||
const responseDto: PersonResponseDto = {
|
||||
|
@ -339,7 +339,7 @@ describe(PersonService.name, () => {
|
|||
).resolves.toEqual(responseDto);
|
||||
|
||||
expect(personMock.getById).toHaveBeenCalledWith('person-1');
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.assetId });
|
||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id });
|
||||
expect(personMock.getFacesByIds).toHaveBeenCalledWith([
|
||||
{
|
||||
assetId: faceStub.face1.assetId,
|
||||
|
@ -375,6 +375,139 @@ describe(PersonService.name, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('reassignFaces', () => {
|
||||
it('should throw an error if user has no access to the person', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
|
||||
await expect(
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
data: [{ personId: 'asset-face-1', assetId: '' }],
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
});
|
||||
it('should reassign a face', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.withName.id]));
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getRandomFace.mockResolvedValue(faceStub.primaryFace1);
|
||||
await expect(
|
||||
sut.reassignFaces(authStub.admin, personStub.noName.id, {
|
||||
data: [{ personId: personStub.withName.id, assetId: assetStub.image.id }],
|
||||
}),
|
||||
).resolves.toEqual([personStub.noName]);
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonMigration', () => {
|
||||
it('should not move person files', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
await expect(sut.handlePersonMigration(personStub.noName)).resolves.toStrictEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFacesById', () => {
|
||||
it('should get the bounding boxes for an asset', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([faceStub.face1.assetId]));
|
||||
personMock.getFaces.mockResolvedValue([faceStub.primaryFace1]);
|
||||
await expect(sut.getFacesById(authStub.admin, { id: faceStub.face1.assetId })).resolves.toStrictEqual([
|
||||
mapFaces(faceStub.primaryFace1, authStub.admin),
|
||||
]);
|
||||
});
|
||||
it('should reject if the user has not access to the asset', async () => {
|
||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set());
|
||||
personMock.getFaces.mockResolvedValue([faceStub.primaryFace1]);
|
||||
await expect(sut.getFacesById(authStub.admin, { id: faceStub.primaryFace1.assetId })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNewFeaturePhoto', () => {
|
||||
it('should change person feature photo', async () => {
|
||||
personMock.getRandomFace.mockResolvedValue(faceStub.primaryFace1);
|
||||
await sut.createNewFeaturePhoto([personStub.newThumbnail.id]);
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
data: { id: personStub.newThumbnail.id },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reassignFacesById', () => {
|
||||
it('should create a new person', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
personMock.getRandomFace.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.reassignFacesById(authStub.admin, personStub.noName.id, {
|
||||
id: faceStub.face1.id,
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
birthDate: personStub.noName.birthDate,
|
||||
isHidden: personStub.noName.isHidden,
|
||||
id: personStub.noName.id,
|
||||
name: personStub.noName.name,
|
||||
thumbnailPath: personStub.noName.thumbnailPath,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should fail if user has not the correct permissions on the asset', async () => {
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set([personStub.noName.id]));
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set());
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
personMock.reassignFace.mockResolvedValue(1);
|
||||
personMock.getById.mockResolvedValue(personStub.noName);
|
||||
personMock.getRandomFace.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.reassignFacesById(authStub.admin, personStub.noName.id, {
|
||||
id: faceStub.face1.id,
|
||||
}),
|
||||
).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(jobMock.queue).not.toHaveBeenCalledWith();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createPerson', () => {
|
||||
it('should create a new person', async () => {
|
||||
personMock.create.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getFaceById.mockResolvedValue(faceStub.face1);
|
||||
accessMock.person.hasFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id]));
|
||||
|
||||
await expect(sut.createPerson(authStub.admin)).resolves.toBe(personStub.primaryPerson);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonDelete', () => {
|
||||
it('should stop if a person has not be found', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
|
||||
await expect(sut.handlePersonDelete({ id: 'person-1' })).resolves.toBe(false);
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
expect(storageMock.unlink).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should delete a person', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
|
||||
await expect(sut.handlePersonDelete({ id: 'person-1' })).resolves.toBe(true);
|
||||
expect(personMock.delete).toHaveBeenCalledWith(personStub.primaryPerson);
|
||||
expect(storageMock.unlink).toHaveBeenCalledWith(personStub.primaryPerson.thumbnailPath);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonCleanup', () => {
|
||||
it('should delete people without faces', async () => {
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]);
|
||||
|
@ -515,6 +648,7 @@ describe(PersonService.name, () => {
|
|||
searchMock.searchFaces.mockResolvedValue(faceSearch.oneRemoteMatch);
|
||||
personMock.create.mockResolvedValue(personStub.noName);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
personMock.createFace.mockResolvedValue(faceStub.primaryFace1);
|
||||
|
||||
await sut.handleRecognizeFaces({ id: assetStub.image.id });
|
||||
|
||||
|
@ -557,16 +691,16 @@ describe(PersonService.name, () => {
|
|||
expect(mediaMock.crop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip an person with a face asset id not found', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
it('should skip a person with a face asset id not found', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.id });
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mediaMock.crop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip a person with a face asset id without a thumbnail', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.face1);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]);
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
expect(mediaMock.crop).not.toHaveBeenCalled();
|
||||
|
@ -574,7 +708,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should generate a thumbnail', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.middle.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.middle]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.middle);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
|
@ -601,7 +735,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should generate a thumbnail without going negative', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.start.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.start]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.start);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
|
@ -622,7 +756,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should generate a thumbnail without overflowing', async () => {
|
||||
personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId });
|
||||
personMock.getFacesByIds.mockResolvedValue([faceStub.end]);
|
||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.end);
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||
|
||||
await sut.handleGeneratePersonThumbnail({ id: 'person-1' });
|
||||
|
@ -646,15 +780,12 @@ describe(PersonService.name, () => {
|
|||
it('should require person.write and person.merge permission', async () => {
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([]);
|
||||
personMock.delete.mockResolvedValue(personStub.mergePerson);
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf(
|
||||
BadRequestException,
|
||||
);
|
||||
|
||||
expect(personMock.prepareReassignFaces).not.toHaveBeenCalled();
|
||||
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
|
||||
expect(personMock.delete).not.toHaveBeenCalled();
|
||||
|
@ -664,7 +795,6 @@ describe(PersonService.name, () => {
|
|||
it('should merge two people', async () => {
|
||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValueOnce(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([]);
|
||||
personMock.delete.mockResolvedValue(personStub.mergePerson);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
@ -673,11 +803,6 @@ describe(PersonService.name, () => {
|
|||
{ id: 'person-2', success: true },
|
||||
]);
|
||||
|
||||
expect(personMock.prepareReassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
});
|
||||
|
||||
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
|
@ -690,29 +815,6 @@ describe(PersonService.name, () => {
|
|||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should delete conflicting faces before merging', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValue(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
||||
await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([
|
||||
{ id: 'person-2', success: true },
|
||||
]);
|
||||
|
||||
expect(personMock.prepareReassignFaces).toHaveBeenCalledWith({
|
||||
newPersonId: personStub.primaryPerson.id,
|
||||
oldPersonId: personStub.mergePerson.id,
|
||||
});
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.SEARCH_REMOVE_FACE,
|
||||
data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id },
|
||||
});
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
|
||||
});
|
||||
|
||||
it('should throw an error when the primary person is not found', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||
|
@ -735,7 +837,6 @@ describe(PersonService.name, () => {
|
|||
{ id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND },
|
||||
]);
|
||||
|
||||
expect(personMock.prepareReassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.reassignFaces).not.toHaveBeenCalled();
|
||||
expect(personMock.delete).not.toHaveBeenCalled();
|
||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1']));
|
||||
|
@ -744,7 +845,6 @@ describe(PersonService.name, () => {
|
|||
it('should handle an error reassigning faces', async () => {
|
||||
personMock.getById.mockResolvedValue(personStub.primaryPerson);
|
||||
personMock.getById.mockResolvedValue(personStub.mergePerson);
|
||||
personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]);
|
||||
personMock.reassignFaces.mockRejectedValue(new Error('update failed'));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2']));
|
||||
|
|
|
@ -28,6 +28,9 @@ import {
|
|||
import { StorageCore } from '../storage';
|
||||
import { SystemConfigCore } from '../system-config';
|
||||
import {
|
||||
AssetFaceResponseDto,
|
||||
AssetFaceUpdateDto,
|
||||
FaceDto,
|
||||
MergePersonDto,
|
||||
PeopleResponseDto,
|
||||
PeopleUpdateDto,
|
||||
|
@ -35,6 +38,7 @@ import {
|
|||
PersonSearchDto,
|
||||
PersonStatisticsResponseDto,
|
||||
PersonUpdateDto,
|
||||
mapFaces,
|
||||
mapPerson,
|
||||
} from './person.dto';
|
||||
|
||||
|
@ -80,6 +84,86 @@ export class PersonService {
|
|||
};
|
||||
}
|
||||
|
||||
createPerson(authUser: AuthUserDto): Promise<PersonResponseDto> {
|
||||
return this.repository.create({ ownerId: authUser.id });
|
||||
}
|
||||
|
||||
async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
|
||||
const person = await this.findOrFail(personId);
|
||||
const result: PersonResponseDto[] = [];
|
||||
const changeFeaturePhoto: string[] = [];
|
||||
for (const data of dto.data) {
|
||||
const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
|
||||
|
||||
for (const face of faces) {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_CREATE, face.id);
|
||||
if (person.faceAssetId === null) {
|
||||
changeFeaturePhoto.push(person.id);
|
||||
}
|
||||
if (face.person && face.person.faceAssetId === face.id) {
|
||||
changeFeaturePhoto.push(face.person.id);
|
||||
}
|
||||
|
||||
await this.repository.reassignFace(face.id, personId);
|
||||
}
|
||||
|
||||
result.push(person);
|
||||
}
|
||||
if (changeFeaturePhoto.length > 0) {
|
||||
// Remove duplicates
|
||||
await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async reassignFacesById(authUser: AuthUserDto, personId: string, dto: FaceDto): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
|
||||
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_CREATE, dto.id);
|
||||
const face = await this.repository.getFaceById(dto.id);
|
||||
const person = await this.findOrFail(personId);
|
||||
|
||||
await this.repository.reassignFace(face.id, personId);
|
||||
if (person.faceAssetId === null) {
|
||||
await this.createNewFeaturePhoto([person.id]);
|
||||
}
|
||||
if (face.person && face.person.faceAssetId === face.id) {
|
||||
await this.createNewFeaturePhoto([face.person.id]);
|
||||
}
|
||||
|
||||
return await this.findOrFail(personId).then(mapPerson);
|
||||
}
|
||||
|
||||
async getFacesById(authUser: AuthUserDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
await this.access.requirePermission(authUser, Permission.ASSET_READ, dto.id);
|
||||
const faces = await this.repository.getFaces(dto.id);
|
||||
return faces.map((asset) => mapFaces(asset, authUser));
|
||||
}
|
||||
|
||||
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
|
||||
this.logger.debug(
|
||||
`Changing feature photos for ${changeFeaturePhoto.length} ${changeFeaturePhoto.length > 1 ? 'people' : 'person'}`,
|
||||
);
|
||||
for (const personId of changeFeaturePhoto) {
|
||||
const assetFace = await this.repository.getRandomFace(personId);
|
||||
|
||||
if (assetFace !== null) {
|
||||
await this.repository.update({
|
||||
id: personId,
|
||||
faceAssetId: assetFace.id,
|
||||
});
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||
data: {
|
||||
id: personId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> {
|
||||
await this.access.requirePermission(authUser, Permission.PERSON_READ, id);
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
|
@ -128,7 +212,7 @@ export class PersonService {
|
|||
throw new BadRequestException('Invalid assetId for feature face');
|
||||
}
|
||||
|
||||
person = await this.repository.update({ id, faceAssetId: assetId });
|
||||
person = await this.repository.update({ id, faceAssetId: face.id });
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||
}
|
||||
|
||||
|
@ -255,9 +339,9 @@ export class PersonService {
|
|||
personId = newPerson.id;
|
||||
}
|
||||
|
||||
const faceId: AssetFaceId = { assetId: asset.id, personId };
|
||||
await this.repository.createFace({
|
||||
...faceId,
|
||||
const face = await this.repository.createFace({
|
||||
assetId: asset.id,
|
||||
personId,
|
||||
embedding,
|
||||
imageHeight: rest.imageHeight,
|
||||
imageWidth: rest.imageWidth,
|
||||
|
@ -266,10 +350,11 @@ export class PersonService {
|
|||
boundingBoxY1: rest.boundingBox.y1,
|
||||
boundingBoxY2: rest.boundingBox.y2,
|
||||
});
|
||||
const faceId: AssetFaceId = { assetId: asset.id, personId };
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACE, data: faceId });
|
||||
|
||||
if (newPerson) {
|
||||
await this.repository.update({ id: personId, faceAssetId: asset.id });
|
||||
await this.repository.update({ id: personId, faceAssetId: face.id });
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
||||
}
|
||||
}
|
||||
|
@ -304,14 +389,13 @@ export class PersonService {
|
|||
return false;
|
||||
}
|
||||
|
||||
const [face] = await this.repository.getFacesByIds([{ personId: person.id, assetId: person.faceAssetId }]);
|
||||
if (!face) {
|
||||
const face = await this.repository.getFaceByIdWithAssets(person.faceAssetId);
|
||||
if (face === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
assetId,
|
||||
personId,
|
||||
boundingBoxX1: x1,
|
||||
boundingBoxX2: x2,
|
||||
boundingBoxY1: y1,
|
||||
|
@ -324,8 +408,7 @@ export class PersonService {
|
|||
if (!asset?.resizePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.verbose(`Cropping face for person: ${personId}`);
|
||||
this.logger.verbose(`Cropping face for person: ${person.id}`);
|
||||
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
||||
this.storageCore.ensureFolders(thumbnailPath);
|
||||
|
||||
|
@ -395,10 +478,6 @@ export class PersonService {
|
|||
const mergeData: UpdateFacesData = { oldPersonId: mergeId, newPersonId: id };
|
||||
this.logger.log(`Merging ${mergeName} into ${primaryName}`);
|
||||
|
||||
const assetIds = await this.repository.prepareReassignFaces(mergeData);
|
||||
for (const assetId of assetIds) {
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_REMOVE_FACE, data: { assetId, personId: mergeId } });
|
||||
}
|
||||
await this.repository.reassignFaces(mergeData);
|
||||
await this.jobRepository.queue({ name: JobName.PERSON_DELETE, data: { id: mergePerson.id } });
|
||||
|
||||
|
|
|
@ -34,6 +34,7 @@ export interface IAccessRepository {
|
|||
};
|
||||
|
||||
person: {
|
||||
hasFaceOwnerAccess(userId: string, assetFaceId: Set<string>): Promise<Set<string>>;
|
||||
checkOwnerAccess(userId: string, personIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ export interface IPersonRepository {
|
|||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||
|
||||
getAssets(personId: string): Promise<AssetEntity[]>;
|
||||
prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
|
||||
|
||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||
|
||||
create(entity: Partial<PersonEntity>): Promise<PersonEntity>;
|
||||
|
@ -48,4 +48,8 @@ export interface IPersonRepository {
|
|||
getFacesByIds(ids: AssetFaceId[]): Promise<AssetFaceEntity[]>;
|
||||
getRandomFace(personId: string): Promise<AssetFaceEntity | null>;
|
||||
createFace(entity: Partial<AssetFaceEntity>): Promise<AssetFaceEntity>;
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]>;
|
||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||
getFaceByIdWithAssets(id: string): Promise<AssetFaceEntity | null>;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
AssetsController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
OAuthController,
|
||||
|
@ -50,6 +51,7 @@ import { ErrorInterceptor, FileUploadInterceptor } from './interceptors';
|
|||
APIKeyController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
OAuthController,
|
||||
|
|
28
server/src/immich/controllers/face.controller.ts
Normal file
28
server/src/immich/controllers/face.controller.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { AssetFaceResponseDto, AuthUserDto, FaceDto, PersonResponseDto, PersonService } from '@app/domain';
|
||||
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthUser, Authenticated } from '../app.guard';
|
||||
import { UseValidation } from '../app.utils';
|
||||
import { UUIDParamDto } from './dto/uuid-param.dto';
|
||||
|
||||
@ApiTags('Face')
|
||||
@Controller('face')
|
||||
@Authenticated()
|
||||
@UseValidation()
|
||||
export class FaceController {
|
||||
constructor(private service: PersonService) {}
|
||||
|
||||
@Get()
|
||||
getFaces(@AuthUser() authUser: AuthUserDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
return this.service.getFacesById(authUser, dto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
reassignFacesById(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: FaceDto,
|
||||
): Promise<PersonResponseDto> {
|
||||
return this.service.reassignFacesById(authUser, id, dto);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ export * from './app.controller';
|
|||
export * from './asset.controller';
|
||||
export * from './audit.controller';
|
||||
export * from './auth.controller';
|
||||
export * from './face.controller';
|
||||
export * from './job.controller';
|
||||
export * from './library.controller';
|
||||
export * from './oauth.controller';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AssetFaceUpdateDto,
|
||||
AssetResponseDto,
|
||||
AuthUserDto,
|
||||
BulkIdResponseDto,
|
||||
|
@ -34,6 +35,20 @@ export class PersonController {
|
|||
return this.service.getAll(authUser, withHidden);
|
||||
}
|
||||
|
||||
@Post()
|
||||
createPerson(@AuthUser() authUser: AuthUserDto): Promise<PersonResponseDto> {
|
||||
return this.service.createPerson(authUser);
|
||||
}
|
||||
|
||||
@Put(':id/reassign')
|
||||
reassignFaces(
|
||||
@AuthUser() authUser: AuthUserDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Body() dto: AssetFaceUpdateDto,
|
||||
): Promise<PersonResponseDto[]> {
|
||||
return this.service.reassignFaces(authUser, id, dto);
|
||||
}
|
||||
|
||||
@Put()
|
||||
updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.updatePeople(authUser, dto);
|
||||
|
|
|
@ -8,7 +8,6 @@ import {
|
|||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
import { AssetFaceEntity } from './asset-face.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
import { UserEntity } from './user.entity';
|
||||
|
||||
@Entity('person')
|
||||
|
@ -40,8 +39,8 @@ export class PersonEntity {
|
|||
@Column({ nullable: true })
|
||||
faceAssetId!: string | null;
|
||||
|
||||
@ManyToOne(() => AssetEntity, { onDelete: 'SET NULL', nullable: true })
|
||||
faceAsset!: AssetEntity | null;
|
||||
@ManyToOne(() => AssetFaceEntity, { onDelete: 'SET NULL', nullable: true })
|
||||
faceAsset!: AssetFaceEntity | null;
|
||||
|
||||
@OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person)
|
||||
faces!: AssetFaceEntity[];
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class EditFaceAssetForeignKey1699727044012 implements MigrationInterface {
|
||||
name = 'EditFaceAssetForeignKey1699727044012'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`);
|
||||
await queryRunner.query(`UPDATE person SET "faceAssetId" = asset_faces."id" FROM asset_faces WHERE person."faceAssetId" = asset_faces."assetId" AND person."id" = asset_faces."personId"`)
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "person" DROP CONSTRAINT "FK_2bbabe31656b6778c6b87b61023"`);
|
||||
await queryRunner.query(`UPDATE person SET "faceAssetId" = assets."id" FROM assets, asset_faces WHERE person."faceAssetId" = asset_faces."id" AND asset_faces."assetId" = assets."id"`);
|
||||
await queryRunner.query(`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "assets"("id") ON DELETE SET NULL ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
}
|
|
@ -5,6 +5,7 @@ import {
|
|||
ActivityEntity,
|
||||
AlbumEntity,
|
||||
AssetEntity,
|
||||
AssetFaceEntity,
|
||||
LibraryEntity,
|
||||
PartnerEntity,
|
||||
PersonEntity,
|
||||
|
@ -20,6 +21,7 @@ export class AccessRepository implements IAccessRepository {
|
|||
@InjectRepository(LibraryEntity) private libraryRepository: Repository<LibraryEntity>,
|
||||
@InjectRepository(PartnerEntity) private partnerRepository: Repository<PartnerEntity>,
|
||||
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
||||
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
||||
@InjectRepository(SharedLinkEntity) private sharedLinkRepository: Repository<SharedLinkEntity>,
|
||||
@InjectRepository(UserTokenEntity) private tokenRepository: Repository<UserTokenEntity>,
|
||||
) {}
|
||||
|
@ -318,6 +320,22 @@ export class AccessRepository implements IAccessRepository {
|
|||
})
|
||||
.then((persons) => new Set(persons.map((person) => person.id)));
|
||||
},
|
||||
hasFaceOwnerAccess: async (userId: string, assetFaceIds: Set<string>): Promise<Set<string>> => {
|
||||
if (assetFaceIds.size === 0) {
|
||||
return new Set();
|
||||
}
|
||||
return this.assetFaceRepository
|
||||
.find({
|
||||
select: { id: true },
|
||||
where: {
|
||||
id: In([...assetFaceIds]),
|
||||
asset: {
|
||||
ownerId: userId,
|
||||
},
|
||||
},
|
||||
})
|
||||
.then((faces) => new Set(faces.map((face) => face.id)));
|
||||
},
|
||||
};
|
||||
|
||||
partner = {
|
||||
|
|
|
@ -107,6 +107,48 @@ export class PersonRepository implements IPersonRepository {
|
|||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaces(assetId: string): Promise<AssetFaceEntity[]> {
|
||||
return this.assetFaceRepository.find({
|
||||
where: { assetId },
|
||||
relations: {
|
||||
person: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaceById(id: string): Promise<AssetFaceEntity> {
|
||||
return this.assetFaceRepository.findOneOrFail({
|
||||
where: { id },
|
||||
relations: {
|
||||
person: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getFaceByIdWithAssets(id: string): Promise<AssetFaceEntity | null> {
|
||||
return this.assetFaceRepository.findOne({
|
||||
where: { id },
|
||||
relations: {
|
||||
person: true,
|
||||
asset: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] })
|
||||
async reassignFace(assetFaceId: string, newPersonId: string): Promise<number> {
|
||||
const result = await this.assetFaceRepository
|
||||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({ personId: newPersonId })
|
||||
.where({ id: assetFaceId })
|
||||
.execute();
|
||||
|
||||
return result.affected ?? 0;
|
||||
}
|
||||
|
||||
getById(personId: string): Promise<PersonEntity | null> {
|
||||
return this.personRepository.findOne({ where: { id: personId } });
|
||||
}
|
||||
|
|
|
@ -133,24 +133,145 @@ GROUP BY
|
|||
HAVING
|
||||
COUNT("face"."assetId") = 0
|
||||
|
||||
-- PersonRepository.getById
|
||||
-- PersonRepository.getFaces
|
||||
SELECT
|
||||
"PersonEntity"."id" AS "PersonEntity_id",
|
||||
"PersonEntity"."createdAt" AS "PersonEntity_createdAt",
|
||||
"PersonEntity"."updatedAt" AS "PersonEntity_updatedAt",
|
||||
"PersonEntity"."ownerId" AS "PersonEntity_ownerId",
|
||||
"PersonEntity"."name" AS "PersonEntity_name",
|
||||
"PersonEntity"."birthDate" AS "PersonEntity_birthDate",
|
||||
"PersonEntity"."thumbnailPath" AS "PersonEntity_thumbnailPath",
|
||||
"PersonEntity"."faceAssetId" AS "PersonEntity_faceAssetId",
|
||||
"PersonEntity"."isHidden" AS "PersonEntity_isHidden"
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
FROM
|
||||
"person" "PersonEntity"
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
WHERE
|
||||
("PersonEntity"."id" = $1)
|
||||
("AssetFaceEntity"."assetId" = $1)
|
||||
|
||||
-- PersonRepository.getFaceById
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
WHERE
|
||||
("AssetFaceEntity"."id" = $1)
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetFaceEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- PersonRepository.getFaceByIdWithAssets
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetFaceEntity_id" AS "ids_AssetFaceEntity_id"
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
"AssetFaceEntity"."id" AS "AssetFaceEntity_id",
|
||||
"AssetFaceEntity"."assetId" AS "AssetFaceEntity_assetId",
|
||||
"AssetFaceEntity"."personId" AS "AssetFaceEntity_personId",
|
||||
"AssetFaceEntity"."embedding" AS "AssetFaceEntity_embedding",
|
||||
"AssetFaceEntity"."imageWidth" AS "AssetFaceEntity_imageWidth",
|
||||
"AssetFaceEntity"."imageHeight" AS "AssetFaceEntity_imageHeight",
|
||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_person_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."name" AS "AssetFaceEntity__AssetFaceEntity_person_name",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."birthDate" AS "AssetFaceEntity__AssetFaceEntity_person_birthDate",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_person_thumbnailPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."faceAssetId" AS "AssetFaceEntity__AssetFaceEntity_person_faceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_person"."isHidden" AS "AssetFaceEntity__AssetFaceEntity_person_isHidden",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."resizePath" AS "AssetFaceEntity__AssetFaceEntity_asset_resizePath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."webpPath" AS "AssetFaceEntity__AssetFaceEntity_asset_webpPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_updatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_deletedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileCreatedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileCreatedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."localDateTime" AS "AssetFaceEntity__AssetFaceEntity_asset_localDateTime",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."fileModifiedAt" AS "AssetFaceEntity__AssetFaceEntity_asset_fileModifiedAt",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isFavorite" AS "AssetFaceEntity__AssetFaceEntity_asset_isFavorite",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isArchived" AS "AssetFaceEntity__AssetFaceEntity_asset_isArchived",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isExternal" AS "AssetFaceEntity__AssetFaceEntity_asset_isExternal",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isReadOnly" AS "AssetFaceEntity__AssetFaceEntity_asset_isReadOnly",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isOffline" AS "AssetFaceEntity__AssetFaceEntity_asset_isOffline",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."checksum" AS "AssetFaceEntity__AssetFaceEntity_asset_checksum",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."duration" AS "AssetFaceEntity__AssetFaceEntity_asset_duration",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."isVisible" AS "AssetFaceEntity__AssetFaceEntity_asset_isVisible",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."livePhotoVideoId" AS "AssetFaceEntity__AssetFaceEntity_asset_livePhotoVideoId",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."originalFileName" AS "AssetFaceEntity__AssetFaceEntity_asset_originalFileName",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."sidecarPath" AS "AssetFaceEntity__AssetFaceEntity_asset_sidecarPath",
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."stackParentId" AS "AssetFaceEntity__AssetFaceEntity_asset_stackParentId"
|
||||
FROM
|
||||
"asset_faces" "AssetFaceEntity"
|
||||
LEFT JOIN "person" "AssetFaceEntity__AssetFaceEntity_person" ON "AssetFaceEntity__AssetFaceEntity_person"."id" = "AssetFaceEntity"."personId"
|
||||
LEFT JOIN "assets" "AssetFaceEntity__AssetFaceEntity_asset" ON "AssetFaceEntity__AssetFaceEntity_asset"."id" = "AssetFaceEntity"."assetId"
|
||||
AND (
|
||||
"AssetFaceEntity__AssetFaceEntity_asset"."deletedAt" IS NULL
|
||||
)
|
||||
WHERE
|
||||
("AssetFaceEntity"."id" = $1)
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetFaceEntity_id" ASC
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- PersonRepository.reassignFace
|
||||
UPDATE "asset_faces"
|
||||
SET
|
||||
"personId" = $1
|
||||
WHERE
|
||||
"id" = $2
|
||||
|
||||
-- PersonRepository.getByName
|
||||
SELECT
|
||||
"person"."id" AS "person_id",
|
||||
|
|
7
server/test/fixtures/person.stub.ts
vendored
7
server/test/fixtures/person.stub.ts
vendored
|
@ -1,5 +1,4 @@
|
|||
import { PersonEntity } from '@app/infra/entities';
|
||||
import { assetStub } from '@test/fixtures/asset.stub';
|
||||
import { userStub } from './user.stub';
|
||||
|
||||
export const personStub = {
|
||||
|
@ -41,7 +40,7 @@ export const personStub = {
|
|||
birthDate: null,
|
||||
thumbnailPath: '/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: null,
|
||||
faceAssetId: 'assetFaceId',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
}),
|
||||
|
@ -97,8 +96,8 @@ export const personStub = {
|
|||
birthDate: null,
|
||||
thumbnailPath: '/new/path/to/thumbnail.jpg',
|
||||
faces: [],
|
||||
faceAssetId: assetStub.image.id,
|
||||
faceAsset: assetStub.image,
|
||||
faceAssetId: 'asset-id',
|
||||
faceAsset: null,
|
||||
isHidden: false,
|
||||
}),
|
||||
primaryPerson: Object.freeze<PersonEntity>({
|
||||
|
|
|
@ -50,6 +50,7 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock =>
|
|||
},
|
||||
|
||||
person: {
|
||||
hasFaceOwnerAccess: jest.fn(),
|
||||
checkOwnerAccess: jest.fn().mockResolvedValue(new Set()),
|
||||
},
|
||||
|
||||
|
|
|
@ -20,8 +20,12 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
|
|||
getAllFaces: jest.fn(),
|
||||
getFacesByIds: jest.fn(),
|
||||
getRandomFace: jest.fn(),
|
||||
prepareReassignFaces: jest.fn(),
|
||||
|
||||
reassignFaces: jest.fn(),
|
||||
createFace: jest.fn(),
|
||||
getFaces: jest.fn(),
|
||||
reassignFace: jest.fn(),
|
||||
getFaceById: jest.fn(),
|
||||
getFaceByIdWithAssets: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
UserApiFp,
|
||||
AuditApi,
|
||||
ActivityApi,
|
||||
FaceApi,
|
||||
} from './open-api';
|
||||
import { BASE_PATH } from './open-api/base';
|
||||
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
|
||||
|
@ -33,6 +34,7 @@ class ImmichApi {
|
|||
public assetApi: AssetApi;
|
||||
public auditApi: AuditApi;
|
||||
public authenticationApi: AuthenticationApi;
|
||||
public faceApi: FaceApi;
|
||||
public jobApi: JobApi;
|
||||
public keyApi: APIKeyApi;
|
||||
public oauthApi: OAuthApi;
|
||||
|
@ -60,6 +62,7 @@ class ImmichApi {
|
|||
this.libraryApi = new LibraryApi(this.config);
|
||||
this.assetApi = new AssetApi(this.config);
|
||||
this.authenticationApi = new AuthenticationApi(this.config);
|
||||
this.faceApi = new FaceApi(this.config);
|
||||
this.jobApi = new JobApi(this.config);
|
||||
this.keyApi = new APIKeyApi(this.config);
|
||||
this.oauthApi = new OAuthApi(this.config);
|
||||
|
|
588
web/src/api/open-api/api.ts
generated
588
web/src/api/open-api/api.ts
generated
|
@ -586,6 +586,142 @@ export const AssetBulkUploadCheckResultReasonEnum = {
|
|||
|
||||
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceResponseDto
|
||||
*/
|
||||
export interface AssetFaceResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxX1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxX2': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxY1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'boundingBoxY2': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'imageHeight': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'imageWidth': number;
|
||||
/**
|
||||
*
|
||||
* @type {PersonResponseDto}
|
||||
* @memberof AssetFaceResponseDto
|
||||
*/
|
||||
'person': PersonResponseDto | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceUpdateDto
|
||||
*/
|
||||
export interface AssetFaceUpdateDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetFaceUpdateItem>}
|
||||
* @memberof AssetFaceUpdateDto
|
||||
*/
|
||||
'data': Array<AssetFaceUpdateItem>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceUpdateItem
|
||||
*/
|
||||
export interface AssetFaceUpdateItem {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceUpdateItem
|
||||
*/
|
||||
'assetId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceUpdateItem
|
||||
*/
|
||||
'personId': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
export interface AssetFaceWithoutPersonResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxX1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxX2': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxY1': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'boundingBoxY2': number;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'imageHeight': number;
|
||||
/**
|
||||
*
|
||||
* @type {number}
|
||||
* @memberof AssetFaceWithoutPersonResponseDto
|
||||
*/
|
||||
'imageWidth': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -842,10 +978,10 @@ export interface AssetResponseDto {
|
|||
'ownerId': string;
|
||||
/**
|
||||
*
|
||||
* @type {Array<PersonResponseDto>}
|
||||
* @type {Array<PersonWithFacesResponseDto>}
|
||||
* @memberof AssetResponseDto
|
||||
*/
|
||||
'people'?: Array<PersonResponseDto>;
|
||||
'people'?: Array<PersonWithFacesResponseDto>;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
|
@ -1672,6 +1808,19 @@ export interface ExifResponseDto {
|
|||
*/
|
||||
'timeZone'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface FaceDto
|
||||
*/
|
||||
export interface FaceDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceDto
|
||||
*/
|
||||
'id': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -2564,6 +2713,49 @@ export interface PersonUpdateDto {
|
|||
*/
|
||||
'name'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PersonWithFacesResponseDto
|
||||
*/
|
||||
export interface PersonWithFacesResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'birthDate': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<AssetFaceWithoutPersonResponseDto>}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'faces': Array<AssetFaceWithoutPersonResponseDto>;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'isHidden': boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'name': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonWithFacesResponseDto
|
||||
*/
|
||||
'thumbnailPath': string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -11349,6 +11541,233 @@ export class AuthenticationApi extends BaseAPI {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* FaceApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getFaces: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getFaces', 'id', id)
|
||||
const localVarPath = `/face`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
if (id !== undefined) {
|
||||
localVarQueryParameter['id'] = id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {FaceDto} faceDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFacesById: async (id: string, faceDto: FaceDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('reassignFacesById', 'id', id)
|
||||
// verify required parameter 'faceDto' is not null or undefined
|
||||
assertParamExists('reassignFacesById', 'faceDto', faceDto)
|
||||
const localVarPath = `/face/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(faceDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FaceApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = FaceApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getFaces(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetFaceResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getFaces(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {FaceDto} faceDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async reassignFacesById(id: string, faceDto: FaceDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFacesById(id, faceDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* FaceApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const FaceApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = FaceApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetFaceResponseDto>> {
|
||||
return localVarFp.getFaces(requestParameters.id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||
return localVarFp.reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Request parameters for getFaces operation in FaceApi.
|
||||
* @export
|
||||
* @interface FaceApiGetFacesRequest
|
||||
*/
|
||||
export interface FaceApiGetFacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceApiGetFaces
|
||||
*/
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for reassignFacesById operation in FaceApi.
|
||||
* @export
|
||||
* @interface FaceApiReassignFacesByIdRequest
|
||||
*/
|
||||
export interface FaceApiReassignFacesByIdRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof FaceApiReassignFacesById
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {FaceDto}
|
||||
* @memberof FaceApiReassignFacesById
|
||||
*/
|
||||
readonly faceDto: FaceDto
|
||||
}
|
||||
|
||||
/**
|
||||
* FaceApi - object-oriented interface
|
||||
* @export
|
||||
* @class FaceApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class FaceApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiGetFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof FaceApi
|
||||
*/
|
||||
public getFaces(requestParameters: FaceApiGetFacesRequest, options?: AxiosRequestConfig) {
|
||||
return FaceApiFp(this.configuration).getFaces(requestParameters.id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {FaceApiReassignFacesByIdRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof FaceApi
|
||||
*/
|
||||
public reassignFacesById(requestParameters: FaceApiReassignFacesByIdRequest, options?: AxiosRequestConfig) {
|
||||
return FaceApiFp(this.configuration).reassignFacesById(requestParameters.id, requestParameters.faceDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* JobApi - axios parameter creator
|
||||
* @export
|
||||
|
@ -13180,6 +13599,44 @@ export class PartnerApi extends BaseAPI {
|
|||
*/
|
||||
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createPerson: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/person`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [withHidden]
|
||||
|
@ -13439,6 +13896,54 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFaces: async (id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('reassignFaces', 'id', id)
|
||||
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
|
||||
assertParamExists('reassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
|
||||
const localVarPath = `/person/{id}/reassign`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(assetFaceUpdateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
|
@ -13541,6 +14046,15 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
|||
export const PersonApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createPerson(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {boolean} [withHidden]
|
||||
|
@ -13602,6 +14116,17 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async reassignFaces(id: string, assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.reassignFaces(id, assetFaceUpdateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
|
@ -13633,6 +14158,14 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
|||
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = PersonApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createPerson(options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
|
||||
return localVarFp.createPerson(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13687,6 +14220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
|
|||
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||
return localVarFp.reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13799,6 +14341,27 @@ export interface PersonApiMergePersonRequest {
|
|||
readonly mergePersonDto: MergePersonDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for reassignFaces operation in PersonApi.
|
||||
* @export
|
||||
* @interface PersonApiReassignFacesRequest
|
||||
*/
|
||||
export interface PersonApiReassignFacesRequest {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof PersonApiReassignFaces
|
||||
*/
|
||||
readonly id: string
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {AssetFaceUpdateDto}
|
||||
* @memberof PersonApiReassignFaces
|
||||
*/
|
||||
readonly assetFaceUpdateDto: AssetFaceUpdateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePeople operation in PersonApi.
|
||||
* @export
|
||||
|
@ -13841,6 +14404,16 @@ export interface PersonApiUpdatePersonRequest {
|
|||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class PersonApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public createPerson(options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).createPerson(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
|
||||
|
@ -13907,6 +14480,17 @@ export class PersonApi extends BaseAPI {
|
|||
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiReassignFacesRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public reassignFaces(requestParameters: PersonApiReassignFacesRequest, options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).reassignFaces(requestParameters.id, requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
|
|
|
@ -560,7 +560,7 @@
|
|||
|
||||
<section
|
||||
id="immich-asset-viewer"
|
||||
class="fixed left-0 top-0 z-[1001] grid h-screen w-screen grid-cols-4 grid-rows-[64px_1fr] overflow-y-hidden bg-black"
|
||||
class="fixed left-0 top-0 z-[1001] grid h-screen w-screen grid-cols-4 grid-rows-[64px_1fr] overflow-hidden bg-black"
|
||||
>
|
||||
<!-- Top navigation bar -->
|
||||
{#if $slideshowState === SlideshowState.None}
|
||||
|
|
|
@ -15,16 +15,18 @@
|
|||
mdiCalendar,
|
||||
mdiCameraIris,
|
||||
mdiClose,
|
||||
mdiPencil,
|
||||
mdiEye,
|
||||
mdiEyeOff,
|
||||
mdiImageOutline,
|
||||
mdiMapMarkerOutline,
|
||||
mdiInformationOutline,
|
||||
mdiEye,
|
||||
mdiEyeOff,
|
||||
mdiPencil,
|
||||
} from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import PersonSidePanel from '../faces-page/person-side-panel.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import Map from '../shared-components/map/map.svelte';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import ChangeLocation from '../shared-components/change-location.svelte';
|
||||
|
@ -35,8 +37,21 @@
|
|||
export let albums: AlbumResponseDto[] = [];
|
||||
export let albumId: string | null = null;
|
||||
|
||||
let showAssetPath = false;
|
||||
let textarea: HTMLTextAreaElement;
|
||||
let description: string;
|
||||
let showEditFaces = false;
|
||||
let previousId: string;
|
||||
|
||||
$: {
|
||||
if (!previousId) {
|
||||
previousId = asset.id;
|
||||
}
|
||||
if (asset.id !== previousId) {
|
||||
showEditFaces = false;
|
||||
previousId = asset.id;
|
||||
}
|
||||
}
|
||||
|
||||
$: isOwner = $page?.data?.user?.id === asset.ownerId;
|
||||
|
||||
|
@ -84,6 +99,14 @@
|
|||
return undefined;
|
||||
};
|
||||
|
||||
const handleRefreshPeople = async () => {
|
||||
await api.assetApi.getAssetById({ id: asset.id }).then((res) => {
|
||||
people = res.data?.people || [];
|
||||
textarea.value = res.data?.exifInfo?.description || '';
|
||||
});
|
||||
showEditFaces = false;
|
||||
};
|
||||
|
||||
const autoGrowHeight = (e: Event) => {
|
||||
const target = e.target as HTMLTextAreaElement;
|
||||
target.style.height = 'auto';
|
||||
|
@ -106,7 +129,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
let showAssetPath = false;
|
||||
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
|
||||
|
||||
let isShowChangeDate = false;
|
||||
|
@ -139,7 +161,7 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
|
||||
<div class="flex place-items-center gap-2">
|
||||
<button
|
||||
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
|
@ -183,54 +205,71 @@
|
|||
<section class="px-4 py-4 text-sm">
|
||||
<div class="flex h-10 w-full items-center justify-between">
|
||||
<h2>PEOPLE</h2>
|
||||
{#if people.some((person) => person.isHidden)}
|
||||
<div class="flex gap-2">
|
||||
{#if people.some((person) => person.isHidden)}
|
||||
<CircleIconButton
|
||||
title="Show hidden people"
|
||||
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||
padding="1"
|
||||
on:click={() => (showingHiddenPeople = !showingHiddenPeople)}
|
||||
/>
|
||||
{/if}
|
||||
<CircleIconButton
|
||||
title="Show hidden people"
|
||||
icon={showingHiddenPeople ? mdiEyeOff : mdiEye}
|
||||
title="Edit people"
|
||||
icon={mdiPencil}
|
||||
padding="1"
|
||||
on:click={() => (showingHiddenPeople = !showingHiddenPeople)}
|
||||
size="20"
|
||||
on:click={() => (showEditFaces = true)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{#each people as person (person.id)}
|
||||
<a
|
||||
href="/people/{person.id}?previousRoute={albumId ? `${AppRoute.ALBUMS}/${albumId}` : AppRoute.PHOTOS}"
|
||||
class="w-[90px] {!showingHiddenPeople && person.isHidden ? 'hidden' : ''}"
|
||||
on:click={() => dispatch('close-viewer')}
|
||||
{#each people as person, index (person.id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex={index}
|
||||
on:focus={() => ($boundingBoxesArray = people[index].faces)}
|
||||
on:mouseover={() => ($boundingBoxesArray = people[index].faces)}
|
||||
on:mouseleave={() => ($boundingBoxesArray = [])}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={person.isHidden}
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
{#if person.birthDate}
|
||||
{@const personBirthDate = DateTime.fromISO(person.birthDate)}
|
||||
<p
|
||||
class="font-light"
|
||||
title={personBirthDate.toLocaleString(
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
>
|
||||
Age {Math.floor(DateTime.fromISO(asset.fileCreatedAt).diff(personBirthDate, 'years').years)}
|
||||
</p>
|
||||
{/if}
|
||||
</a>
|
||||
<a
|
||||
href="/people/{person.id}?previousRoute={albumId ? `${AppRoute.ALBUMS}/${albumId}` : AppRoute.PHOTOS}"
|
||||
class="w-[90px] {!showingHiddenPeople && person.isHidden ? 'hidden' : ''}"
|
||||
on:click={() => dispatch('close-viewer')}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
title={person.name}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={person.isHidden}
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
{#if person.birthDate}
|
||||
{@const personBirthDate = DateTime.fromISO(person.birthDate)}
|
||||
<p
|
||||
class="font-light"
|
||||
title={personBirthDate.toLocaleString(
|
||||
{
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
},
|
||||
{ locale: $locale },
|
||||
)}
|
||||
>
|
||||
Age {Math.floor(DateTime.fromISO(asset.fileCreatedAt).diff(personBirthDate, 'years').years)}
|
||||
</p>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
|
@ -589,3 +628,13 @@
|
|||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if showEditFaces}
|
||||
<PersonSidePanel
|
||||
assetId={asset.id}
|
||||
on:close={() => {
|
||||
showEditFaces = false;
|
||||
}}
|
||||
on:refresh={handleRefreshPeople}
|
||||
/>
|
||||
{/if}
|
||||
|
|
|
@ -8,6 +8,9 @@
|
|||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||
import { photoViewer } from '$lib/stores/assets.store';
|
||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let element: HTMLDivElement | undefined = undefined;
|
||||
|
@ -20,6 +23,13 @@
|
|||
let copyImageToClipboard: (src: string) => Promise<Blob>;
|
||||
let canCopyImagesToClipboard: () => boolean;
|
||||
|
||||
$: if (imgElement) {
|
||||
createZoomImageWheel(imgElement, {
|
||||
maxZoom: 10,
|
||||
wheelZoomRatio: 0.2,
|
||||
});
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
// Import hack :( see https://github.com/vadimkorr/svelte-carousel/issues/27#issuecomment-851022295
|
||||
// TODO: Move to regular import once the package correctly supports ESM.
|
||||
|
@ -29,6 +39,7 @@
|
|||
});
|
||||
|
||||
onDestroy(() => {
|
||||
$boundingBoxesArray = [];
|
||||
abortController?.abort();
|
||||
});
|
||||
|
||||
|
@ -105,16 +116,10 @@
|
|||
|
||||
if (state.currentZoom > 1 && isWebCompatibleImage(asset) && !hasZoomed) {
|
||||
hasZoomed = true;
|
||||
|
||||
loadAssetData({ loadOriginal: true });
|
||||
}
|
||||
});
|
||||
|
||||
$: if (imgElement) {
|
||||
createZoomImageWheel(imgElement, {
|
||||
maxZoom: 10,
|
||||
wheelZoomRatio: 0.2,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} />
|
||||
|
@ -129,12 +134,19 @@
|
|||
{:then}
|
||||
<div bind:this={imgElement} class="h-full w-full">
|
||||
<img
|
||||
bind:this={$photoViewer}
|
||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||
src={assetData}
|
||||
alt={asset.id}
|
||||
class="h-full w-full object-contain"
|
||||
draggable="false"
|
||||
/>
|
||||
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewer) as boundingbox}
|
||||
<div
|
||||
class="absolute border-solid border-white border-[3px] rounded-lg p-3"
|
||||
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
|
||||
{#if hidden}
|
||||
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
|
||||
<Icon path={mdiEyeOffOutline} size="2em" class="text-{eyeColor}" />
|
||||
<Icon {title} path={mdiEyeOffOutline} size="2em" class="text-{eyeColor}" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
export let size: string | number = '1em';
|
||||
export let color = 'currentColor';
|
||||
export let path: string;
|
||||
export let title = '';
|
||||
export let title: string | null = null;
|
||||
export let desc = '';
|
||||
export let flipped = false;
|
||||
let className = '';
|
||||
|
|
246
web/src/lib/components/faces-page/assign-face-side-panel.svelte
Normal file
246
web/src/lib/components/faces-page/assign-face-side-panel.svelte
Normal file
|
@ -0,0 +1,246 @@
|
|||
<script lang="ts">
|
||||
import { api, type AssetFaceResponseDto, type PersonResponseDto } from '@api';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { linear } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import { getPersonNameWithHiddenValue, searchNameLocal } from '$lib/utils/person';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { photoViewer } from '$lib/stores/assets.store';
|
||||
|
||||
export let peopleWithFaces: AssetFaceResponseDto[];
|
||||
export let allPeople: PersonResponseDto[];
|
||||
export let editedPersonIndex: number;
|
||||
|
||||
// loading spinners
|
||||
let isShowLoadingNewPerson = false;
|
||||
let isShowLoadingSearch = false;
|
||||
|
||||
// search people
|
||||
let searchedPeople: PersonResponseDto[] = [];
|
||||
let searchedPeopleCopy: PersonResponseDto[] = [];
|
||||
let searchWord: string;
|
||||
let searchFaces = false;
|
||||
let searchName = '';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const handleBackButton = () => {
|
||||
dispatch('close');
|
||||
};
|
||||
const zoomImageToBase64 = async (face: AssetFaceResponseDto): Promise<string | null> => {
|
||||
if ($photoViewer === null) {
|
||||
return null;
|
||||
}
|
||||
const { boundingBoxX1: x1, boundingBoxX2: x2, boundingBoxY1: y1, boundingBoxY2: y2 } = face;
|
||||
|
||||
const coordinates = {
|
||||
x1: ($photoViewer.naturalWidth / face.imageWidth) * x1,
|
||||
x2: ($photoViewer.naturalWidth / face.imageWidth) * x2,
|
||||
y1: ($photoViewer.naturalHeight / face.imageHeight) * y1,
|
||||
y2: ($photoViewer.naturalHeight / face.imageHeight) * y2,
|
||||
};
|
||||
|
||||
const faceWidth = coordinates.x2 - coordinates.x1;
|
||||
const faceHeight = coordinates.y2 - coordinates.y1;
|
||||
|
||||
const faceImage = new Image();
|
||||
faceImage.src = $photoViewer.src;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
faceImage.onload = resolve;
|
||||
faceImage.onerror = () => resolve(null);
|
||||
});
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = faceWidth;
|
||||
canvas.height = faceHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(faceImage, coordinates.x1, coordinates.y1, faceWidth, faceHeight, 0, 0, faceWidth, faceHeight);
|
||||
|
||||
return canvas.toDataURL();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePerson = async () => {
|
||||
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), 100);
|
||||
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id);
|
||||
|
||||
const newFeaturePhoto = personToUpdate ? await zoomImageToBase64(personToUpdate) : null;
|
||||
|
||||
dispatch('createPerson', newFeaturePhoto);
|
||||
|
||||
clearTimeout(timeout);
|
||||
isShowLoadingNewPerson = false;
|
||||
dispatch('createPerson', newFeaturePhoto);
|
||||
};
|
||||
|
||||
const searchPeople = async () => {
|
||||
if ((searchedPeople.length < 20 && searchName.startsWith(searchWord)) || searchName === '') {
|
||||
return;
|
||||
}
|
||||
const timeout = setTimeout(() => (isShowLoadingSearch = true), 100);
|
||||
try {
|
||||
const { data } = await api.searchApi.searchPerson({ name: searchName });
|
||||
searchedPeople = data;
|
||||
searchedPeopleCopy = data;
|
||||
searchWord = searchName;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't search people");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
isShowLoadingSearch = false;
|
||||
};
|
||||
|
||||
$: {
|
||||
searchedPeople = searchNameLocal(searchName, searchedPeopleCopy, 10);
|
||||
}
|
||||
|
||||
const initInput = (element: HTMLInputElement) => {
|
||||
element.focus();
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
||||
class="absolute top-0 z-[2001] h-full w-[360px] overflow-x-hidden p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
{#if !searchFaces}
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={handleBackButton}
|
||||
>
|
||||
<div>
|
||||
<Icon path={mdiArrowLeftThin} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Select face</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<button
|
||||
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
title="Search existing person"
|
||||
on:click={() => {
|
||||
searchFaces = true;
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Icon path={mdiMagnify} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
{#if !isShowLoadingNewPerson}
|
||||
<button
|
||||
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={handleCreatePerson}
|
||||
title="Create new person"
|
||||
>
|
||||
<div>
|
||||
<Icon path={mdiPlus} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="flex place-content-center place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={handleBackButton}
|
||||
>
|
||||
<div>
|
||||
<Icon path={mdiArrowLeftThin} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<div class="w-full flex">
|
||||
<input
|
||||
class="w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg"
|
||||
type="text"
|
||||
placeholder="Name or nickname"
|
||||
bind:value={searchName}
|
||||
on:input={searchPeople}
|
||||
use:initInput
|
||||
/>
|
||||
{#if isShowLoadingSearch}
|
||||
<div>
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="flex place-content-center place-items-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={() => (searchFaces = false)}
|
||||
>
|
||||
<div>
|
||||
<Icon path={mdiClose} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="px-4 py-4 text-sm">
|
||||
<h2 class="mb-8 mt-4 uppercase">All people</h2>
|
||||
<div class="immich-scrollbar mt-4 flex flex-wrap gap-2 overflow-y-auto">
|
||||
{#if searchName == ''}
|
||||
{#each allPeople as person (person.id)}
|
||||
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id}
|
||||
<div class="w-fit">
|
||||
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={getPersonNameWithHiddenValue(person.name, person.isHidden)}
|
||||
title={getPersonNameWithHiddenValue(person.name, person.isHidden)}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={person.isHidden}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p class="mt-1 truncate font-medium" title={getPersonNameWithHiddenValue(person.name, person.isHidden)}>
|
||||
{person.name}
|
||||
</p>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
{#each searchedPeople as person (person.id)}
|
||||
{#if person.id !== peopleWithFaces[editedPersonIndex].person?.id}
|
||||
<div class="w-fit">
|
||||
<button class="w-[90px]" on:click={() => dispatch('reassign', person)}>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={api.getPeopleThumbnailUrl(person.id)}
|
||||
altText={getPersonNameWithHiddenValue(person.name, person.isHidden)}
|
||||
title={getPersonNameWithHiddenValue(person.name, person.isHidden)}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={person.isHidden}
|
||||
/>
|
||||
</div>
|
||||
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
|
@ -12,23 +12,18 @@
|
|||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { mdiCallMerge, mdiClose, mdiMagnify, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { searchNameLocal } from '$lib/utils/person';
|
||||
import PeopleList from './people-list.svelte';
|
||||
import { page } from '$app/stores';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
let people: PersonResponseDto[] = [];
|
||||
let peopleCopy: PersonResponseDto[] = [];
|
||||
let selectedPeople: PersonResponseDto[] = [];
|
||||
let screenHeight: number;
|
||||
let isShowConfirmation = false;
|
||||
let name = '';
|
||||
let searchWord: string;
|
||||
let isSearchingPeople = false;
|
||||
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
$: hasSelection = selectedPeople.length > 0;
|
||||
|
@ -39,44 +34,12 @@
|
|||
onMount(async () => {
|
||||
const { data } = await api.personApi.getAllPeople({ withHidden: false });
|
||||
people = data.people;
|
||||
peopleCopy = cloneDeep(people);
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
dispatch('go-back');
|
||||
};
|
||||
|
||||
const resetSearch = () => {
|
||||
name = '';
|
||||
people = peopleCopy;
|
||||
};
|
||||
|
||||
const searchPeople = async (force: boolean) => {
|
||||
if (name === '') {
|
||||
people = peopleCopy;
|
||||
return;
|
||||
}
|
||||
if (!force) {
|
||||
if (people.length < 20 && name.startsWith(searchWord)) {
|
||||
people = searchNameLocal(name, peopleCopy, 10);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => (isSearchingPeople = true), 100);
|
||||
try {
|
||||
const { data } = await api.searchApi.searchPerson({ name });
|
||||
people = data;
|
||||
searchWord = name;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't search people");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
isSearchingPeople = false;
|
||||
};
|
||||
|
||||
const handleSwapPeople = () => {
|
||||
[person, selectedPeople[0]] = [selectedPeople[0], person];
|
||||
$page.url.searchParams.set('action', 'merge');
|
||||
|
@ -113,7 +76,7 @@
|
|||
});
|
||||
dispatch('merge');
|
||||
} catch (error) {
|
||||
handleError(error, 'Cannot merge faces');
|
||||
handleError(error, 'Cannot merge people');
|
||||
} finally {
|
||||
isShowConfirmation = false;
|
||||
}
|
||||
|
@ -131,7 +94,7 @@
|
|||
{#if hasSelection}
|
||||
Selected {selectedPeople.length}
|
||||
{:else}
|
||||
Merge faces
|
||||
Merge people
|
||||
{/if}
|
||||
<div />
|
||||
</svelte:fragment>
|
||||
|
@ -151,7 +114,7 @@
|
|||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<section id="merge-face-selector relative">
|
||||
<div class="mb-10 h-[200px] place-content-center place-items-center">
|
||||
<p class="mb-4 text-center uppercase dark:text-white">Choose matching faces to merge</p>
|
||||
<p class="mb-4 text-center uppercase dark:text-white">Choose matching people to merge</p>
|
||||
|
||||
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
|
||||
{#each selectedPeople as person (person.id)}
|
||||
|
@ -178,57 +141,25 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-40 sm:w-48 md:w-96 h-14 rounded-lg bg-gray-100 p-2 dark:bg-gray-700 mb-8 gap-2 place-items-center"
|
||||
>
|
||||
<button on:click={() => searchPeople(true)}>
|
||||
<div class="w-fit">
|
||||
<Icon path={mdiMagnify} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||
type="text"
|
||||
placeholder="Search names"
|
||||
bind:value={name}
|
||||
on:input={() => searchPeople(false)}
|
||||
/>
|
||||
{#if name}
|
||||
<button on:click={resetSearch}>
|
||||
<Icon path={mdiClose} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if isSearchingPeople}
|
||||
<div class="flex place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 pt-8 px-8 pb-10 dark:bg-immich-dark-gray"
|
||||
style:max-height={screenHeight - 250 - 250 + 'px'}
|
||||
>
|
||||
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
|
||||
{#each unselectedPeople as person (person.id)}
|
||||
<FaceThumbnail {person} on:click={() => onSelect(person)} circle border selectable />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<PeopleList
|
||||
people={unselectedPeople}
|
||||
peopleCopy={unselectedPeople}
|
||||
unselectedPeople={selectedPeople}
|
||||
{screenHeight}
|
||||
on:select={({ detail }) => onSelect(detail)}
|
||||
/>
|
||||
</section>
|
||||
|
||||
{#if isShowConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Merge faces"
|
||||
title="Merge people"
|
||||
confirmText="Merge"
|
||||
on:confirm={handleMerge}
|
||||
on:cancel={() => (isShowConfirmation = false)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<p>Are you sure you want merge these faces? <br />This action is <strong>irreversible</strong>.</p>
|
||||
</svelte:fragment>
|
||||
<p>Are you sure you want merge these people ?</p></svelte:fragment
|
||||
>
|
||||
</ConfirmDialogue>
|
||||
{/if}
|
||||
</section>
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
>
|
||||
<div class="relative flex items-center justify-between">
|
||||
<h1 class="truncate px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Merge faces - {title}
|
||||
Merge People - {title}
|
||||
</h1>
|
||||
<div class="p-2">
|
||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('close')} />
|
||||
|
@ -108,7 +108,7 @@
|
|||
</div>
|
||||
|
||||
<div class="flex px-4 md:px-8 md:pt-4">
|
||||
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same face?</h1>
|
||||
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
|
||||
</div>
|
||||
<div class="flex px-4 pt-2 md:px-8">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
|
||||
|
|
|
@ -14,12 +14,12 @@
|
|||
export let person: PersonResponseDto;
|
||||
export let preload = false;
|
||||
|
||||
type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face';
|
||||
type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-people' | 'hide-person';
|
||||
let dispatch = createEventDispatcher<{
|
||||
'change-name': void;
|
||||
'set-birth-date': void;
|
||||
'merge-faces': void;
|
||||
'hide-face': void;
|
||||
'merge-people': void;
|
||||
'hide-person': void;
|
||||
}>();
|
||||
|
||||
let showVerticalDots = false;
|
||||
|
@ -82,10 +82,10 @@
|
|||
{#if showContextMenu}
|
||||
<Portal target="body">
|
||||
<ContextMenu {...contextMenuPosition} on:outclick={() => onMenuExit()}>
|
||||
<MenuOption on:click={() => onMenuClick('hide-face')} text="Hide face" />
|
||||
<MenuOption on:click={() => onMenuClick('hide-person')} text="Hide Person" />
|
||||
<MenuOption on:click={() => onMenuClick('change-name')} text="Change name" />
|
||||
<MenuOption on:click={() => onMenuClick('set-birth-date')} text="Set date of birth" />
|
||||
<MenuOption on:click={() => onMenuClick('merge-faces')} text="Merge faces" />
|
||||
<MenuOption on:click={() => onMenuClick('merge-people')} text="Merge People" />
|
||||
</ContextMenu>
|
||||
</Portal>
|
||||
{/if}
|
||||
|
|
106
web/src/lib/components/faces-page/people-list.svelte
Normal file
106
web/src/lib/components/faces-page/people-list.svelte
Normal file
|
@ -0,0 +1,106 @@
|
|||
<script lang="ts">
|
||||
import { api, type PersonResponseDto } from '@api';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import { mdiClose, mdiMagnify } from '@mdi/js';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { searchNameLocal } from '$lib/utils/person';
|
||||
|
||||
export let screenHeight: number;
|
||||
export let people: PersonResponseDto[];
|
||||
export let peopleCopy: PersonResponseDto[];
|
||||
export let unselectedPeople: PersonResponseDto[];
|
||||
|
||||
let name = '';
|
||||
let searchWord: string;
|
||||
let isSearchingPeople = false;
|
||||
|
||||
let dispatch = createEventDispatcher<{
|
||||
select: PersonResponseDto;
|
||||
}>();
|
||||
|
||||
const resetSearch = () => {
|
||||
name = '';
|
||||
people = peopleCopy;
|
||||
};
|
||||
|
||||
$: {
|
||||
people = peopleCopy.filter(
|
||||
(person) => !unselectedPeople.some((unselectedPerson) => unselectedPerson.id === person.id),
|
||||
);
|
||||
people = searchNameLocal(name, people, 10);
|
||||
}
|
||||
|
||||
const searchPeople = async (force: boolean) => {
|
||||
if (name === '') {
|
||||
people = peopleCopy;
|
||||
return;
|
||||
}
|
||||
if (!force) {
|
||||
if (people.length < 20 && name.startsWith(searchWord)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => (isSearchingPeople = true), 100);
|
||||
try {
|
||||
const { data } = await api.searchApi.searchPerson({ name });
|
||||
people = data;
|
||||
searchWord = name;
|
||||
} catch (error) {
|
||||
handleError(error, "Can't search people");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
isSearchingPeople = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex w-40 sm:w-48 md:w-96 h-14 rounded-lg bg-gray-100 p-2 dark:bg-gray-700 mb-8 gap-2 place-items-center">
|
||||
<button on:click={() => searchPeople(true)}>
|
||||
<div class="w-fit">
|
||||
<Icon path={mdiMagnify} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||
type="text"
|
||||
placeholder="Search names"
|
||||
bind:value={name}
|
||||
on:input={() => searchPeople(false)}
|
||||
/>
|
||||
{#if name}
|
||||
<button on:click={resetSearch}>
|
||||
<Icon path={mdiClose} />
|
||||
</button>
|
||||
{/if}
|
||||
{#if isSearchingPeople}
|
||||
<div class="flex place-items-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
|
||||
style:max-height={screenHeight - 400 + 'px'}
|
||||
>
|
||||
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
|
||||
{#each people as person (person.id)}
|
||||
<FaceThumbnail
|
||||
{person}
|
||||
on:click={() => {
|
||||
dispatch('select', person);
|
||||
}}
|
||||
circle
|
||||
border
|
||||
selectable
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
278
web/src/lib/components/faces-page/person-side-panel.svelte
Normal file
278
web/src/lib/components/faces-page/person-side-panel.svelte
Normal file
|
@ -0,0 +1,278 @@
|
|||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import { linear } from 'svelte/easing';
|
||||
import { api, type PersonResponseDto, AssetFaceResponseDto } from '@api';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
import { mdiArrowLeftThin, mdiRestart } from '@mdi/js';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
||||
import { getPersonNameWithHiddenValue } from '$lib/utils/person';
|
||||
|
||||
export let assetId: string;
|
||||
|
||||
// keep track of the changes
|
||||
let numberOfPersonToCreate: string[] = [];
|
||||
let numberOfAssetFaceGenerated: string[] = [];
|
||||
|
||||
// faces
|
||||
let peopleWithFaces: AssetFaceResponseDto[] = [];
|
||||
let selectedPersonToReassign: (PersonResponseDto | null)[];
|
||||
let selectedPersonToCreate: (string | null)[];
|
||||
let editedPersonIndex: number;
|
||||
|
||||
// loading spinners
|
||||
let isShowLoadingDone = false;
|
||||
let isShowLoadingPeople = false;
|
||||
|
||||
// search people
|
||||
let showSeletecFaces = false;
|
||||
let allPeople: PersonResponseDto[] = [];
|
||||
|
||||
// timers
|
||||
let loaderLoadingDoneTimeout: NodeJS.Timeout;
|
||||
let automaticRefreshTimeout: NodeJS.Timeout;
|
||||
|
||||
const { onPersonThumbnail } = websocketStore;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// Reset value
|
||||
$onPersonThumbnail = '';
|
||||
|
||||
$: {
|
||||
if ($onPersonThumbnail) {
|
||||
numberOfAssetFaceGenerated.push($onPersonThumbnail);
|
||||
if (
|
||||
isEqual(numberOfAssetFaceGenerated, numberOfPersonToCreate) &&
|
||||
loaderLoadingDoneTimeout &&
|
||||
automaticRefreshTimeout &&
|
||||
selectedPersonToCreate.filter((person) => person !== null).length === numberOfPersonToCreate.length
|
||||
) {
|
||||
clearTimeout(loaderLoadingDoneTimeout);
|
||||
clearTimeout(automaticRefreshTimeout);
|
||||
dispatch('refresh');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const timeout = setTimeout(() => (isShowLoadingPeople = true), 100);
|
||||
try {
|
||||
const { data } = await api.personApi.getAllPeople({ withHidden: true });
|
||||
allPeople = data.people;
|
||||
const result = await api.faceApi.getFaces({ id: assetId });
|
||||
peopleWithFaces = result.data;
|
||||
selectedPersonToCreate = new Array<string | null>(peopleWithFaces.length);
|
||||
selectedPersonToReassign = new Array<PersonResponseDto | null>(peopleWithFaces.length);
|
||||
} catch (error) {
|
||||
handleError(error, "Can't get faces");
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
isShowLoadingPeople = false;
|
||||
});
|
||||
|
||||
const isEqual = (a: string[], b: string[]): boolean => {
|
||||
return b.every((valueB) => a.includes(valueB));
|
||||
};
|
||||
|
||||
const handleBackButton = () => {
|
||||
dispatch('close');
|
||||
};
|
||||
|
||||
const handleReset = (index: number) => {
|
||||
if (selectedPersonToReassign[index]) {
|
||||
selectedPersonToReassign[index] = null;
|
||||
}
|
||||
if (selectedPersonToCreate[index]) {
|
||||
selectedPersonToCreate[index] = null;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditFaces = async () => {
|
||||
loaderLoadingDoneTimeout = setTimeout(() => (isShowLoadingDone = true), 100);
|
||||
const numberOfChanges =
|
||||
selectedPersonToCreate.filter((person) => person !== null).length +
|
||||
selectedPersonToReassign.filter((person) => person !== null).length;
|
||||
if (numberOfChanges > 0) {
|
||||
try {
|
||||
for (let i = 0; i < peopleWithFaces.length; i++) {
|
||||
const personId = selectedPersonToReassign[i]?.id;
|
||||
|
||||
if (personId) {
|
||||
await api.faceApi.reassignFacesById({
|
||||
id: personId,
|
||||
faceDto: { id: peopleWithFaces[i].id },
|
||||
});
|
||||
} else if (selectedPersonToCreate[i]) {
|
||||
const { data } = await api.personApi.createPerson();
|
||||
numberOfPersonToCreate.push(data.id);
|
||||
await api.faceApi.reassignFacesById({
|
||||
id: data.id,
|
||||
faceDto: { id: peopleWithFaces[i].id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
notificationController.show({
|
||||
message: `Edited ${numberOfChanges} ${numberOfChanges > 1 ? 'people' : 'person'}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, "Can't apply changes");
|
||||
}
|
||||
}
|
||||
|
||||
isShowLoadingDone = false;
|
||||
if (numberOfPersonToCreate.length === 0) {
|
||||
clearTimeout(loaderLoadingDoneTimeout);
|
||||
dispatch('refresh');
|
||||
} else {
|
||||
automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreatePerson = (newFeaturePhoto: string | null) => {
|
||||
const personToUpdate = peopleWithFaces.find((person) => person.id === peopleWithFaces[editedPersonIndex].id);
|
||||
if (newFeaturePhoto && personToUpdate) {
|
||||
selectedPersonToCreate[peopleWithFaces.indexOf(personToUpdate)] = newFeaturePhoto;
|
||||
}
|
||||
showSeletecFaces = false;
|
||||
};
|
||||
|
||||
const handleReassignFace = (person: PersonResponseDto | null) => {
|
||||
if (person) {
|
||||
selectedPersonToReassign[editedPersonIndex] = person;
|
||||
showSeletecFaces = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handlePersonPicker = async (index: number) => {
|
||||
editedPersonIndex = index;
|
||||
showSeletecFaces = true;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
transition:fly={{ x: 360, duration: 100, easing: linear }}
|
||||
class="absolute top-0 z-[2000] h-full w-[360px] overflow-x-hidden p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
class="flex place-content-center rounded-full p-3 transition-colors hover:bg-gray-200 dark:text-immich-dark-fg dark:hover:bg-gray-900"
|
||||
on:click={handleBackButton}
|
||||
>
|
||||
<div>
|
||||
<Icon path={mdiArrowLeftThin} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Edit faces</p>
|
||||
</div>
|
||||
{#if !isShowLoadingDone}
|
||||
<button
|
||||
class="justify-self-end rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
|
||||
on:click={() => handleEditFaces()}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4 text-sm">
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{#if isShowLoadingPeople}
|
||||
<div class="flex w-full justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else}
|
||||
{#each peopleWithFaces as face, index}
|
||||
{#if face.person}
|
||||
<div class="relative z-[20001] h-[115px] w-[95px]">
|
||||
<div
|
||||
role="button"
|
||||
tabindex={index}
|
||||
class="absolute left-0 top-0 h-[90px] w-[90px] cursor-default"
|
||||
on:focus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
on:mouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
on:mouseleave={() => ($boundingBoxesArray = [])}
|
||||
>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
shadow
|
||||
url={selectedPersonToCreate[index] ||
|
||||
api.getPeopleThumbnailUrl(selectedPersonToReassign[index]?.id || face.person.id)}
|
||||
altText={selectedPersonToReassign[index]
|
||||
? selectedPersonToReassign[index]?.name || ''
|
||||
: selectedPersonToCreate[index]
|
||||
? 'new person'
|
||||
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
|
||||
title={selectedPersonToReassign[index]
|
||||
? selectedPersonToReassign[index]?.name || ''
|
||||
: selectedPersonToCreate[index]
|
||||
? 'new person'
|
||||
: getPersonNameWithHiddenValue(face.person?.name, face.person?.isHidden)}
|
||||
widthStyle="90px"
|
||||
heightStyle="90px"
|
||||
thumbhash={null}
|
||||
hidden={selectedPersonToReassign[index]
|
||||
? selectedPersonToReassign[index]?.isHidden
|
||||
: selectedPersonToCreate[index]
|
||||
? false
|
||||
: face.person?.isHidden}
|
||||
/>
|
||||
</div>
|
||||
{#if !selectedPersonToCreate[index]}
|
||||
<p class="relative mt-1 truncate font-medium" title={face.person?.name}>
|
||||
{#if selectedPersonToReassign[index]?.id}
|
||||
{selectedPersonToReassign[index]?.name}
|
||||
{:else}
|
||||
{face.person?.name}
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700">
|
||||
{#if selectedPersonToCreate[index] || selectedPersonToReassign[index]}
|
||||
<button on:click={() => handleReset(index)} class="flex h-full w-full">
|
||||
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
|
||||
<div>
|
||||
<Icon path={mdiRestart} size={18} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<button on:click={() => handlePersonPicker(index)} class="flex h-full w-full">
|
||||
<div
|
||||
class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white"
|
||||
/>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if showSeletecFaces}
|
||||
<AssignFaceSidePanel
|
||||
{peopleWithFaces}
|
||||
{allPeople}
|
||||
{editedPersonIndex}
|
||||
on:close={() => (showSeletecFaces = false)}
|
||||
on:createPerson={(event) => handleCreatePerson(event.detail)}
|
||||
on:reassign={(event) => handleReassignFace(event.detail)}
|
||||
/>
|
||||
{/if}
|
|
@ -22,12 +22,12 @@
|
|||
>
|
||||
<div class="flex items-center">
|
||||
<CircleIconButton icon={mdiClose} on:click={() => dispatch('closeClick')} />
|
||||
<p class="ml-4 hidden sm:block">Show & hide faces</p>
|
||||
<p class="ml-4 hidden sm:block">Show & hide people</p>
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex items-center md:mr-8">
|
||||
<CircleIconButton
|
||||
title="Reset faces visibility"
|
||||
title="Reset people visibility"
|
||||
icon={mdiRestart}
|
||||
on:click={() => dispatch('reset-visibility')}
|
||||
/>
|
||||
|
|
190
web/src/lib/components/faces-page/unmerge-face-selector.svelte
Normal file
190
web/src/lib/components/faces-page/unmerge-face-selector.svelte
Normal file
|
@ -0,0 +1,190 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import FaceThumbnail from './face-thumbnail.svelte';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { api, AssetFaceUpdateItem, type PersonResponseDto } from '@api';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import { mdiPlus, mdiMerge } from '@mdi/js';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { notificationController, NotificationType } from '../shared-components/notification/notification';
|
||||
import PeopleList from './people-list.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
|
||||
export let assetIds: string[];
|
||||
export let personAssets: PersonResponseDto;
|
||||
|
||||
let people: PersonResponseDto[] = [];
|
||||
let selectedPerson: PersonResponseDto | null = null;
|
||||
let disableButtons = false;
|
||||
let showLoadingSpinnerCreate = false;
|
||||
let showLoadingSpinnerReassign = false;
|
||||
let hasSelection = false;
|
||||
let screenHeight: number;
|
||||
|
||||
$: unselectedPeople = selectedPerson
|
||||
? people.filter((person) => selectedPerson && person.id !== selectedPerson.id && personAssets.id !== person.id)
|
||||
: people;
|
||||
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
const selectedPeople: AssetFaceUpdateItem[] = [];
|
||||
|
||||
for (const assetId of assetIds) {
|
||||
selectedPeople.push({ assetId, personId: personAssets.id });
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.personApi.getAllPeople({ withHidden: false });
|
||||
people = data.people;
|
||||
});
|
||||
|
||||
const onClose = () => {
|
||||
dispatch('close');
|
||||
};
|
||||
|
||||
const handleSelectedPerson = (person: PersonResponseDto) => {
|
||||
if (selectedPerson && selectedPerson.id === person.id) {
|
||||
handleRemoveSelectedPerson();
|
||||
return;
|
||||
}
|
||||
selectedPerson = person;
|
||||
hasSelection = true;
|
||||
};
|
||||
|
||||
const handleRemoveSelectedPerson = () => {
|
||||
selectedPerson = null;
|
||||
hasSelection = false;
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
const timeout = setTimeout(() => (showLoadingSpinnerCreate = true), 100);
|
||||
|
||||
try {
|
||||
disableButtons = true;
|
||||
const { data } = await api.personApi.createPerson();
|
||||
await api.personApi.reassignFaces({
|
||||
id: data.id,
|
||||
assetFaceUpdateDto: { data: selectedPeople },
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
message: `Re-assigned ${assetIds.length} asset${assetIds.length > 1 ? 's' : ''} to a new person`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to reassign assets to a new person');
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
showLoadingSpinnerCreate = false;
|
||||
dispatch('confirm');
|
||||
};
|
||||
|
||||
const handleReassign = async () => {
|
||||
const timeout = setTimeout(() => (showLoadingSpinnerReassign = true), 100);
|
||||
try {
|
||||
disableButtons = true;
|
||||
if (selectedPerson) {
|
||||
await api.personApi.reassignFaces({
|
||||
id: selectedPerson.id,
|
||||
assetFaceUpdateDto: { data: selectedPeople },
|
||||
});
|
||||
notificationController.show({
|
||||
message: `Re-assigned ${assetIds.length} asset${assetIds.length > 1 ? 's' : ''} to ${
|
||||
selectedPerson.name || 'an existing person'
|
||||
}`,
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, `Unable to reassign assets to ${selectedPerson?.name || 'an existing person'}`);
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
showLoadingSpinnerReassign = false;
|
||||
dispatch('confirm');
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={screenHeight} />
|
||||
|
||||
<section
|
||||
transition:fly={{ y: 500, duration: 100, easing: quintOut }}
|
||||
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<ControlAppBar on:close-button-click={onClose}>
|
||||
<svelte:fragment slot="leading">
|
||||
<slot name="header" />
|
||||
<div />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
title={'Assign selected assets to a new person'}
|
||||
size={'sm'}
|
||||
disabled={disableButtons || hasSelection}
|
||||
on:click={() => {
|
||||
handleCreate();
|
||||
}}
|
||||
>
|
||||
{#if !showLoadingSpinnerCreate}
|
||||
<Icon path={mdiPlus} size={18} />
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<span class="ml-2"> Create new Person</span></Button
|
||||
>
|
||||
<Button
|
||||
size={'sm'}
|
||||
title={'Assign selected assets to an existing person'}
|
||||
disabled={disableButtons || !hasSelection}
|
||||
on:click={() => {
|
||||
handleReassign();
|
||||
}}
|
||||
>
|
||||
{#if !showLoadingSpinnerReassign}
|
||||
<div>
|
||||
<Icon path={mdiMerge} size={18} class="rotate-180" />
|
||||
</div>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
<span class="ml-2"> Reassign</span></Button
|
||||
>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
<slot name="merge" />
|
||||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<section id="merge-face-selector relative">
|
||||
{#if selectedPerson !== null}
|
||||
<div class="mb-10 h-[200px] place-content-center place-items-center">
|
||||
<p class="mb-4 text-center uppercase dark:text-white">Choose matching faces to re assign</p>
|
||||
|
||||
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
|
||||
<FaceThumbnail
|
||||
person={selectedPerson}
|
||||
border
|
||||
circle
|
||||
selectable
|
||||
thumbnailSize={180}
|
||||
on:click={handleRemoveSelectedPerson}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<PeopleList
|
||||
people={unselectedPeople}
|
||||
peopleCopy={unselectedPeople}
|
||||
unselectedPeople={selectedPerson ? [selectedPerson, personAssets] : [personAssets]}
|
||||
{screenHeight}
|
||||
on:select={({ detail }) => handleSelectedPerson(detail)}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
|
@ -58,6 +58,8 @@ interface TrashAsset {
|
|||
value: string;
|
||||
}
|
||||
|
||||
export const photoViewer = writable<HTMLImageElement | null>(null);
|
||||
|
||||
type PendingChange = AddAsset | DeleteAsset | TrashAsset;
|
||||
|
||||
export class AssetStore {
|
||||
|
|
12
web/src/lib/stores/people.store.ts
Normal file
12
web/src/lib/stores/people.store.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
export interface Faces {
|
||||
imageHeight: number;
|
||||
imageWidth: number;
|
||||
boundingBoxX1: number;
|
||||
boundingBoxX2: number;
|
||||
boundingBoxY1: number;
|
||||
boundingBoxY2: number;
|
||||
}
|
||||
|
||||
export const boundingBoxesArray = writable<Faces[]>([]);
|
71
web/src/lib/utils/people-utils.ts
Normal file
71
web/src/lib/utils/people-utils.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
import type { Faces } from '$lib/stores/people.store';
|
||||
import type { ZoomImageWheelState } from '@zoom-image/core';
|
||||
|
||||
const getContainedSize = (img: HTMLImageElement): { width: number; height: number } => {
|
||||
const ratio = img.naturalWidth / img.naturalHeight;
|
||||
let width = img.height * ratio;
|
||||
let height = img.height;
|
||||
if (width > img.width) {
|
||||
width = img.width;
|
||||
height = img.width / ratio;
|
||||
}
|
||||
return { width, height };
|
||||
};
|
||||
|
||||
export interface boundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const getBoundingBox = (
|
||||
faces: Faces[],
|
||||
zoom: ZoomImageWheelState,
|
||||
photoViewer: HTMLImageElement | null,
|
||||
): boundingBox[] => {
|
||||
const boxes: boundingBox[] = [];
|
||||
|
||||
if (photoViewer === null) {
|
||||
return boxes;
|
||||
}
|
||||
const clientHeight = photoViewer.clientHeight;
|
||||
const clientWidth = photoViewer.clientWidth;
|
||||
|
||||
const { width, height } = getContainedSize(photoViewer);
|
||||
|
||||
for (const face of faces) {
|
||||
/*
|
||||
*
|
||||
* Create the coordinates of the box based on the displayed image.
|
||||
* The coordinates must take into account margins due to the 'object-fit: contain;' css property of the photo-viewer.
|
||||
*
|
||||
*/
|
||||
const coordinates = {
|
||||
x1:
|
||||
(width / face.imageWidth) * zoom.currentZoom * face.boundingBoxX1 +
|
||||
((clientWidth - width) / 2) * zoom.currentZoom +
|
||||
zoom.currentPositionX,
|
||||
x2:
|
||||
(width / face.imageWidth) * zoom.currentZoom * face.boundingBoxX2 +
|
||||
((clientWidth - width) / 2) * zoom.currentZoom +
|
||||
zoom.currentPositionX,
|
||||
y1:
|
||||
(height / face.imageHeight) * zoom.currentZoom * face.boundingBoxY1 +
|
||||
((clientHeight - height) / 2) * zoom.currentZoom +
|
||||
zoom.currentPositionY,
|
||||
y2:
|
||||
(height / face.imageHeight) * zoom.currentZoom * face.boundingBoxY2 +
|
||||
((clientHeight - height) / 2) * zoom.currentZoom +
|
||||
zoom.currentPositionY,
|
||||
};
|
||||
|
||||
boxes.push({
|
||||
top: Math.round(coordinates.y1),
|
||||
left: Math.round(coordinates.x1),
|
||||
width: Math.round(coordinates.x2 - coordinates.x1),
|
||||
height: Math.round(coordinates.y2 - coordinates.y1),
|
||||
});
|
||||
}
|
||||
return boxes;
|
||||
};
|
|
@ -30,3 +30,7 @@ export const searchNameLocal = (
|
|||
})
|
||||
.slice(0, slice);
|
||||
};
|
||||
|
||||
export const getPersonNameWithHiddenValue = (name: string, isHidden: boolean) => {
|
||||
return `${name ? name + (isHidden ? ' ' : '') : ''}${isHidden ? '(hidden)' : ''}`;
|
||||
};
|
||||
|
|
|
@ -79,7 +79,7 @@
|
|||
// trigger reactivity
|
||||
people = people;
|
||||
|
||||
// Reset variables used on the "Show & hide faces" modal
|
||||
// Reset variables used on the "Show & hide people" modal
|
||||
showLoadingSpinner = false;
|
||||
selectHidden = false;
|
||||
toggleVisibility = false;
|
||||
|
@ -145,13 +145,13 @@
|
|||
`Unable to change the visibility for ${changed.length} ${changed.length <= 1 ? 'person' : 'people'}`,
|
||||
);
|
||||
}
|
||||
// Reset variables used on the "Show & hide faces" modal
|
||||
// Reset variables used on the "Show & hide people" modal
|
||||
showLoadingSpinner = false;
|
||||
selectHidden = false;
|
||||
toggleVisibility = false;
|
||||
};
|
||||
|
||||
const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
||||
const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
||||
const [personToMerge, personToBeMergedIn] = response;
|
||||
showMergeModal = false;
|
||||
|
||||
|
@ -167,7 +167,7 @@
|
|||
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Merge faces succesfully',
|
||||
message: 'Merge people succesfully',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -213,7 +213,7 @@
|
|||
edittingPerson = detail;
|
||||
};
|
||||
|
||||
const handleHideFace = async (detail: PersonResponseDto) => {
|
||||
const handleHidePerson = async (detail: PersonResponseDto) => {
|
||||
try {
|
||||
const { data: updatedPerson } = await api.personApi.updatePerson({
|
||||
id: detail.id,
|
||||
|
@ -244,7 +244,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleMergeFaces = (detail: PersonResponseDto) => {
|
||||
const handleMergePeople = (detail: PersonResponseDto) => {
|
||||
goto(`${AppRoute.PEOPLE}/${detail.id}?action=merge&previousRoute=${AppRoute.PEOPLE}`);
|
||||
};
|
||||
|
||||
|
@ -352,7 +352,7 @@
|
|||
{potentialMergePeople}
|
||||
on:close={() => (showMergeModal = false)}
|
||||
on:reject={() => changeName()}
|
||||
on:confirm={(event) => handleMergeSameFace(event.detail)}
|
||||
on:confirm={(event) => handleMergeSamePerson(event.detail)}
|
||||
/>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
@ -363,7 +363,7 @@
|
|||
<IconButton on:click={() => (selectHidden = !selectHidden)}>
|
||||
<div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm">
|
||||
<Icon path={mdiEyeOutline} size="18" />
|
||||
<p class="ml-2">Show & hide faces</p>
|
||||
<p class="ml-2">Show & hide people</p>
|
||||
</div>
|
||||
</IconButton>
|
||||
{/if}
|
||||
|
@ -379,8 +379,8 @@
|
|||
preload={idx < 20}
|
||||
on:change-name={() => handleChangeName(person)}
|
||||
on:set-birth-date={() => handleSetBirthDate(person)}
|
||||
on:merge-faces={() => handleMergeFaces(person)}
|
||||
on:hide-face={() => handleHideFace(person)}
|
||||
on:merge-people={() => handleMergePeople(person)}
|
||||
on:hide-person={() => handleHidePerson(person)}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||
import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
|
||||
import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte';
|
||||
import UnMergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte';
|
||||
import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte';
|
||||
import SetBirthDateModal from '$lib/components/faces-page/set-birth-date-modal.svelte';
|
||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
|
||||
|
@ -46,10 +47,11 @@
|
|||
|
||||
enum ViewMode {
|
||||
VIEW_ASSETS = 'view-assets',
|
||||
SELECT_FACE = 'select-face',
|
||||
MERGE_FACES = 'merge-faces',
|
||||
SELECT_PERSON = 'select-person',
|
||||
MERGE_PEOPLE = 'merge-people',
|
||||
SUGGEST_MERGE = 'suggest-merge',
|
||||
BIRTH_DATE = 'birth-date',
|
||||
UNASSIGN_ASSETS = 'unassign-faces',
|
||||
}
|
||||
|
||||
let assetStore = new AssetStore({
|
||||
|
@ -124,7 +126,7 @@
|
|||
previousRoute = getPreviousRoute;
|
||||
}
|
||||
if (action == 'merge') {
|
||||
viewMode = ViewMode.MERGE_FACES;
|
||||
viewMode = ViewMode.MERGE_PEOPLE;
|
||||
}
|
||||
});
|
||||
const handleEscape = () => {
|
||||
|
@ -155,7 +157,17 @@
|
|||
}
|
||||
});
|
||||
|
||||
const toggleHideFace = async () => {
|
||||
const handleUnmerge = () => {
|
||||
$assetStore.removeAssets(Array.from($selectedAssets).map((a) => a.id));
|
||||
assetInteractionStore.clearMultiselect();
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
};
|
||||
|
||||
const handleReassignAssets = () => {
|
||||
viewMode = ViewMode.UNASSIGN_ASSETS;
|
||||
};
|
||||
|
||||
const toggleHidePerson = async () => {
|
||||
try {
|
||||
await api.personApi.updatePerson({
|
||||
id: data.person.id,
|
||||
|
@ -179,7 +191,7 @@
|
|||
};
|
||||
|
||||
const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => {
|
||||
if (viewMode !== ViewMode.SELECT_FACE) {
|
||||
if (viewMode !== ViewMode.SELECT_PERSON) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -202,7 +214,7 @@
|
|||
}
|
||||
};
|
||||
|
||||
const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
||||
const handleMergeSamePerson = async (response: [PersonResponseDto, PersonResponseDto]) => {
|
||||
const [personToMerge, personToBeMergedIn] = response;
|
||||
viewMode = ViewMode.VIEW_ASSETS;
|
||||
isEditingName = false;
|
||||
|
@ -212,7 +224,7 @@
|
|||
mergePersonDto: { ids: [personToMerge.id] },
|
||||
});
|
||||
notificationController.show({
|
||||
message: 'Merge faces succesfully',
|
||||
message: 'Merge people succesfully',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id);
|
||||
|
@ -333,6 +345,15 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
{#if viewMode === ViewMode.UNASSIGN_ASSETS}
|
||||
<UnMergeFaceSelector
|
||||
assetIds={Array.from($selectedAssets).map((a) => a.id)}
|
||||
personAssets={data.person}
|
||||
on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
|
||||
on:confirm={handleUnmerge}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.SUGGEST_MERGE}
|
||||
<MergeSuggestionModal
|
||||
{personMerge1}
|
||||
|
@ -340,7 +361,7 @@
|
|||
{potentialMergePeople}
|
||||
on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
|
||||
on:reject={() => changeName()}
|
||||
on:confirm={(event) => handleMergeSameFace(event.detail)}
|
||||
on:confirm={(event) => handleMergeSamePerson(event.detail)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -352,7 +373,7 @@
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.MERGE_FACES}
|
||||
{#if viewMode === ViewMode.MERGE_PEOPLE}
|
||||
<MergeFaceSelector person={data.person} on:go-back={handleGoBack} on:merge={handleMerge} />
|
||||
{/if}
|
||||
|
||||
|
@ -370,6 +391,7 @@
|
|||
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
|
||||
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
|
||||
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} />
|
||||
<MenuOption text="Fix incorrect match" on:click={handleReassignAssets} />
|
||||
<ChangeDate menuItem />
|
||||
<ChangeLocation menuItem />
|
||||
</AssetSelectContextMenu>
|
||||
|
@ -379,16 +401,19 @@
|
|||
<ControlAppBar showBackButton backIcon={mdiArrowLeft} on:close-button-click={() => goto(previousRoute)}>
|
||||
<svelte:fragment slot="trailing">
|
||||
<AssetSelectContextMenu icon={mdiDotsVertical} title="Menu">
|
||||
<MenuOption text="Change feature photo" on:click={() => (viewMode = ViewMode.SELECT_FACE)} />
|
||||
<MenuOption text="Change feature photo" on:click={() => (viewMode = ViewMode.SELECT_PERSON)} />
|
||||
<MenuOption text="Set date of birth" on:click={() => (viewMode = ViewMode.BIRTH_DATE)} />
|
||||
<MenuOption text="Merge face" on:click={() => (viewMode = ViewMode.MERGE_FACES)} />
|
||||
<MenuOption text={data.person.isHidden ? 'Unhide face' : 'Hide face'} on:click={() => toggleHideFace()} />
|
||||
<MenuOption text="Merge person" on:click={() => (viewMode = ViewMode.MERGE_PEOPLE)} />
|
||||
<MenuOption
|
||||
text={data.person.isHidden ? 'Unhide person' : 'Hide person'}
|
||||
on:click={() => toggleHidePerson()}
|
||||
/>
|
||||
</AssetSelectContextMenu>
|
||||
</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.SELECT_FACE}
|
||||
{#if viewMode === ViewMode.SELECT_PERSON}
|
||||
<ControlAppBar on:close-button-click={() => (viewMode = ViewMode.VIEW_ASSETS)}>
|
||||
<svelte:fragment slot="leading">Select feature photo</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
|
@ -401,13 +426,13 @@
|
|||
<AssetGrid
|
||||
{assetStore}
|
||||
{assetInteractionStore}
|
||||
isSelectionMode={viewMode === ViewMode.SELECT_FACE}
|
||||
singleSelect={viewMode === ViewMode.SELECT_FACE}
|
||||
isSelectionMode={viewMode === ViewMode.SELECT_PERSON}
|
||||
singleSelect={viewMode === ViewMode.SELECT_PERSON}
|
||||
on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)}
|
||||
on:escape={handleEscape}
|
||||
>
|
||||
{#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE}
|
||||
<!-- Face information block -->
|
||||
<!-- Person information block -->
|
||||
<div
|
||||
role="button"
|
||||
class="relative w-fit p-4 sm:px-6"
|
||||
|
|
Loading…
Reference in a new issue