Compare commits

...

31 commits

Author SHA1 Message Date
martabal
c9feb474be
use icons 2023-10-26 00:20:20 +02:00
martabal
00ee2aca62
fix merge 2023-10-25 23:54:29 +02:00
martabal
fc0d358d4e
merge main 2023-10-25 23:41:01 +02:00
martabal
c5ca5a528e
feat: unassign faces 2023-10-25 23:36:51 +02:00
martabal
9247eb4c1a
add available person 2023-10-25 15:44:11 +02:00
martabal
c8d173697f
feat: add number of unassigned faces 2023-10-24 23:34:55 +02:00
martabal
f42cf33605
regenerate api 2023-10-24 22:21:06 +02:00
martabal
6027a2fda6
merge main 2023-10-24 22:07:17 +02:00
martabal
24f9968606
regenerate api 2023-10-21 15:03:36 +02:00
martabal
7a557c08db
merge main 2023-10-21 15:01:25 +02:00
martabal
89ad9d58c5
fix: migration 2023-10-21 15:00:06 +02:00
martabal
1a1b3dd996
Merge branch 'main' into feat/unmerge-people 2023-10-13 17:07:32 +02:00
martabal
aa6c90c66c
simplify 2023-10-12 10:56:48 +02:00
martabal
7a0047f631
feat: add remote search 2023-10-11 23:47:32 +02:00
martabal
6ef7844613
regenerate api 2023-10-11 20:51:42 +02:00
martabal
00d7052b84
merge main 2023-10-11 20:50:36 +02:00
martabal
47b3a8bad9
Merge branch 'main' into feat/unmerge-people 2023-10-09 10:13:00 +02:00
martabal
d7489cf808
use fonction 2023-10-08 14:33:56 +02:00
martabal
348a827281
feat: change feature photo if it's unmerged 2023-10-08 12:49:45 +02:00
martabal
2babfe4ad4
fix: change name when switching face 2023-10-07 17:16:47 +02:00
martabal
8699d8cfbf
regenerate api 2023-10-07 17:02:44 +02:00
martabal
26b2f66f0f
merge main 2023-10-07 16:53:37 +02:00
martabal
8c37a7cb60
feat: add modularity 2023-10-07 16:51:53 +02:00
martabal
02f1b828d3
feat: add notifications on person page 2023-10-04 20:47:40 +02:00
martabal
977a17f9c0
fix: people page 2023-10-04 17:21:53 +02:00
martabal
4dcc1131fa
regenerate api 2023-10-04 14:06:18 +02:00
martabal
698e45a5cb
fix merge 2023-10-04 13:52:51 +02:00
martabal
f6170e5f7f
merge main 2023-10-04 13:40:00 +02:00
martabal
79f7a95e66
feat: rework ui 2023-10-04 13:37:12 +02:00
martabal
a1f54de082
improve webui 2023-09-21 10:56:37 +02:00
martabal
c30a5db8f4
feat: unmerge people 2023-09-21 00:45:16 +02:00
45 changed files with 3961 additions and 310 deletions

View file

@ -502,6 +502,81 @@ export const AssetBulkUploadCheckResultReasonEnum = {
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
/**
*
* @export
* @interface AssetFaceBoxDto
*/
export interface AssetFaceBoxDto {
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxX1': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxX2': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxY1': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxY2': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'imageHeight': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'imageWidth': number;
}
/**
*
* @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
@ -744,10 +819,10 @@ export interface AssetResponseDto {
'ownerId': string;
/**
*
* @type {Array<PersonResponseDto>}
* @type {Array<PeopleAssetResponseDto>}
* @memberof AssetResponseDto
*/
'people'?: Array<PersonResponseDto>;
'people'?: Array<PeopleAssetResponseDto>;
/**
*
* @type {boolean}
@ -796,6 +871,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto
*/
'type': AssetTypeEnum;
/**
*
* @type {Array<UnassignedFacesResponseDto>}
* @memberof AssetResponseDto
*/
'unassignedPeople'?: Array<UnassignedFacesResponseDto>;
/**
*
* @type {string}
@ -2315,6 +2396,25 @@ export const PathType = {
export type PathType = typeof PathType[keyof typeof PathType];
/**
*
* @export
* @interface PeopleAssetResponseDto
*/
export interface PeopleAssetResponseDto {
/**
*
* @type {string}
* @memberof PeopleAssetResponseDto
*/
'assetFaceId': string;
/**
*
* @type {PersonResponseDto}
* @memberof PeopleAssetResponseDto
*/
'person': PersonResponseDto;
}
/**
*
* @export
@ -3949,6 +4049,31 @@ export const TranscodePolicy = {
export type TranscodePolicy = typeof TranscodePolicy[keyof typeof TranscodePolicy];
/**
*
* @export
* @interface UnassignedFacesResponseDto
*/
export interface UnassignedFacesResponseDto {
/**
*
* @type {string}
* @memberof UnassignedFacesResponseDto
*/
'assetFaceId': string;
/**
*
* @type {string}
* @memberof UnassignedFacesResponseDto
*/
'assetId': string;
/**
*
* @type {AssetFaceBoxDto}
* @memberof UnassignedFacesResponseDto
*/
'boudinxBox': AssetFaceBoxDto;
}
/**
*
* @export
@ -11766,6 +11891,50 @@ export class PartnerApi extends BaseAPI {
*/
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPerson: async (assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
assertParamExists('createPerson', 'assetFaceUpdateDto', assetFaceUpdateDto)
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)
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 {boolean} [withHidden]
@ -11800,6 +11969,52 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
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 {string} assetId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetFace: async (id: string, assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getAssetFace', 'id', id)
// verify required parameter 'assetId' is not null or undefined
assertParamExists('getAssetFace', 'assetId', assetId)
const localVarPath = `/person/{id}/{assetId}/faceasset`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"assetId"}}`, encodeURIComponent(String(assetId)));
// 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)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -12025,6 +12240,98 @@ 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 {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unassignFaces: async (assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
assertParamExists('unassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
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: 'DELETE', ...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
@ -12127,6 +12434,16 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
return {
/**
*
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createPerson(assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(assetFaceUpdateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {boolean} [withHidden]
@ -12137,6 +12454,17 @@ export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(withHidden, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {string} assetId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetFace(id: string, assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFaceBoxDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetFace(id, assetId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
@ -12188,6 +12516,27 @@ 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 {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async unassignFaces(assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.unassignFaces(assetFaceUpdateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -12219,6 +12568,15 @@ export const PersonApiFp = function(configuration?: Configuration) {
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = PersonApiFp(configuration)
return {
/**
*
* @param {PersonApiCreatePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPerson(requestParameters: PersonApiCreatePersonRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
return localVarFp.createPerson(requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -12228,6 +12586,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: AxiosRequestConfig): AxiosPromise<PeopleResponseDto> {
return localVarFp.getAllPeople(requestParameters.withHidden, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetAssetFaceRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetFace(requestParameters: PersonApiGetAssetFaceRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFaceBoxDto> {
return localVarFp.getAssetFace(requestParameters.id, requestParameters.assetId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetPersonRequest} requestParameters Request parameters.
@ -12273,6 +12640,24 @@ 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 {PersonApiUnassignFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
return localVarFp.unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
@ -12294,6 +12679,20 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
};
};
/**
* Request parameters for createPerson operation in PersonApi.
* @export
* @interface PersonApiCreatePersonRequest
*/
export interface PersonApiCreatePersonRequest {
/**
*
* @type {AssetFaceUpdateDto}
* @memberof PersonApiCreatePerson
*/
readonly assetFaceUpdateDto: AssetFaceUpdateDto
}
/**
* Request parameters for getAllPeople operation in PersonApi.
* @export
@ -12308,6 +12707,27 @@ export interface PersonApiGetAllPeopleRequest {
readonly withHidden?: boolean
}
/**
* Request parameters for getAssetFace operation in PersonApi.
* @export
* @interface PersonApiGetAssetFaceRequest
*/
export interface PersonApiGetAssetFaceRequest {
/**
*
* @type {string}
* @memberof PersonApiGetAssetFace
*/
readonly id: string
/**
*
* @type {string}
* @memberof PersonApiGetAssetFace
*/
readonly assetId: string
}
/**
* Request parameters for getPerson operation in PersonApi.
* @export
@ -12385,6 +12805,41 @@ 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 unassignFaces operation in PersonApi.
* @export
* @interface PersonApiUnassignFacesRequest
*/
export interface PersonApiUnassignFacesRequest {
/**
*
* @type {AssetFaceUpdateDto}
* @memberof PersonApiUnassignFaces
*/
readonly assetFaceUpdateDto: AssetFaceUpdateDto
}
/**
* Request parameters for updatePeople operation in PersonApi.
* @export
@ -12427,6 +12882,17 @@ export interface PersonApiUpdatePersonRequest {
* @extends {BaseAPI}
*/
export class PersonApi extends BaseAPI {
/**
*
* @param {PersonApiCreatePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public createPerson(requestParameters: PersonApiCreatePersonRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).createPerson(requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -12438,6 +12904,17 @@ export class PersonApi extends BaseAPI {
return PersonApiFp(this.configuration).getAllPeople(requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetAssetFaceRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public getAssetFace(requestParameters: PersonApiGetAssetFaceRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).getAssetFace(requestParameters.id, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetPersonRequest} requestParameters Request parameters.
@ -12493,6 +12970,28 @@ 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 {PersonApiUnassignFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.

View file

@ -21,6 +21,9 @@ doc/AssetBulkUploadCheckDto.md
doc/AssetBulkUploadCheckItem.md
doc/AssetBulkUploadCheckResponseDto.md
doc/AssetBulkUploadCheckResult.md
doc/AssetFaceBoxDto.md
doc/AssetFaceUpdateDto.md
doc/AssetFaceUpdateItem.md
doc/AssetFileUploadResponseDto.md
doc/AssetIdsDto.md
doc/AssetIdsResponseDto.md
@ -89,6 +92,7 @@ doc/OAuthConfigResponseDto.md
doc/PartnerApi.md
doc/PathEntityType.md
doc/PathType.md
doc/PeopleAssetResponseDto.md
doc/PeopleResponseDto.md
doc/PeopleUpdateDto.md
doc/PeopleUpdateItem.md
@ -147,6 +151,7 @@ doc/TimeBucketSize.md
doc/ToneMapping.md
doc/TranscodeHWAccel.md
doc/TranscodePolicy.md
doc/UnassignedFacesResponseDto.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateLibraryDto.md
@ -200,6 +205,9 @@ 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_box_dto.dart
lib/model/asset_face_update_dto.dart
lib/model/asset_face_update_item.dart
lib/model/asset_file_upload_response_dto.dart
lib/model/asset_ids_dto.dart
lib/model/asset_ids_response_dto.dart
@ -262,6 +270,7 @@ lib/model/o_auth_config_dto.dart
lib/model/o_auth_config_response_dto.dart
lib/model/path_entity_type.dart
lib/model/path_type.dart
lib/model/people_asset_response_dto.dart
lib/model/people_response_dto.dart
lib/model/people_update_dto.dart
lib/model/people_update_item.dart
@ -314,6 +323,7 @@ lib/model/time_bucket_size.dart
lib/model/tone_mapping.dart
lib/model/transcode_hw_accel.dart
lib/model/transcode_policy.dart
lib/model/unassigned_faces_response_dto.dart
lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart
lib/model/update_library_dto.dart
@ -344,6 +354,9 @@ 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_box_dto_test.dart
test/asset_face_update_dto_test.dart
test/asset_face_update_item_test.dart
test/asset_file_upload_response_dto_test.dart
test/asset_ids_dto_test.dart
test/asset_ids_response_dto_test.dart
@ -412,6 +425,7 @@ test/o_auth_config_response_dto_test.dart
test/partner_api_test.dart
test/path_entity_type_test.dart
test/path_type_test.dart
test/people_asset_response_dto_test.dart
test/people_response_dto_test.dart
test/people_update_dto_test.dart
test/people_update_item_test.dart
@ -470,6 +484,7 @@ test/time_bucket_size_test.dart
test/tone_mapping_test.dart
test/transcode_hw_accel_test.dart
test/transcode_policy_test.dart
test/unassigned_faces_response_dto_test.dart
test/update_album_dto_test.dart
test/update_asset_dto_test.dart
test/update_library_dto_test.dart

View file

@ -148,12 +148,16 @@ Class | Method | HTTP request | Description
*PartnerApi* | [**createPartner**](doc//PartnerApi.md#createpartner) | **POST** /partner/{id} |
*PartnerApi* | [**getPartners**](doc//PartnerApi.md#getpartners) | **GET** /partner |
*PartnerApi* | [**removePartner**](doc//PartnerApi.md#removepartner) | **DELETE** /partner/{id} |
*PersonApi* | [**createPerson**](doc//PersonApi.md#createperson) | **POST** /person |
*PersonApi* | [**getAllPeople**](doc//PersonApi.md#getallpeople) | **GET** /person |
*PersonApi* | [**getAssetFace**](doc//PersonApi.md#getassetface) | **GET** /person/{id}/{assetId}/faceasset |
*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* | [**unassignFaces**](doc//PersonApi.md#unassignfaces) | **DELETE** /person |
*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 |
@ -215,6 +219,9 @@ Class | Method | HTTP request | Description
- [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md)
- [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md)
- [AssetBulkUploadCheckResult](doc//AssetBulkUploadCheckResult.md)
- [AssetFaceBoxDto](doc//AssetFaceBoxDto.md)
- [AssetFaceUpdateDto](doc//AssetFaceUpdateDto.md)
- [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md)
- [AssetFileUploadResponseDto](doc//AssetFileUploadResponseDto.md)
- [AssetIdsDto](doc//AssetIdsDto.md)
- [AssetIdsResponseDto](doc//AssetIdsResponseDto.md)
@ -277,6 +284,7 @@ Class | Method | HTTP request | Description
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
- [PathEntityType](doc//PathEntityType.md)
- [PathType](doc//PathType.md)
- [PeopleAssetResponseDto](doc//PeopleAssetResponseDto.md)
- [PeopleResponseDto](doc//PeopleResponseDto.md)
- [PeopleUpdateDto](doc//PeopleUpdateDto.md)
- [PeopleUpdateItem](doc//PeopleUpdateItem.md)
@ -329,6 +337,7 @@ Class | Method | HTTP request | Description
- [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md)
- [UnassignedFacesResponseDto](doc//UnassignedFacesResponseDto.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)

20
mobile/openapi/doc/AssetFaceBoxDto.md generated Normal file
View file

@ -0,0 +1,20 @@
# openapi.model.AssetFaceBoxDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**boundingBoxX1** | **int** | |
**boundingBoxX2** | **int** | |
**boundingBoxY1** | **int** | |
**boundingBoxY2** | **int** | |
**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)

15
mobile/openapi/doc/AssetFaceUpdateDto.md generated Normal file
View 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)

View 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)

View file

@ -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<PeopleAssetResponseDto>**](PeopleAssetResponseDto.md) | | [optional] [default to const []]
**resized** | **bool** | |
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) | | [optional]
**stack** | [**List<AssetResponseDto>**](AssetResponseDto.md) | | [optional] [default to const []]
@ -39,6 +39,7 @@ Name | Type | Description | Notes
**tags** | [**List<TagResponseDto>**](TagResponseDto.md) | | [optional] [default to const []]
**thumbhash** | **String** | |
**type** | [**AssetTypeEnum**](AssetTypeEnum.md) | |
**unassignedPeople** | [**List<UnassignedFacesResponseDto>**](UnassignedFacesResponseDto.md) | | [optional] [default to const []]
**updatedAt** | [**DateTime**](DateTime.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)

View file

@ -0,0 +1,16 @@
# openapi.model.PeopleAssetResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetFaceId** | **String** | |
**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)

View file

@ -9,16 +9,75 @@ All URIs are relative to */api*
Method | HTTP request | Description
------------- | ------------- | -------------
[**createPerson**](PersonApi.md#createperson) | **POST** /person |
[**getAllPeople**](PersonApi.md#getallpeople) | **GET** /person |
[**getAssetFace**](PersonApi.md#getassetface) | **GET** /person/{id}/{assetId}/faceasset |
[**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 |
[**unassignFaces**](PersonApi.md#unassignfaces) | **DELETE** /person |
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} |
# **createPerson**
> PersonResponseDto createPerson(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 assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto |
try {
final result = api_instance.createPerson(assetFaceUpdateDto);
print(result);
} catch (e) {
print('Exception when calling PersonApi->createPerson: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.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)
# **getAllPeople**
> PeopleResponseDto getAllPeople(withHidden)
@ -74,6 +133,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)
# **getAssetFace**
> AssetFaceBoxDto getAssetFace(id, assetId)
### 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 assetId = assetId_example; // String |
try {
final result = api_instance.getAssetFace(id, assetId);
print(result);
} catch (e) {
print('Exception when calling PersonApi->getAssetFace: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**id** | **String**| |
**assetId** | **String**| |
### Return type
[**AssetFaceBoxDto**](AssetFaceBoxDto.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)
# **getPerson**
> PersonResponseDto getPerson(id)
@ -351,6 +467,118 @@ 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)
# **unassignFaces**
> List<BulkIdResponseDto> unassignFaces(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 assetFaceUpdateDto = AssetFaceUpdateDto(); // AssetFaceUpdateDto |
try {
final result = api_instance.unassignFaces(assetFaceUpdateDto);
print(result);
} catch (e) {
print('Exception when calling PersonApi->unassignFaces: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**assetFaceUpdateDto** | [**AssetFaceUpdateDto**](AssetFaceUpdateDto.md)| |
### Return type
[**List<BulkIdResponseDto>**](BulkIdResponseDto.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)

View file

@ -0,0 +1,17 @@
# openapi.model.UnassignedFacesResponseDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**assetFaceId** | **String** | |
**assetId** | **String** | |
**boudinxBox** | [**AssetFaceBoxDto**](AssetFaceBoxDto.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)

View file

@ -60,6 +60,9 @@ 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_box_dto.dart';
part 'model/asset_face_update_dto.dart';
part 'model/asset_face_update_item.dart';
part 'model/asset_file_upload_response_dto.dart';
part 'model/asset_ids_dto.dart';
part 'model/asset_ids_response_dto.dart';
@ -122,6 +125,7 @@ part 'model/o_auth_config_dto.dart';
part 'model/o_auth_config_response_dto.dart';
part 'model/path_entity_type.dart';
part 'model/path_type.dart';
part 'model/people_asset_response_dto.dart';
part 'model/people_response_dto.dart';
part 'model/people_update_dto.dart';
part 'model/people_update_item.dart';
@ -174,6 +178,7 @@ part 'model/time_bucket_size.dart';
part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart';
part 'model/unassigned_faces_response_dto.dart';
part 'model/update_album_dto.dart';
part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart';

View file

@ -16,6 +16,53 @@ class PersonApi {
final ApiClient apiClient;
/// Performs an HTTP 'POST /person' operation and returns the [Response].
/// Parameters:
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<Response> createPersonWithHttpInfo(AssetFaceUpdateDto assetFaceUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/person';
// 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,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<PersonResponseDto?> createPerson(AssetFaceUpdateDto assetFaceUpdateDto,) async {
final response = await createPersonWithHttpInfo(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) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PersonResponseDto',) as PersonResponseDto;
}
return null;
}
/// Performs an HTTP 'GET /person' operation and returns the [Response].
/// Parameters:
///
@ -67,6 +114,59 @@ class PersonApi {
return null;
}
/// Performs an HTTP 'GET /person/{id}/{assetId}/faceasset' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
/// * [String] assetId (required):
Future<Response> getAssetFaceWithHttpInfo(String id, String assetId,) async {
// ignore: prefer_const_declarations
final path = r'/person/{id}/{assetId}/faceasset'
.replaceAll('{id}', id)
.replaceAll('{assetId}', assetId);
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] id (required):
///
/// * [String] assetId (required):
Future<AssetFaceBoxDto?> getAssetFace(String id, String assetId,) async {
final response = await getAssetFaceWithHttpInfo(id, assetId,);
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), 'AssetFaceBoxDto',) as AssetFaceBoxDto;
}
return null;
}
/// Performs an HTTP 'GET /person/{id}' operation and returns the [Response].
/// Parameters:
///
@ -317,6 +417,111 @@ 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 'DELETE /person' operation and returns the [Response].
/// Parameters:
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<Response> unassignFacesWithHttpInfo(AssetFaceUpdateDto assetFaceUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/person';
// 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,
'DELETE',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [AssetFaceUpdateDto] assetFaceUpdateDto (required):
Future<List<BulkIdResponseDto>?> unassignFaces(AssetFaceUpdateDto assetFaceUpdateDto,) async {
final response = await unassignFacesWithHttpInfo(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<BulkIdResponseDto>') as List)
.cast<BulkIdResponseDto>()
.toList();
}
return null;
}
/// Performs an HTTP 'PUT /person' operation and returns the [Response].
/// Parameters:
///

View file

@ -211,6 +211,12 @@ class ApiClient {
return AssetBulkUploadCheckResponseDto.fromJson(value);
case 'AssetBulkUploadCheckResult':
return AssetBulkUploadCheckResult.fromJson(value);
case 'AssetFaceBoxDto':
return AssetFaceBoxDto.fromJson(value);
case 'AssetFaceUpdateDto':
return AssetFaceUpdateDto.fromJson(value);
case 'AssetFaceUpdateItem':
return AssetFaceUpdateItem.fromJson(value);
case 'AssetFileUploadResponseDto':
return AssetFileUploadResponseDto.fromJson(value);
case 'AssetIdsDto':
@ -335,6 +341,8 @@ class ApiClient {
return PathEntityTypeTypeTransformer().decode(value);
case 'PathType':
return PathTypeTypeTransformer().decode(value);
case 'PeopleAssetResponseDto':
return PeopleAssetResponseDto.fromJson(value);
case 'PeopleResponseDto':
return PeopleResponseDto.fromJson(value);
case 'PeopleUpdateDto':
@ -439,6 +447,8 @@ class ApiClient {
return TranscodeHWAccelTypeTransformer().decode(value);
case 'TranscodePolicy':
return TranscodePolicyTypeTransformer().decode(value);
case 'UnassignedFacesResponseDto':
return UnassignedFacesResponseDto.fromJson(value);
case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value);
case 'UpdateAssetDto':

View file

@ -0,0 +1,138 @@
//
// 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 AssetFaceBoxDto {
/// Returns a new [AssetFaceBoxDto] instance.
AssetFaceBoxDto({
required this.boundingBoxX1,
required this.boundingBoxX2,
required this.boundingBoxY1,
required this.boundingBoxY2,
required this.imageHeight,
required this.imageWidth,
});
int boundingBoxX1;
int boundingBoxX2;
int boundingBoxY1;
int boundingBoxY2;
int imageHeight;
int imageWidth;
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFaceBoxDto &&
other.boundingBoxX1 == boundingBoxX1 &&
other.boundingBoxX2 == boundingBoxX2 &&
other.boundingBoxY1 == boundingBoxY1 &&
other.boundingBoxY2 == boundingBoxY2 &&
other.imageHeight == imageHeight &&
other.imageWidth == imageWidth;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(boundingBoxX1.hashCode) +
(boundingBoxX2.hashCode) +
(boundingBoxY1.hashCode) +
(boundingBoxY2.hashCode) +
(imageHeight.hashCode) +
(imageWidth.hashCode);
@override
String toString() => 'AssetFaceBoxDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, 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'imageHeight'] = this.imageHeight;
json[r'imageWidth'] = this.imageWidth;
return json;
}
/// Returns a new [AssetFaceBoxDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static AssetFaceBoxDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return AssetFaceBoxDto(
boundingBoxX1: mapValueOfType<int>(json, r'boundingBoxX1')!,
boundingBoxX2: mapValueOfType<int>(json, r'boundingBoxX2')!,
boundingBoxY1: mapValueOfType<int>(json, r'boundingBoxY1')!,
boundingBoxY2: mapValueOfType<int>(json, r'boundingBoxY2')!,
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
);
}
return null;
}
static List<AssetFaceBoxDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <AssetFaceBoxDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = AssetFaceBoxDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, AssetFaceBoxDto> mapFromJson(dynamic json) {
final map = <String, AssetFaceBoxDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = AssetFaceBoxDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of AssetFaceBoxDto-objects as value to a dart map
static Map<String, List<AssetFaceBoxDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<AssetFaceBoxDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = AssetFaceBoxDto.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',
'imageHeight',
'imageWidth',
};
}

View 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',
};
}

View 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',
};
}

View file

@ -44,6 +44,7 @@ class AssetResponseDto {
this.tags = const [],
required this.thumbhash,
required this.type,
this.unassignedPeople = const [],
required this.updatedAt,
});
@ -104,7 +105,7 @@ class AssetResponseDto {
String ownerId;
List<PersonResponseDto> people;
List<PeopleAssetResponseDto> people;
bool resized;
@ -128,6 +129,8 @@ class AssetResponseDto {
AssetTypeEnum type;
List<UnassignedFacesResponseDto> unassignedPeople;
DateTime updatedAt;
@override
@ -163,6 +166,7 @@ class AssetResponseDto {
other.tags == tags &&
other.thumbhash == thumbhash &&
other.type == type &&
other.unassignedPeople == unassignedPeople &&
other.updatedAt == updatedAt;
@override
@ -199,10 +203,11 @@ class AssetResponseDto {
(tags.hashCode) +
(thumbhash == null ? 0 : thumbhash!.hashCode) +
(type.hashCode) +
(unassignedPeople.hashCode) +
(updatedAt.hashCode);
@override
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, updatedAt=$updatedAt]';
String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isExternal=$isExternal, isFavorite=$isFavorite, isOffline=$isOffline, isReadOnly=$isReadOnly, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedPeople=$unassignedPeople, updatedAt=$updatedAt]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -261,6 +266,7 @@ class AssetResponseDto {
// json[r'thumbhash'] = null;
}
json[r'type'] = this.type;
json[r'unassignedPeople'] = this.unassignedPeople;
json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
return json;
}
@ -295,7 +301,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: PeopleAssetResponseDto.listFromJson(json[r'people']),
resized: mapValueOfType<bool>(json, r'resized')!,
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetResponseDto.listFromJson(json[r'stack']),
@ -304,6 +310,7 @@ class AssetResponseDto {
tags: TagResponseDto.listFromJson(json[r'tags']),
thumbhash: mapValueOfType<String>(json, r'thumbhash'),
type: AssetTypeEnum.fromJson(json[r'type'])!,
unassignedPeople: UnassignedFacesResponseDto.listFromJson(json[r'unassignedPeople']),
updatedAt: mapDateTime(json, r'updatedAt', '')!,
);
}

View 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 PeopleAssetResponseDto {
/// Returns a new [PeopleAssetResponseDto] instance.
PeopleAssetResponseDto({
required this.assetFaceId,
required this.person,
});
String assetFaceId;
PersonResponseDto person;
@override
bool operator ==(Object other) => identical(this, other) || other is PeopleAssetResponseDto &&
other.assetFaceId == assetFaceId &&
other.person == person;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetFaceId.hashCode) +
(person.hashCode);
@override
String toString() => 'PeopleAssetResponseDto[assetFaceId=$assetFaceId, person=$person]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assetFaceId'] = this.assetFaceId;
json[r'person'] = this.person;
return json;
}
/// Returns a new [PeopleAssetResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static PeopleAssetResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return PeopleAssetResponseDto(
assetFaceId: mapValueOfType<String>(json, r'assetFaceId')!,
person: PersonResponseDto.fromJson(json[r'person'])!,
);
}
return null;
}
static List<PeopleAssetResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PeopleAssetResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PeopleAssetResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, PeopleAssetResponseDto> mapFromJson(dynamic json) {
final map = <String, PeopleAssetResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = PeopleAssetResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of PeopleAssetResponseDto-objects as value to a dart map
static Map<String, List<PeopleAssetResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<PeopleAssetResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = PeopleAssetResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetFaceId',
'person',
};
}

View file

@ -0,0 +1,114 @@
//
// 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 UnassignedFacesResponseDto {
/// Returns a new [UnassignedFacesResponseDto] instance.
UnassignedFacesResponseDto({
required this.assetFaceId,
required this.assetId,
required this.boudinxBox,
});
String assetFaceId;
String assetId;
AssetFaceBoxDto boudinxBox;
@override
bool operator ==(Object other) => identical(this, other) || other is UnassignedFacesResponseDto &&
other.assetFaceId == assetFaceId &&
other.assetId == assetId &&
other.boudinxBox == boudinxBox;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(assetFaceId.hashCode) +
(assetId.hashCode) +
(boudinxBox.hashCode);
@override
String toString() => 'UnassignedFacesResponseDto[assetFaceId=$assetFaceId, assetId=$assetId, boudinxBox=$boudinxBox]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'assetFaceId'] = this.assetFaceId;
json[r'assetId'] = this.assetId;
json[r'boudinxBox'] = this.boudinxBox;
return json;
}
/// Returns a new [UnassignedFacesResponseDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static UnassignedFacesResponseDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return UnassignedFacesResponseDto(
assetFaceId: mapValueOfType<String>(json, r'assetFaceId')!,
assetId: mapValueOfType<String>(json, r'assetId')!,
boudinxBox: AssetFaceBoxDto.fromJson(json[r'boudinxBox'])!,
);
}
return null;
}
static List<UnassignedFacesResponseDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <UnassignedFacesResponseDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = UnassignedFacesResponseDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, UnassignedFacesResponseDto> mapFromJson(dynamic json) {
final map = <String, UnassignedFacesResponseDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = UnassignedFacesResponseDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of UnassignedFacesResponseDto-objects as value to a dart map
static Map<String, List<UnassignedFacesResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<UnassignedFacesResponseDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = UnassignedFacesResponseDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetFaceId',
'assetId',
'boudinxBox',
};
}

View 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 AssetFaceBoxDto
void main() {
// final instance = AssetFaceBoxDto();
group('test AssetFaceBoxDto', () {
// 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
});
// int imageHeight
test('to test the property `imageHeight`', () async {
// TODO
});
// int imageWidth
test('to test the property `imageWidth`', () async {
// TODO
});
});
}

View 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
});
});
}

View 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
});
});
}

View file

@ -127,7 +127,7 @@ void main() {
// TODO
});
// List<PersonResponseDto> people (default value: const [])
// List<PeopleAssetResponseDto> people (default value: const [])
test('to test the property `people`', () async {
// TODO
});
@ -172,6 +172,11 @@ void main() {
// TODO
});
// List<UnassignedFacesResponseDto> unassignedPeople (default value: const [])
test('to test the property `unassignedPeople`', () async {
// TODO
});
// DateTime updatedAt
test('to test the property `updatedAt`', () async {
// TODO

View 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 PeopleAssetResponseDto
void main() {
// final instance = PeopleAssetResponseDto();
group('test PeopleAssetResponseDto', () {
// String assetFaceId
test('to test the property `assetFaceId`', () async {
// TODO
});
// PersonResponseDto person
test('to test the property `person`', () async {
// TODO
});
});
}

View file

@ -17,11 +17,21 @@ void main() {
// final instance = PersonApi();
group('tests for PersonApi', () {
//Future<PersonResponseDto> createPerson(AssetFaceUpdateDto assetFaceUpdateDto) async
test('test createPerson', () async {
// TODO
});
//Future<PeopleResponseDto> getAllPeople({ bool withHidden }) async
test('test getAllPeople', () async {
// TODO
});
//Future<AssetFaceBoxDto> getAssetFace(String id, String assetId) async
test('test getAssetFace', () async {
// TODO
});
//Future<PersonResponseDto> getPerson(String id) async
test('test getPerson', () async {
// TODO
@ -47,6 +57,16 @@ void main() {
// TODO
});
//Future<List<PersonResponseDto>> reassignFaces(String id, AssetFaceUpdateDto assetFaceUpdateDto) async
test('test reassignFaces', () async {
// TODO
});
//Future<List<BulkIdResponseDto>> unassignFaces(AssetFaceUpdateDto assetFaceUpdateDto) async
test('test unassignFaces', () async {
// TODO
});
//Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
test('test updatePeople', () async {
// TODO

View file

@ -0,0 +1,37 @@
//
// 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 UnassignedFacesResponseDto
void main() {
// final instance = UnassignedFacesResponseDto();
group('test UnassignedFacesResponseDto', () {
// String assetFaceId
test('to test the property `assetFaceId`', () async {
// TODO
});
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
// AssetFaceBoxDto boudinxBox
test('to test the property `boudinxBox`', () async {
// TODO
});
});
}

View file

@ -3357,6 +3357,49 @@
}
},
"/person": {
"delete": {
"operationId": "unassignFaces",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetFaceUpdateDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/BulkIdResponseDto"
},
"type": "array"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Person"
]
},
"get": {
"operationId": "getAllPeople",
"parameters": [
@ -3397,6 +3440,46 @@
"Person"
]
},
"post": {
"operationId": "createPerson",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetFaceUpdateDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PersonResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Person"
]
},
"put": {
"operationId": "updatePeople",
"parameters": [],
@ -3633,6 +3716,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",
@ -3718,6 +3856,56 @@
]
}
},
"/person/{id}/{assetId}/faceasset": {
"get": {
"operationId": "getAssetFace",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"format": "uuid",
"type": "string"
}
},
{
"name": "assetId",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/AssetFaceBoxDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Person"
]
}
},
"/search": {
"get": {
"operationId": "search",
@ -5816,6 +6004,66 @@
],
"type": "object"
},
"AssetFaceBoxDto": {
"properties": {
"boundingBoxX1": {
"type": "integer"
},
"boundingBoxX2": {
"type": "integer"
},
"boundingBoxY1": {
"type": "integer"
},
"boundingBoxY2": {
"type": "integer"
},
"imageHeight": {
"type": "integer"
},
"imageWidth": {
"type": "integer"
}
},
"required": [
"imageWidth",
"imageHeight",
"boundingBoxX1",
"boundingBoxY1",
"boundingBoxX2",
"boundingBoxY2"
],
"type": "object"
},
"AssetFaceUpdateDto": {
"properties": {
"data": {
"items": {
"$ref": "#/components/schemas/AssetFaceUpdateItem"
},
"type": "array"
}
},
"required": [
"data"
],
"type": "object"
},
"AssetFaceUpdateItem": {
"properties": {
"assetId": {
"type": "string"
},
"personId": {
"type": "string"
}
},
"required": [
"personId",
"assetId"
],
"type": "object"
},
"AssetFileUploadResponseDto": {
"properties": {
"duplicate": {
@ -5971,7 +6219,7 @@
},
"people": {
"items": {
"$ref": "#/components/schemas/PersonResponseDto"
"$ref": "#/components/schemas/PeopleAssetResponseDto"
},
"type": "array"
},
@ -6007,6 +6255,12 @@
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
},
"unassignedPeople": {
"items": {
"$ref": "#/components/schemas/UnassignedFacesResponseDto"
},
"type": "array"
},
"updatedAt": {
"format": "date-time",
"type": "string"
@ -7275,6 +7529,21 @@
],
"type": "string"
},
"PeopleAssetResponseDto": {
"properties": {
"assetFaceId": {
"type": "string"
},
"person": {
"$ref": "#/components/schemas/PersonResponseDto"
}
},
"required": [
"assetFaceId",
"person"
],
"type": "object"
},
"PeopleResponseDto": {
"properties": {
"people": {
@ -8539,6 +8808,25 @@
],
"type": "string"
},
"UnassignedFacesResponseDto": {
"properties": {
"assetFaceId": {
"type": "string"
},
"assetId": {
"type": "string"
},
"boudinxBox": {
"$ref": "#/components/schemas/AssetFaceBoxDto"
}
},
"required": [
"assetFaceId",
"assetId",
"boudinxBox"
],
"type": "object"
},
"UpdateAlbumDto": {
"properties": {
"albumName": {

View file

@ -1,6 +1,11 @@
import { AssetEntity, AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger';
import { PersonResponseDto, mapFace } from '../../person/person.dto';
import {
PeopleAssetResponseDto,
UnassignedFacesResponseDto,
mapFace,
mapUnassignedFace,
} 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 +44,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
exifInfo?: ExifResponseDto;
smartInfo?: SmartInfoResponseDto;
tags?: TagResponseDto[];
people?: PersonResponseDto[];
unassignedPeople?: UnassignedFacesResponseDto[];
people?: PeopleAssetResponseDto[];
/**base64 encoded sha1 hash */
checksum!: string;
stackParentId?: string | null;
@ -96,9 +102,12 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag),
unassignedPeople: entity.faces
?.map(mapUnassignedFace)
.filter((assetFace) => assetFace) as UnassignedFacesResponseDto[],
people: entity.faces
?.map(mapFace)
.filter((person): person is PersonResponseDto => person !== null && !person.isHidden),
.filter((assetFace) => assetFace && !assetFace.person.isHidden) as PeopleAssetResponseDto[],
checksum: entity.checksum.toString('base64'),
stackParentId: entity.stackParentId,
stack: withStack ? entity.stack?.map((a) => mapAsset(a, { stripMetadata })) ?? undefined : undefined,

View file

@ -44,6 +44,23 @@ export class PeopleUpdateDto {
people!: PeopleUpdateItem[];
}
export class AssetFaceUpdateDto {
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetFaceUpdateItem)
data!: AssetFaceUpdateItem[];
}
export class AssetFaceUpdateItem {
@IsString()
@IsNotEmpty()
personId!: string;
@IsString()
@IsNotEmpty()
assetId!: string;
}
export class PeopleUpdateItem extends PersonUpdateDto {
/**
* Person id.
@ -64,6 +81,26 @@ export class PersonSearchDto {
withHidden?: boolean = false;
}
export class AssetFaceBoxDto {
@ApiProperty({ type: 'integer' })
imageWidth!: number;
@ApiProperty({ type: 'integer' })
imageHeight!: number;
@ApiProperty({ type: 'integer' })
boundingBoxX1!: number;
@ApiProperty({ type: 'integer' })
boundingBoxY1!: number;
@ApiProperty({ type: 'integer' })
boundingBoxX2!: number;
@ApiProperty({ type: 'integer' })
boundingBoxY2!: number;
}
export class PersonResponseDto {
id!: string;
name!: string;
@ -73,6 +110,17 @@ export class PersonResponseDto {
isHidden!: boolean;
}
export class PeopleAssetResponseDto {
assetFaceId!: string;
person!: PersonResponseDto;
}
export class UnassignedFacesResponseDto {
assetFaceId!: string;
assetId!: string;
boudinxBox!: AssetFaceBoxDto;
}
export class PersonStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
assets!: number;
@ -98,10 +146,32 @@ export function mapPerson(person: PersonEntity): PersonResponseDto {
};
}
export function mapFace(face: AssetFaceEntity): PersonResponseDto | null {
export function mapFace(face: AssetFaceEntity): PeopleAssetResponseDto | null {
if (face.person) {
return mapPerson(face.person);
return {
assetFaceId: face.id,
person: face.person,
};
} else {
return null;
}
}
export function mapUnassignedFace(face: AssetFaceEntity): UnassignedFacesResponseDto | null {
if (face.person !== null) {
return null;
} else {
return {
assetFaceId: face.id,
assetId: face.assetId,
boudinxBox: {
imageWidth: face.imageWidth,
imageHeight: face.imageHeight,
boundingBoxX1: face.boundingBoxX1,
boundingBoxY1: face.boundingBoxY1,
boundingBoxX2: face.boundingBoxX2,
boundingBoxY2: face.boundingBoxY2,
},
};
}
return null;
}

View file

@ -28,6 +28,8 @@ import {
import { StorageCore } from '../storage';
import { SystemConfigCore } from '../system-config';
import {
AssetFaceBoxDto,
AssetFaceUpdateDto,
MergePersonDto,
PeopleResponseDto,
PeopleUpdateDto,
@ -210,6 +212,137 @@ export class PersonService {
return true;
}
async getFaceEntity(authUser: AuthUserDto, personId: string, assetId: string): Promise<AssetFaceBoxDto> {
await this.access.requirePermission(authUser, Permission.PERSON_READ, personId);
const [face] = await this.repository.getFacesByIds([{ personId, assetId }]);
if (!face) {
throw new BadRequestException('Invalid assetId for feature face');
}
return {
boundingBoxX1: face.boundingBoxX1,
boundingBoxX2: face.boundingBoxX2,
boundingBoxY1: face.boundingBoxY1,
boundingBoxY2: face.boundingBoxY2,
imageHeight: face.imageHeight,
imageWidth: face.imageWidth,
};
}
async createPerson(authUser: AuthUserDto, dto: AssetFaceUpdateDto): Promise<PersonResponseDto> {
const changeFeaturePhoto: string[] = [];
const newPerson = await this.repository.create({ ownerId: authUser.id });
for (const data of dto.data) {
try {
await this.repository.reassignFace(data.personId, newPerson.id, data.assetId);
await this.repository.update({
id: newPerson.id,
faceAssetId: data.assetId,
});
const [face] = await this.repository.getFacesByIds([{ personId: newPerson.id, assetId: data.assetId }]);
const oldPerson = await this.findOrFail(data.personId);
if (oldPerson.faceAssetId === face?.assetId) {
changeFeaturePhoto.push(oldPerson.id);
}
if (!face) {
throw new BadRequestException('Invalid assetId for feature face');
}
} catch (error: Error | any) {
this.logger.error(`Unable to create a new person`, error?.stack);
}
}
const newPersonFeaturePhoto = await this.repository.getRandomFace(newPerson.id);
if (newPersonFeaturePhoto) {
await this.repository.update({
id: newPerson.id,
faceAssetId: newPersonFeaturePhoto.assetId,
});
await this.jobRepository.queue({
name: JobName.GENERATE_PERSON_THUMBNAIL,
data: {
id: newPerson.id,
},
});
}
await this.createNewFeaturePhoto(changeFeaturePhoto);
return newPerson;
}
async reassignFaces(authUser: AuthUserDto, personId: string, dto: AssetFaceUpdateDto): Promise<PersonResponseDto[]> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, personId);
const result: PersonResponseDto[] = [];
const changeFeaturePhoto: string[] = [];
for (const data of dto.data) {
try {
const [face] = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
const oldPerson = await this.findOrFail(data.personId);
if (oldPerson.faceAssetId === face?.assetId) {
changeFeaturePhoto.push(oldPerson.id);
}
await this.repository.reassignFace(data.personId, personId, data.assetId);
result.push(await this.findOrFail(personId).then(mapPerson));
} catch (error: Error | any) {
this.logger.error(
`Unable to un-merge asset ${data.assetId} from ${data.personId} to ${personId}`,
error?.stack,
);
}
}
await this.createNewFeaturePhoto(changeFeaturePhoto);
return result;
}
async unassignFaces(authUser: AuthUserDto, dto: AssetFaceUpdateDto): Promise<BulkIdResponseDto[]> {
const results: BulkIdResponseDto[] = [];
for (const data of dto.data) {
try {
const [face] = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]);
await this.access.requirePermission(authUser, Permission.ASSET_VIEW, face.assetId);
await this.repository.unassignFace(face.id);
results.push({id:face.id, success:true});
} catch (error: Error | any) {
this.logger.error(
`Unable to un-merge asset ${data.assetId} from ${data.personId}`,
error?.stack,
);
results.push({ id: 'face.id', success: false, error: BulkIdErrorReason.UNKNOWN });
}
}
return results;
}
async createNewFeaturePhoto(changeFeaturePhoto: string[]) {
for (const personId of changeFeaturePhoto) {
const assetFace = await this.repository.getRandomFace(personId);
if (assetFace !== null) {
await this.repository.update({
id: personId,
faceAssetId: assetFace.assetId,
});
await this.jobRepository.queue({
name: JobName.GENERATE_PERSON_THUMBNAIL,
data: {
id: personId,
},
});
}
}
}
async handleRecognizeFaces({ id }: IEntityJob) {
const { machineLearning } = await this.configCore.getConfig();
if (!machineLearning.enabled || !machineLearning.facialRecognition.enabled) {

View file

@ -36,6 +36,8 @@ export interface IPersonRepository {
getAssets(personId: string): Promise<AssetEntity[]>;
prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
reassignFaces(data: UpdateFacesData): Promise<number>;
reassignFace(oldPersonId: string, newPersonId: string, assetId: string): Promise<number>;
unassignFace(id: string): Promise<number>;
create(entity: Partial<PersonEntity>): Promise<PersonEntity>;
update(entity: Partial<PersonEntity>): Promise<PersonEntity>;

View file

@ -1,4 +1,6 @@
import {
AssetFaceBoxDto,
AssetFaceUpdateDto,
AssetResponseDto,
AuthUserDto,
BulkIdResponseDto,
@ -12,8 +14,9 @@ import {
PersonStatisticsResponseDto,
PersonUpdateDto,
} from '@app/domain';
import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
import { Body, Controller, Delete, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { ParseMeUUIDPipe } from '../api-v1/validation/parse-me-uuid-pipe';
import { AuthUser, Authenticated } from '../app.guard';
import { UseValidation } from '../app.utils';
import { UUIDParamDto } from './dto/uuid-param.dto';
@ -29,6 +32,37 @@ function asStreamableFile({ stream, type, length }: ImmichReadStream) {
export class PersonController {
constructor(private service: PersonService) {}
@Put(':id/reassign')
reassignFaces(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Body() dto: AssetFaceUpdateDto,
): Promise<PersonResponseDto[]> {
return this.service.reassignFaces(authUser, id, dto);
}
@Delete('')
unassignFaces(
@AuthUser() authUser: AuthUserDto,
@Body() dto: AssetFaceUpdateDto,
): Promise<BulkIdResponseDto[]> {
return this.service.unassignFaces(authUser, dto);
}
@Get(':id/:assetId/faceasset')
getAssetFace(
@AuthUser() authUser: AuthUserDto,
@Param() { id }: UUIDParamDto,
@Param('assetId', new ParseMeUUIDPipe({ version: '4' })) assetId: string,
): Promise<AssetFaceBoxDto> {
return this.service.getFaceEntity(authUser, id, assetId);
}
@Post('')
createPerson(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetFaceUpdateDto): Promise<PersonResponseDto> {
return this.service.createPerson(authUser, dto);
}
@Get()
getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(authUser, withHidden);

View file

@ -47,6 +47,29 @@ export class PersonRepository implements IPersonRepository {
return result.affected ?? 0;
}
async reassignFace(oldPersonId: string, newPersonId: string, assetId: string): Promise<number> {
const result = await this.assetFaceRepository
.createQueryBuilder()
.update()
.set({ personId: newPersonId })
.where({ personId: oldPersonId })
.andWhere({ assetId })
.execute();
return result.affected ?? 0;
}
async unassignFace(id: string): Promise<number> {
const result = await this.assetFaceRepository
.createQueryBuilder()
.update()
.set({ personId: null })
.where({ id })
.execute();
return result.affected ?? 0;
}
delete(entity: PersonEntity): Promise<PersonEntity | null> {
return this.personRepository.remove(entity);
}

View file

@ -9,6 +9,7 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
getAssets: jest.fn(),
getAllWithoutFaces: jest.fn(),
reassignFace: jest.fn(),
getByName: jest.fn(),
create: jest.fn(),

View file

@ -502,6 +502,81 @@ export const AssetBulkUploadCheckResultReasonEnum = {
export type AssetBulkUploadCheckResultReasonEnum = typeof AssetBulkUploadCheckResultReasonEnum[keyof typeof AssetBulkUploadCheckResultReasonEnum];
/**
*
* @export
* @interface AssetFaceBoxDto
*/
export interface AssetFaceBoxDto {
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxX1': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxX2': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxY1': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'boundingBoxY2': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'imageHeight': number;
/**
*
* @type {number}
* @memberof AssetFaceBoxDto
*/
'imageWidth': number;
}
/**
*
* @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
@ -744,10 +819,10 @@ export interface AssetResponseDto {
'ownerId': string;
/**
*
* @type {Array<PersonResponseDto>}
* @type {Array<PeopleAssetResponseDto>}
* @memberof AssetResponseDto
*/
'people'?: Array<PersonResponseDto>;
'people'?: Array<PeopleAssetResponseDto>;
/**
*
* @type {boolean}
@ -796,6 +871,12 @@ export interface AssetResponseDto {
* @memberof AssetResponseDto
*/
'type': AssetTypeEnum;
/**
*
* @type {Array<UnassignedFacesResponseDto>}
* @memberof AssetResponseDto
*/
'unassignedPeople'?: Array<UnassignedFacesResponseDto>;
/**
*
* @type {string}
@ -2315,6 +2396,25 @@ export const PathType = {
export type PathType = typeof PathType[keyof typeof PathType];
/**
*
* @export
* @interface PeopleAssetResponseDto
*/
export interface PeopleAssetResponseDto {
/**
*
* @type {string}
* @memberof PeopleAssetResponseDto
*/
'assetFaceId': string;
/**
*
* @type {PersonResponseDto}
* @memberof PeopleAssetResponseDto
*/
'person': PersonResponseDto;
}
/**
*
* @export
@ -3949,6 +4049,31 @@ export const TranscodePolicy = {
export type TranscodePolicy = typeof TranscodePolicy[keyof typeof TranscodePolicy];
/**
*
* @export
* @interface UnassignedFacesResponseDto
*/
export interface UnassignedFacesResponseDto {
/**
*
* @type {string}
* @memberof UnassignedFacesResponseDto
*/
'assetFaceId': string;
/**
*
* @type {string}
* @memberof UnassignedFacesResponseDto
*/
'assetId': string;
/**
*
* @type {AssetFaceBoxDto}
* @memberof UnassignedFacesResponseDto
*/
'boudinxBox': AssetFaceBoxDto;
}
/**
*
* @export
@ -11766,6 +11891,50 @@ export class PartnerApi extends BaseAPI {
*/
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPerson: async (assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
assertParamExists('createPerson', 'assetFaceUpdateDto', assetFaceUpdateDto)
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)
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 {boolean} [withHidden]
@ -11800,6 +11969,52 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
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 {string} assetId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetFace: async (id: string, assetId: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getAssetFace', 'id', id)
// verify required parameter 'assetId' is not null or undefined
assertParamExists('getAssetFace', 'assetId', assetId)
const localVarPath = `/person/{id}/{assetId}/faceasset`
.replace(`{${"id"}}`, encodeURIComponent(String(id)))
.replace(`{${"assetId"}}`, encodeURIComponent(String(assetId)));
// 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)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -12025,6 +12240,98 @@ 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 {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unassignFaces: async (assetFaceUpdateDto: AssetFaceUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetFaceUpdateDto' is not null or undefined
assertParamExists('unassignFaces', 'assetFaceUpdateDto', assetFaceUpdateDto)
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: 'DELETE', ...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
@ -12127,6 +12434,16 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
return {
/**
*
* @param {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createPerson(assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createPerson(assetFaceUpdateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {boolean} [withHidden]
@ -12137,6 +12454,17 @@ export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(withHidden, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {string} assetId
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAssetFace(id: string, assetId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFaceBoxDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAssetFace(id, assetId, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
@ -12188,6 +12516,27 @@ 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 {AssetFaceUpdateDto} assetFaceUpdateDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async unassignFaces(assetFaceUpdateDto: AssetFaceUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.unassignFaces(assetFaceUpdateDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -12219,6 +12568,15 @@ export const PersonApiFp = function(configuration?: Configuration) {
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = PersonApiFp(configuration)
return {
/**
*
* @param {PersonApiCreatePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createPerson(requestParameters: PersonApiCreatePersonRequest, options?: AxiosRequestConfig): AxiosPromise<PersonResponseDto> {
return localVarFp.createPerson(requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -12228,6 +12586,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: AxiosRequestConfig): AxiosPromise<PeopleResponseDto> {
return localVarFp.getAllPeople(requestParameters.withHidden, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetAssetFaceRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAssetFace(requestParameters: PersonApiGetAssetFaceRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFaceBoxDto> {
return localVarFp.getAssetFace(requestParameters.id, requestParameters.assetId, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiGetPersonRequest} requestParameters Request parameters.
@ -12273,6 +12640,24 @@ 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 {PersonApiUnassignFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
return localVarFp.unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
@ -12294,6 +12679,20 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
};
};
/**
* Request parameters for createPerson operation in PersonApi.
* @export
* @interface PersonApiCreatePersonRequest
*/
export interface PersonApiCreatePersonRequest {
/**
*
* @type {AssetFaceUpdateDto}
* @memberof PersonApiCreatePerson
*/
readonly assetFaceUpdateDto: AssetFaceUpdateDto
}
/**
* Request parameters for getAllPeople operation in PersonApi.
* @export
@ -12308,6 +12707,27 @@ export interface PersonApiGetAllPeopleRequest {
readonly withHidden?: boolean
}
/**
* Request parameters for getAssetFace operation in PersonApi.
* @export
* @interface PersonApiGetAssetFaceRequest
*/
export interface PersonApiGetAssetFaceRequest {
/**
*
* @type {string}
* @memberof PersonApiGetAssetFace
*/
readonly id: string
/**
*
* @type {string}
* @memberof PersonApiGetAssetFace
*/
readonly assetId: string
}
/**
* Request parameters for getPerson operation in PersonApi.
* @export
@ -12385,6 +12805,41 @@ 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 unassignFaces operation in PersonApi.
* @export
* @interface PersonApiUnassignFacesRequest
*/
export interface PersonApiUnassignFacesRequest {
/**
*
* @type {AssetFaceUpdateDto}
* @memberof PersonApiUnassignFaces
*/
readonly assetFaceUpdateDto: AssetFaceUpdateDto
}
/**
* Request parameters for updatePeople operation in PersonApi.
* @export
@ -12427,6 +12882,17 @@ export interface PersonApiUpdatePersonRequest {
* @extends {BaseAPI}
*/
export class PersonApi extends BaseAPI {
/**
*
* @param {PersonApiCreatePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public createPerson(requestParameters: PersonApiCreatePersonRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).createPerson(requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters.
@ -12438,6 +12904,17 @@ export class PersonApi extends BaseAPI {
return PersonApiFp(this.configuration).getAllPeople(requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetAssetFaceRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public getAssetFace(requestParameters: PersonApiGetAssetFaceRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).getAssetFace(requestParameters.id, requestParameters.assetId, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiGetPersonRequest} requestParameters Request parameters.
@ -12493,6 +12970,28 @@ 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 {PersonApiUnassignFacesRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public unassignFaces(requestParameters: PersonApiUnassignFacesRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).unassignFaces(requestParameters.assetFaceUpdateDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.

View file

@ -145,6 +145,9 @@
/>
{/if}
{#if isOwner}
{#if !asset.isReadOnly && !asset.isExternal}
<CircleIconButton isOpacity={true} icon={mdiDeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
{/if}
<CircleIconButton
isOpacity={true}
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}

View file

@ -355,7 +355,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-x-hidden overflow-y-hidden bg-black"
bind:this={assetViewerHtmlElement}
>
<div class="z-[1000] col-span-4 col-start-1 row-span-1 row-start-1 transition-transform">

View file

@ -3,18 +3,44 @@
import { locale } from '$lib/stores/preferences.store';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
import { getAssetFilename } from '$lib/utils/asset-utils';
import { AlbumResponseDto, AssetResponseDto, ThumbnailFormat, api } from '@api';
import {
AlbumResponseDto,
AssetResponseDto,
PersonResponseDto,
ThumbnailFormat,
UnassignedFacesResponseDto,
api,
} from '@api';
import type { LatLngTuple } from 'leaflet';
import { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { asByteUnitString } from '../../utils/byte-units';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import { mdiCalendar, mdiCameraIris, mdiClose, mdiImageOutline, mdiMapMarkerOutline } from '@mdi/js';
import PersonSidePanel, { PersonToCreate } from '../faces-page/person-side-panel.svelte';
import { mdiCalendar, mdiCameraIris, mdiClose, mdiImageOutline, mdiMapMarkerOutline, mdiPencil } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = [];
let unassignedFaces: UnassignedFacesResponseDto[] = [];
let people: PersonResponseDto[] = [];
let customFeaturePhoto = new Array<PersonToCreate | null>(asset.people?.length || 0);
let previousId: string;
$: {
if (!previousId) {
previousId = asset.id;
}
if (asset.id !== previousId) {
customFeaturePhoto = new Array<PersonToCreate | null>(asset.people?.length || 0);
showEditFaces = false;
previousId = asset.id;
}
}
let showEditFaces = false;
let textarea: HTMLTextAreaElement;
let description: string;
@ -23,9 +49,15 @@
$: {
// Get latest description from server
if (asset.id && !api.isSharedLink) {
if (asset.id && !api.isSharedLink && !showEditFaces) {
api.assetApi.getAssetById({ id: asset.id }).then((res) => {
people = res.data?.people || [];
if (res.data && res.data.people) {
unassignedFaces = res.data?.unassignedPeople || [];
people = (res.data?.people || [])
.map((peopleAsset) => peopleAsset.person)
.filter((person): person is PersonResponseDto => person !== null);
}
textarea.value = res.data?.exifInfo?.description || '';
});
}
@ -43,8 +75,6 @@
$: lat = latlng ? latlng[0] : undefined;
$: lng = latlng ? latlng[1] : undefined;
$: people = asset.people || [];
const dispatch = createEventDispatcher();
const getMegapixel = (width: number, height: number): number | undefined => {
@ -82,285 +112,307 @@
};
</script>
<section class="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"
on:click={() => dispatch('close')}
>
<Icon path={mdiClose} size="24" />
</button>
<div class="relative overflow-x-hidden">
<section class="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"
on:click={() => dispatch('close')}
>
<Icon path={mdiClose} size="24" />
</button>
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
</div>
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
</div>
{#if asset.isOffline}
<section class="px-4 py-4">
<div role="alert">
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">Asset offline</div>
<div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
<p>
This asset is offline. Immich can not access its file location. Please ensure the asset is available and
then rescan the library.
</p>
{#if asset.isOffline}
<section class="px-4 py-4">
<div role="alert">
<div class="rounded-t bg-red-500 px-4 py-2 font-bold text-white">Asset offline</div>
<div class="rounded-b border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
<p>
This asset is offline. Immich can not access its file location. Please ensure the asset is available and
then rescan the library.
</p>
</div>
</div>
</div>
</section>
{/if}
<section class="mx-4 mt-10" style:display={!isOwner && textarea?.value == '' ? 'none' : 'block'}>
<textarea
bind:this={textarea}
class="max-h-[500px]
w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary"
placeholder={!isOwner ? '' : 'Add a description'}
on:focusin={handleFocusIn}
on:focusout={handleFocusOut}
on:input={autoGrowHeight}
bind:value={description}
disabled={!isOwner}
/>
</section>
{#if !api.isSharedLink && people.length > 0}
<section class="px-4 py-4 text-sm">
<h2>PEOPLE</h2>
<div class="mt-4 flex flex-wrap gap-2">
{#each people as person (person.id)}
<a href="/people/{person.id}" class="w-[90px]" on:click={() => dispatch('close-viewer')}>
<ImageThumbnail
curve
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
/>
<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>
{/each}
</div>
</section>
{/if}
<div class="px-4 py-4">
{#if !asset.exifInfo && !asset.isExternal}
<p class="text-sm">NO EXIF INFO AVAILABLE</p>
{:else if !asset.exifInfo && asset.isExternal}
<div class="flex gap-4 py-4">
<div>
<p class="break-all">
Metadata not loaded for {asset.originalPath}
</p>
</div>
</div>
{:else}
<p class="text-sm">DETAILS</p>
</section>
{/if}
{#if asset.exifInfo?.dateTimeOriginal}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
<div class="flex gap-4 py-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
<section class="mx-4 mt-10" style:display={!isOwner && textarea?.value == '' ? 'none' : 'block'}>
<textarea
bind:this={textarea}
class="max-h-[500px]
w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary"
placeholder={!isOwner ? '' : 'Add a description'}
on:focusin={handleFocusIn}
on:focusout={handleFocusOut}
on:input={autoGrowHeight}
bind:value={description}
disabled={!isOwner}
/>
</section>
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{
month: 'short',
day: 'numeric',
year: 'numeric',
},
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
{#if !api.isSharedLink && people.length > 0}
<section class="px-4 py-4 text-sm">
<div class="grid grid-cols-2 items-center">
<h2 class="justify-self-start uppercase">People</h2>
<button class="justify-self-end" on:click={() => (showEditFaces = true)}>
<Icon path={mdiPencil} size={18} />
</button>
{#if unassignedFaces.length > 0}
<p>{`${unassignedFaces.length} face${unassignedFaces.length > 1 ? 's' : ''} available to add`}</p>
{/if}
</div>
{#if people.length > 0}
<div class="mt-4 flex flex-wrap gap-2">
{#each people as person, index (person.id)}
<a href="/people/{person.id}" class="w-[90px]" on:click={() => dispatch('close-viewer')}>
<ImageThumbnail
curve
shadow
url={customFeaturePhoto[index]?.thumbnail || api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
/>
<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>
{/each}
</div>
{/if}
</section>
{/if}
<div class="px-4 py-4">
{#if !asset.exifInfo && !asset.isExternal}
<p class="text-sm">NO EXIF INFO AVAILABLE</p>
{:else if !asset.exifInfo && asset.isExternal}
<div class="flex gap-4 py-4">
<div>
<p class="break-all">
Metadata not loaded for {asset.originalPath}
</p>
</div>
</div>
{:else}
<p class="text-sm">DETAILS</p>
{/if}
{#if asset.exifInfo?.dateTimeOriginal}
{@const assetDateTimeOriginal = DateTime.fromISO(asset.exifInfo.dateTimeOriginal, {
zone: asset.exifInfo.timeZone ?? undefined,
})}
<div class="flex gap-4 py-4">
<div>
<Icon path={mdiCalendar} size="24" />
</div>
<div>
<p>
{assetDateTimeOriginal.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
month: 'short',
day: 'numeric',
year: 'numeric',
},
{ locale: $locale },
)}
</p>
<div class="flex gap-2 text-sm">
<p>
{assetDateTimeOriginal.toLocaleString(
{
weekday: 'short',
hour: 'numeric',
minute: '2-digit',
timeZoneName: 'longOffset',
},
{ locale: $locale },
)}
</p>
</div>
</div>
</div>{/if}
{#if asset.exifInfo?.fileSizeInByte}
<div class="flex gap-4 py-4">
<div><Icon path={mdiImageOutline} size="24" /></div>
<div>
<p class="break-all">
{getAssetFilename(asset)}
</p>
<div class="flex gap-2 text-sm">
{#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
<p>
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
</p>
{/if}
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>
{/if}
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
</div>
</div>
</div>
</div>{/if}
{/if}
{#if asset.exifInfo?.fileSizeInByte}
<div class="flex gap-4 py-4">
<div><Icon path={mdiImageOutline} size="24" /></div>
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
<div class="flex gap-4 py-4">
<div><Icon path={mdiCameraIris} size="24" /></div>
<div>
<p class="break-all">
{getAssetFilename(asset)}
</p>
<div class="flex gap-2 text-sm">
{#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
<div>
<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
<div class="flex gap-2 text-sm">
{#if asset.exifInfo?.fNumber}
<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` || ''}</p>
{/if}
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime}`}</p>
{/if}
{#if asset.exifInfo.focalLength}
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
{/if}
{#if asset.exifInfo.iso}
<p>
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
{`ISO ${asset.exifInfo.iso}`}
</p>
{/if}
<p>{asset.exifInfo.exifImageHeight} x {asset.exifInfo.exifImageWidth}</p>
{/if}
<p>{asByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
</div>
</div>
</div>
</div>
{/if}
{/if}
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
<div class="flex gap-4 py-4">
<div><Icon path={mdiCameraIris} size="24" /></div>
<div>
<p>{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}</p>
<div class="flex gap-2 text-sm">
{#if asset.exifInfo?.fNumber}
<p>{`ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` || ''}</p>
{/if}
{#if asset.exifInfo.exposureTime}
<p>{`${asset.exifInfo.exposureTime}`}</p>
{/if}
{#if asset.exifInfo.focalLength}
<p>{`${asset.exifInfo.focalLength.toLocaleString($locale)} mm`}</p>
{/if}
{#if asset.exifInfo.iso}
<p>
{`ISO ${asset.exifInfo.iso}`}
</p>
{/if}
</div>
</div>
</div>
{/if}
{#if asset.exifInfo?.city}
<div class="flex gap-4 py-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>
</div>
{/if}
{#if asset.exifInfo?.country}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.country}</p>
</div>
{/if}
</div>
</div>
{/if}
</div>
</section>
{#if latlng && $featureFlags.loaded && $featureFlags.map}
<div class="h-[360px]">
{#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
<Map center={latlng} zoom={14}>
<TileLayer
urlTemplate={$serverConfig.mapTileUrl}
options={{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}}
/>
<Marker {latlng}>
<p>
{lat}, {lng}
</p>
<a href="https://www.openstreetmap.org/?mlat={lat}&mlon={lng}&zoom=15#map=15/{lat}/{lng}">
Open in OpenStreetMap
</a>
</Marker>
</Map>
{/await}
</div>
{/if}
{#if asset.owner && !isOwner}
<section class="px-6 pt-6 dark:text-immich-dark-fg">
<p class="text-sm">SHARED BY</p>
<div class="flex gap-4 pt-4">
<div>
<UserAvatar user={asset.owner} size="md" autoColor />
</div>
<div class="mb-auto mt-auto">
<p>
{asset.owner.firstName}
{asset.owner.lastName}
</p>
</div>
</div>
</section>
{/if}
{#if albums.length > 0}
<section class="p-6 dark:text-immich-dark-fg">
<p class="pb-4 text-sm">APPEARS IN</p>
{#each albums as album}
<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex gap-4 py-2 hover:cursor-pointer"
on:click={() => dispatch('click', album)}
on:keydown={() => dispatch('click', album)}
>
{#if asset.exifInfo?.city}
<div class="flex gap-4 py-4">
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
<div>
<img
alt={album.albumName}
class="h-[50px] w-[50px] rounded object-cover"
src={album.albumThumbnailAssetId &&
api.getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Jpeg)}
draggable="false"
/>
</div>
<div class="mb-auto mt-auto">
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
<div class="flex gap-2 text-sm">
<p>{album.assetCount} items</p>
{#if album.shared}
<p>· Shared</p>
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>
</div>
{/if}
{#if asset.exifInfo?.country}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.country}</p>
</div>
{/if}
</div>
</div>
</div>
</a>
{/each}
{/if}
</div>
</section>
{#if latlng && $featureFlags.loaded && $featureFlags.map}
<div class="h-[360px]">
{#await import('../shared-components/leaflet') then { Map, TileLayer, Marker }}
<Map center={latlng} zoom={14}>
<TileLayer
urlTemplate={$serverConfig.mapTileUrl}
options={{
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
}}
/>
<Marker {latlng}>
<p>
{lat}, {lng}
</p>
<a href="https://www.openstreetmap.org/?mlat={lat}&mlon={lng}&zoom=15#map=15/{lat}/{lng}">
Open in OpenStreetMap
</a>
</Marker>
</Map>
{/await}
</div>
{/if}
{#if asset.owner && !isOwner}
<section class="px-6 pt-6 dark:text-immich-dark-fg">
<p class="text-sm">SHARED BY</p>
<div class="flex gap-4 pt-4">
<div>
<UserAvatar user={asset.owner} size="md" autoColor />
</div>
<div class="mb-auto mt-auto">
<p>
{asset.owner.firstName}
{asset.owner.lastName}
</p>
</div>
</div>
</section>
{/if}
{#if albums.length > 0}
<section class="p-6 dark:text-immich-dark-fg">
<p class="pb-4 text-sm">APPEARS IN</p>
{#each albums as album}
<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
class="flex gap-4 py-2 hover:cursor-pointer"
on:click={() => dispatch('click', album)}
on:keydown={() => dispatch('click', album)}
>
<div>
<img
alt={album.albumName}
class="h-[50px] w-[50px] rounded object-cover"
src={album.albumThumbnailAssetId &&
api.getAssetThumbnailUrl(album.albumThumbnailAssetId, ThumbnailFormat.Jpeg)}
draggable="false"
/>
</div>
<div class="mb-auto mt-auto">
<p class="dark:text-immich-dark-primary">{album.albumName}</p>
<div class="flex gap-2 text-sm">
<p>{album.assetCount} items</p>
{#if album.shared}
<p>· Shared</p>
{/if}
</div>
</div>
</div>
</a>
{/each}
</section>
{/if}
</div>
{#if showEditFaces}
<PersonSidePanel
bind:people
bind:unassignedFaces
bind:selectedPersonToCreate={customFeaturePhoto}
assetId={asset.id}
on:close={() => (showEditFaces = false)}
/>
{/if}

View file

@ -17,8 +17,8 @@
<div
class="flex w-full h-14 place-items-center {suggestedPeople
? 'rounded-t-lg dark:border-immich-dark-gray'
: 'rounded-lg'} bg-gray-100 p-2 dark:bg-gray-700"
? 'rounded-t-lg border-immich-primary dark:border-immich-dark-gray'
: 'rounded-lg'} bg-gray-200 p-2 dark:bg-gray-700"
>
<ImageThumbnail
circle
@ -36,7 +36,7 @@
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
class="w-full gap-2 bg-gray-200 dark:bg-gray-700 dark:text-white"
type="text"
placeholder="New name or nickname"
bind:value={name}

View file

@ -12,6 +12,8 @@
import { handleError } from '$lib/utils/handle-error';
import { goto, invalidateAll } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import PeopleList from './people-list.svelte';
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
@ -69,7 +71,6 @@
message: `Merged ${count} ${count === 1 ? 'person' : 'people'}`,
type: NotificationType.Info,
});
people = people.filter((person) => !results.some((result) => result.id === person.id && result.success === true));
await invalidateAll();
dispatch('merge');
} catch (error) {
@ -133,16 +134,8 @@
<FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
</div>
</div>
<div
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
style:max-height={screenHeight - 200 - 200 + '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} {screenHeight} on:select={({ detail }) => onSelect(detail)} />
</section>
{#if isShowConfirmation}

View file

@ -0,0 +1,31 @@
<script lang="ts">
import type { PersonResponseDto } from '@api';
import FaceThumbnail from './face-thumbnail.svelte';
import { createEventDispatcher } from 'svelte';
export let screenHeight: number;
export let people: PersonResponseDto[] = [];
let dispatch = createEventDispatcher<{
select: PersonResponseDto;
}>();
</script>
<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>

View file

@ -0,0 +1,453 @@
<script lang="ts" context="module">
export type PersonToCreate = {
thumbnail: string;
canEdit: boolean;
};
</script>
<script lang="ts">
import { blur, fly } from 'svelte/transition';
import { linear } from 'svelte/easing';
import { api, ThumbnailFormat, type PersonResponseDto, UnassignedFacesResponseDto } from '@api';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import { cloneDeep } from 'lodash-es';
import { handleError } from '$lib/utils/handle-error';
import { createEventDispatcher, onMount } from 'svelte';
import { mdiRestart, mdiAccount, mdiMinus, mdiClose, mdiPlus, mdiMagnify, mdiArrowLeftThin } from '@mdi/js';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import Icon from '$lib/components/elements/icon.svelte';
export let people: PersonResponseDto[];
export let unassignedFaces: UnassignedFacesResponseDto[];
export let assetId: string;
let peopleToAdd: (PersonResponseDto | string)[] = [];
let searchedPeople: PersonResponseDto[] = [];
let searchWord: string;
const dispatch = createEventDispatcher();
let searchFaces = false;
let searchName = '';
let isSearchingPeople = false;
let allPeople: PersonResponseDto[] = [];
let editedPerson: number;
let selectedPersonToReassign: (PersonResponseDto | null)[] = new Array<PersonResponseDto | null>(people.length);
export let selectedPersonToCreate: (PersonToCreate | null)[] = new Array<PersonToCreate | null>(people.length);
let showSeletecFaces = false;
let showLoadingSpinner = false;
let editedPeople = cloneDeep(people);
onMount(async () => {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
allPeople = data.people;
peopleToAdd = await initUnassignedFaces();
});
const searchPeople = async () => {
if ((people.length < 20 && searchName.startsWith(searchWord)) || searchName === '') {
return;
}
const timeout = setTimeout(() => (isSearchingPeople = true), 100);
try {
const { data } = await api.searchApi.searchPerson({ name: searchName });
searchedPeople = data.filter((item) => item.id !== people[editedPerson].id);
searchWord = searchName;
} catch (error) {
handleError(error, "Can't search people");
} finally {
clearTimeout(timeout);
}
isSearchingPeople = false;
};
$: {
searchedPeople = !searchName
? allPeople
: allPeople
.filter((person: PersonResponseDto) => {
const nameParts = person.name.split(' ');
return nameParts.some((splitName) => splitName.toLowerCase().startsWith(searchName.toLowerCase()));
})
.slice(0, 5);
}
const initInput = (element: HTMLInputElement) => {
element.focus();
};
const initUnassignedFaces = async (): Promise<string[]> => {
const results: string[] = [];
for (let i = 0; i < unassignedFaces.length; i++) {
const data = await api.getAssetThumbnailUrl(assetId, ThumbnailFormat.Jpeg);
const newFeaturePhoto = await zoomImageToBase64(
data,
unassignedFaces[i].boudinxBox.boundingBoxX1,
unassignedFaces[i].boudinxBox.boundingBoxX2,
unassignedFaces[i].boudinxBox.boundingBoxY1,
unassignedFaces[i].boudinxBox.boundingBoxY2,
);
if (newFeaturePhoto) {
results.push(newFeaturePhoto);
}
}
return results;
};
const handleBackButton = () => {
searchName = '';
searchFaces = false;
selectedPersonToCreate = new Array<PersonToCreate | null>(people.length);
if (showSeletecFaces) {
showSeletecFaces = false;
} else {
dispatch('close');
}
};
async function zoomImageToBase64(
imageSrc: string,
x1: number,
x2: number,
y1: number,
y2: number,
): Promise<string | null> {
// Calculate width and height from the coordinates
const width = x2 - x1;
const height = y2 - y1;
// Create an image element and load the image source
const img = new Image();
img.src = imageSrc;
// Wait for the image to load
await new Promise((resolve) => {
img.onload = resolve;
img.onerror = () => resolve(null); // Handle image load errors
});
// Calculate the new width and height after zooming out
const newWidth = width * 1.5;
const newHeight = height * 1.5;
// Create a canvas element to draw the zoomed-out image
const canvas = document.createElement('canvas');
canvas.width = newWidth;
canvas.height = newHeight;
// Draw the zoomed-out portion of the image onto the canvas
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(
img,
x1 - (newWidth - width) / 2,
y1 - (newHeight - height) / 2,
newWidth,
newHeight,
0,
0,
newWidth,
newHeight,
);
// Convert the canvas content to base64
const base64Image = canvas.toDataURL('image/webp');
return base64Image;
} else {
return null;
}
}
const handleReset = (index: number) => {
editedPeople[index] = people[index];
if (selectedPersonToReassign[index]) {
selectedPersonToReassign[index] = null;
}
if (selectedPersonToCreate[index]) {
selectedPersonToCreate[index] = null;
}
};
const handleEditFaces = async () => {
const numberOfChanges =
selectedPersonToCreate.filter((person) => person !== null && person.canEdit !== false).length +
selectedPersonToReassign.filter((person) => person !== null).length;
if (numberOfChanges > 0) {
showLoadingSpinner = true;
try {
for (let i = 0; i < selectedPersonToReassign.length; i++) {
const personId = selectedPersonToReassign[i]?.id;
if (personId) {
await api.personApi.reassignFaces({
id: personId,
assetFaceUpdateDto: { data: [{ assetId: assetId, personId: people[i].id }] },
});
people[i] = selectedPersonToReassign[i] as PersonResponseDto;
}
}
for (let i = 0; i < selectedPersonToCreate.length; i++) {
const personToCreate = selectedPersonToCreate[i];
if (personToCreate && personToCreate.canEdit !== false) {
const { data } = await api.personApi.createPerson({
assetFaceUpdateDto: { data: [{ assetId: assetId, personId: people[i].id }] },
});
people[i] = data;
selectedPersonToCreate[i] = personToCreate;
}
}
notificationController.show({
message: `Edited ${numberOfChanges} ${numberOfChanges > 1 ? 'people' : 'person'}`,
type: NotificationType.Info,
});
} catch (error) {
handleError(error, "Can't apply changes");
}
showLoadingSpinner = false;
}
dispatch('close');
};
const handleCreatePerson = async () => {
const { data } = await api.personApi.getAssetFace({ id: people[editedPerson].id, assetId });
const assetFace = data;
for (let i = 0; i < people.length; i++) {
if (people[i].id === people[editedPerson].id) {
const data = await api.getAssetThumbnailUrl(assetId, ThumbnailFormat.Jpeg);
const newFeaturePhoto = await zoomImageToBase64(
data,
assetFace.boundingBoxX1,
assetFace.boundingBoxX2,
assetFace.boundingBoxY1,
assetFace.boundingBoxY2,
);
if (newFeaturePhoto) {
selectedPersonToCreate[i] = { canEdit: true, thumbnail: data };
}
break;
}
}
showSeletecFaces = false;
};
const handleReassignFace = (person: PersonResponseDto) => {
selectedPersonToReassign[editedPerson] = person;
editedPeople[editedPerson] = person;
console.log(selectedPersonToReassign);
showSeletecFaces = false;
};
const handlePersonPicker = async (index: number) => {
editedPerson = index;
searchedPeople = allPeople.filter((item) => item.id !== people[index].id);
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}
>
<Icon path={mdiArrowLeftThin} size="24" />
</button>
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">Edit faces</p>
</div>
{#if !showLoadingSpinner}
<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">
{#each editedPeople as person, index}
<div class="relative h-[115px] w-[95px]">
<a href="/people/{person.id}">
<div class="absolute top-0 left-1/2 transform -translate-x-1/2 h-[90px] w-[90px]">
<ImageThumbnail
curve
shadow
url={selectedPersonToCreate[index]?.thumbnail || api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
/>
<p class="relative mt-1 truncate font-medium" title={person.name}>
{person.name}
</p>
</div>
</a>
<div
transition:blur={{ amount: 10, duration: 50 }}
class="absolute -left-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-red-700"
>
<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">
<Icon path={mdiMinus} size={18} />
</div>
</button>
</div>
<div
transition:blur={{ amount: 10, duration: 50 }}
class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700"
>
{#if (selectedPersonToCreate[index] && selectedPersonToCreate[index]?.canEdit !== false) || 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">
<Icon path={mdiRestart} size={18} />
</div>
</button>
{:else}
<button on:click={() => handlePersonPicker(index)} class="flex h-full w-full">
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<Icon path={mdiAccount} size={18} />
</div>
</button>
{/if}
</div>
</div>
{/each}
{#each peopleToAdd as face, index}
<div class="relative h-[115px] w-[95px]">
<div class="absolute top-0 left-1/2 transform -translate-x-1/2 h-[90px] w-[90px]">
<ImageThumbnail
curve
shadow
url={typeof face === 'string' ? face : api.getPeopleThumbnailUrl(face.id)}
altText="Unassigned face"
title="TO DO"
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
/>
</div>
<div
transition:blur={{ amount: 10, duration: 50 }}
class="absolute -right-[5px] -top-[5px] h-[20px] w-[20px] rounded-full bg-blue-700"
>
<button on:click={() => handlePersonPicker(index)} class="flex h-full w-full">
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<Icon path={mdiMinus} size={18} />
</div>
</button>
</div>
</div>
{/each}
</div>
</div>
</section>
{#if showSeletecFaces}
<section
transition:fly={{ x: 360, duration: 100, easing: linear }}
class="absolute top-0 z-[2001] h-full 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}
>
<Icon path={mdiArrowLeftThin} size="24" />
</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">
{#if isSearchingPeople}
<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;
}}
>
<Icon path={mdiMagnify} size="24" />
</button>
{:else}
<div
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"
>
<LoadingSpinner />
</div>
{/if}
<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"
>
<Icon path={mdiPlus} size="24" />
</button>
</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}
>
<Icon path={mdiArrowLeftThin} size="24" />
</button>
<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
/>
<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)}
>
<Icon path={mdiClose} size="24" />
</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">
{#each searchName == '' ? allPeople : searchedPeople as person (person.id)}
<div class="w-fit">
<button class="w-[90px]" on:click={() => handleReassignFace(person)}>
<ImageThumbnail
curve
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
title={person.name}
widthStyle="90px"
heightStyle="90px"
thumbhash={null}
/>
<p class="mt-1 truncate font-medium" title={person.name}>{person.name}</p>
</button>
</div>
{/each}
</div>
</div>
</section>
{/if}

View file

@ -0,0 +1,200 @@
<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, mdiCloseThick, 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';
let people: PersonResponseDto[] = [];
export let assetIds: string[];
export let personId: string;
const data: AssetFaceUpdateItem[] = [];
onMount(async () => {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
people = data.people;
});
for (const assetId of assetIds) {
data.push({ assetId, personId });
}
let selectedPerson: PersonResponseDto | null = null;
let disableButtons = false;
let showLoadingSpinnerCreate = false;
let showLoadingSpinnerReassign = false;
let showLoadingSpinnerUnassign = false;
let hasSelection = false;
let screenHeight: number;
let dispatch = createEventDispatcher();
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 handleUnassign = async () => {
try {
showLoadingSpinnerCreate = true;
disableButtons = true;
await api.personApi.unassignFaces({
assetFaceUpdateDto: { data },
});
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');
}
dispatch('confirm');
};
const handleCreate = async () => {
try {
showLoadingSpinnerCreate = true;
disableButtons = true;
await api.personApi.createPerson({
assetFaceUpdateDto: { data },
});
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');
}
dispatch('confirm');
};
const handleReassign = async () => {
try {
showLoadingSpinnerReassign = true;
disableButtons = true;
if (selectedPerson) {
await api.personApi.reassignFaces({
id: selectedPerson.id,
assetFaceUpdateDto: { data },
});
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'}`);
}
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">
<!-- TODO: Implement actions -->
<Button
title={'Unassign selected assets'}
size={'sm'}
disabled={disableButtons || hasSelection}
on:click={() => {
handleUnassign();
}}
>
{#if !showLoadingSpinnerUnassign}
<Icon path={mdiCloseThick} size={18} />
{:else}
<LoadingSpinner />
{/if}
<span class="ml-2"> Unassign assets</span></Button
>
<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}
<Icon path={mdiMerge} size={18} class="rotate-180" />
{: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} {screenHeight} on:select={({ detail }) => handleSelectedPerson(detail)} />
</section>
</section>
</section>

View file

@ -22,7 +22,9 @@
OBJECTS = 'smartInfo.objects',
}
const MAX_ITEMS = 12;
let MAX_ITEMS = 12;
let innerWidth: number | null = null;
const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => {
const targetField = items.find((item) => item.fieldName === field);
return targetField?.items || [];
@ -32,6 +34,7 @@
$: places = getFieldItems(data.items, Field.CITY);
$: people = data.response.people.slice(0, MAX_ITEMS);
$: hasPeople = data.response.total > 0;
$: MAX_ITEMS = innerWidth ? Math.floor(innerWidth / 112) : MAX_ITEMS;
</script>
<UserPageLayout user={data.user} title={data.meta.title}>
@ -45,19 +48,21 @@
draggable="false">View All</a
>
</div>
<div class="flex flex-row flex-wrap gap-4">
{#each people as person (person.id)}
<a href="/people/{person.id}" class="w-24 text-center">
<ImageThumbnail
circle
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
/>
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
</a>
{/each}
<div class="flex flex-row {MAX_ITEMS < 5 ? 'justify-center' : ''} flex-wrap gap-4" bind:offsetWidth={innerWidth}>
{#if innerWidth}
{#each people as person (person.id)}
<a href="/people/{person.id}" class="w-24 text-center">
<ImageThumbnail
circle
shadow
url={api.getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
/>
<p class="mt-2 text-ellipsis text-sm font-medium dark:text-white">{person.name}</p>
</a>
{/each}
{/if}
</div>
</div>
{/if}

View file

@ -30,6 +30,7 @@
import { AssetResponseDto, PersonResponseDto, TimeBucketSize, api } from '@api';
import { onMount } from 'svelte';
import type { PageData } from './$types';
import UnmergeFaceSelector from '$lib/components/faces-page/unmerge-face-selector.svelte';
import { clickOutside } from '$lib/utils/click-outside';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
@ -46,6 +47,7 @@
MERGE_FACES = 'merge-faces',
SUGGEST_MERGE = 'suggest-merge',
BIRTH_DATE = 'birth-date',
UNASSIGN_ASSETS = 'unassign-faces',
}
let assetStore = new AssetStore({
@ -88,7 +90,7 @@
if ((people.length < 20 && name.startsWith(searchWord)) || name === '') {
return;
}
const timeout = setTimeout(() => (isSearchingPeople = true), 300);
const timeout = setTimeout(() => (isSearchingPeople = true), 100);
try {
const { data } = await api.searchApi.searchPerson({ name });
people = data;
@ -158,6 +160,7 @@
personId: data.person.id,
});
previousPersonId = data.person.id;
name = data.person.name;
refreshAssetGrid = !refreshAssetGrid;
}
});
@ -198,6 +201,10 @@
viewMode = ViewMode.VIEW_ASSETS;
};
const handleReassignAssets = () => {
viewMode = ViewMode.UNASSIGN_ASSETS;
};
const updateAssetCount = async () => {
try {
const { data: statistics } = await api.personApi.getPersonStatistics({
@ -209,6 +216,12 @@
}
};
const handleUnmerge = () => {
$assetStore.removeAssets(Array.from($selectedAssets).map((a) => a.id));
assetInteractionStore.clearMultiselect();
viewMode = ViewMode.VIEW_ASSETS;
};
const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => {
const [personToMerge, personToBeMergedIn] = response;
viewMode = ViewMode.VIEW_ASSETS;
@ -267,7 +280,7 @@
if (viewMode === ViewMode.SUGGEST_MERGE) {
return;
}
isSearchingPeople = false;
isEditingName = false;
};
@ -351,6 +364,15 @@
/>
{/if}
{#if viewMode === ViewMode.UNASSIGN_ASSETS}
<UnmergeFaceSelector
assetIds={Array.from($selectedAssets).map((a) => a.id)}
personId={data.person.id}
on:close={() => (viewMode = ViewMode.VIEW_ASSETS)}
on:confirm={handleUnmerge}
/>
{/if}
{#if viewMode === ViewMode.BIRTH_DATE}
<SetBirthDateModal
birthDate={data.person.birthDate ?? ''}
@ -377,6 +399,7 @@
<DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" />
<FavoriteAction menuItem removeFavorite={isAllFavorite} />
<ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} />
<MenuOption text="Unmerge assets" on:click={handleReassignAssets} />
</AssetSelectContextMenu>
</AssetSelectControlBar>
{:else}
@ -465,7 +488,7 @@
<div class="absolute z-[999] w-64 sm:w-96">
{#if isSearchingPeople}
<div
class="flex rounded-b-lg dark:border-immich-dark-gray place-items-center bg-gray-100 p-2 dark:bg-gray-700"
class="flex border h-14 rounded-b-lg border-gray-400 dark:border-immich-dark-gray place-items-center bg-gray-200 p-2 dark:bg-gray-700"
>
<div class="flex w-full place-items-center">
<LoadingSpinner />
@ -474,8 +497,10 @@
{:else}
{#each suggestedPeople as person, index (person.id)}
<div
class="flex border-t dark:border-immich-dark-gray place-items-center bg-gray-100 p-2 dark:bg-gray-700 {index ===
suggestedPeople.length - 1 && 'rounded-b-lg'}"
class="flex border-t border-x border-gray-400 dark:border-immich-dark-gray h-14 place-items-center bg-gray-200 p-2 dark:bg-gray-700 hover:bg-gray-300 hover:dark:bg-[#232932] {index ===
suggestedPeople.length - 1
? 'rounded-b-lg border-b'
: ''}"
>
<button class="flex w-full place-items-center" on:click={() => handleSuggestPeople(person)}>
<ImageThumbnail