feat: unmerge people

This commit is contained in:
martabal 2023-09-21 00:45:16 +02:00
parent acdc66413c
commit c30a5db8f4
No known key found for this signature in database
GPG key ID: C00196E3148A52BD
22 changed files with 696 additions and 41 deletions

View file

@ -3663,6 +3663,25 @@ export const TranscodePolicy = {
export type TranscodePolicy = typeof TranscodePolicy[keyof typeof TranscodePolicy];
/**
*
* @export
* @interface UnMergePersonDto
*/
export interface UnMergePersonDto {
/**
*
* @type {string}
* @memberof UnMergePersonDto
*/
'assetId': string;
/**
*
* @type {string}
* @memberof UnMergePersonDto
*/
'personId': string;
}
/**
*
* @export
@ -11080,6 +11099,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
options: localVarRequestOptions,
};
},
/**
*
* @param {UnMergePersonDto} unMergePersonDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unMergePerson: async (unMergePersonDto: UnMergePersonDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'unMergePersonDto' is not null or undefined
assertParamExists('unMergePerson', 'unMergePersonDto', unMergePersonDto)
const localVarPath = `/person/unmerge`;
// 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(unMergePersonDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -11233,6 +11296,16 @@ export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {UnMergePersonDto} unMergePersonDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async unMergePerson(unMergePersonDto: UnMergePersonDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<BulkIdResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.unMergePerson(unMergePersonDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -11309,6 +11382,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUnMergePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unMergePerson(requestParameters: PersonApiUnMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<BulkIdResponseDto> {
return localVarFp.unMergePerson(requestParameters.unMergePersonDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
@ -11407,6 +11489,20 @@ export interface PersonApiMergePersonRequest {
readonly mergePersonDto: MergePersonDto
}
/**
* Request parameters for unMergePerson operation in PersonApi.
* @export
* @interface PersonApiUnMergePersonRequest
*/
export interface PersonApiUnMergePersonRequest {
/**
*
* @type {UnMergePersonDto}
* @memberof PersonApiUnMergePerson
*/
readonly unMergePersonDto: UnMergePersonDto
}
/**
* Request parameters for updatePeople operation in PersonApi.
* @export
@ -11504,6 +11600,17 @@ export class PersonApi extends BaseAPI {
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUnMergePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public unMergePerson(requestParameters: PersonApiUnMergePersonRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).unMergePerson(requestParameters.unMergePersonDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.

View file

@ -138,6 +138,7 @@ doc/TimeBucketSize.md
doc/ToneMapping.md
doc/TranscodeHWAccel.md
doc/TranscodePolicy.md
doc/UnMergePersonDto.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateLibraryDto.md
@ -295,6 +296,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/un_merge_person_dto.dart
lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart
lib/model/update_library_dto.dart
@ -441,6 +443,7 @@ test/time_bucket_size_test.dart
test/tone_mapping_test.dart
test/transcode_hw_accel_test.dart
test/transcode_policy_test.dart
test/un_merge_person_dto_test.dart
test/update_album_dto_test.dart
test/update_asset_dto_test.dart
test/update_library_dto_test.dart

View file

@ -146,6 +146,7 @@ Class | Method | HTTP request | Description
*PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
*PersonApi* | [**unMergePerson**](doc//PersonApi.md#unmergeperson) | **POST** /person/unmerge |
*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 |
@ -311,6 +312,7 @@ Class | Method | HTTP request | Description
- [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md)
- [UnMergePersonDto](doc//UnMergePersonDto.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)

View file

@ -14,6 +14,7 @@ Method | HTTP request | Description
[**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
[**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
[**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
[**unMergePerson**](PersonApi.md#unmergeperson) | **POST** /person/unmerge |
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} |
@ -295,6 +296,61 @@ 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)
# **unMergePerson**
> BulkIdResponseDto unMergePerson(unMergePersonDto)
### 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 unMergePersonDto = UnMergePersonDto(); // UnMergePersonDto |
try {
final result = api_instance.unMergePerson(unMergePersonDto);
print(result);
} catch (e) {
print('Exception when calling PersonApi->unMergePerson: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**unMergePersonDto** | [**UnMergePersonDto**](UnMergePersonDto.md)| |
### Return type
[**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)

16
mobile/openapi/doc/UnMergePersonDto.md generated Normal file
View file

@ -0,0 +1,16 @@
# openapi.model.UnMergePersonDto
## 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

@ -165,6 +165,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/un_merge_person_dto.dart';
part 'model/update_album_dto.dart';
part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart';

View file

@ -269,6 +269,53 @@ class PersonApi {
return null;
}
/// Performs an HTTP 'POST /person/unmerge' operation and returns the [Response].
/// Parameters:
///
/// * [UnMergePersonDto] unMergePersonDto (required):
Future<Response> unMergePersonWithHttpInfo(UnMergePersonDto unMergePersonDto,) async {
// ignore: prefer_const_declarations
final path = r'/person/unmerge';
// ignore: prefer_final_locals
Object? postBody = unMergePersonDto;
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:
///
/// * [UnMergePersonDto] unMergePersonDto (required):
Future<BulkIdResponseDto?> unMergePerson(UnMergePersonDto unMergePersonDto,) async {
final response = await unMergePersonWithHttpInfo(unMergePersonDto,);
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), 'BulkIdResponseDto',) as BulkIdResponseDto;
}
return null;
}
/// Performs an HTTP 'PUT /person' operation and returns the [Response].
/// Parameters:
///

View file

@ -421,6 +421,8 @@ class ApiClient {
return TranscodeHWAccelTypeTransformer().decode(value);
case 'TranscodePolicy':
return TranscodePolicyTypeTransformer().decode(value);
case 'UnMergePersonDto':
return UnMergePersonDto.fromJson(value);
case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value);
case 'UpdateAssetDto':

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

@ -42,6 +42,11 @@ void main() {
// TODO
});
//Future<BulkIdResponseDto> unMergePerson(UnMergePersonDto unMergePersonDto) async
test('test unMergePerson', () async {
// TODO
});
//Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
test('test updatePeople', () 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 UnMergePersonDto
void main() {
// final instance = UnMergePersonDto();
group('test UnMergePersonDto', () {
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
// String personId
test('to test the property `personId`', () async {
// TODO
});
});
}

View file

@ -3189,6 +3189,48 @@
]
}
},
"/person/unmerge": {
"post": {
"operationId": "unMergePerson",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnMergePersonDto"
}
}
},
"required": true
},
"responses": {
"201": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/BulkIdResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Person"
]
}
},
"/person/{id}": {
"get": {
"operationId": "getPerson",
@ -7967,6 +8009,21 @@
],
"type": "string"
},
"UnMergePersonDto": {
"properties": {
"assetId": {
"type": "string"
},
"personId": {
"type": "string"
}
},
"required": [
"assetId",
"personId"
],
"type": "object"
},
"UpdateAlbumDto": {
"properties": {
"albumName": {

View file

@ -58,6 +58,15 @@ export class MergePersonDto {
ids!: string[];
}
export class UnMergePersonDto {
@IsString()
@IsNotEmpty()
assetId!: string;
@IsString()
@IsNotEmpty()
personId!: string;
}
export class PersonSearchDto {
@IsBoolean()
@Transform(toBoolean)

View file

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

View file

@ -13,6 +13,7 @@ import {
PersonResponseDto,
PersonSearchDto,
PersonUpdateDto,
UnMergePersonDto,
mapPerson,
} from './person.dto';
import { IPersonRepository, UpdateFacesData } from './person.repository';
@ -147,6 +148,56 @@ export class PersonService {
return true;
}
async unMergePerson(authUser: AuthUserDto, dto: UnMergePersonDto): Promise<BulkIdResponseDto> {
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, dto.personId);
const oldPerson = await this.findOrFail(dto.personId);
const newPerson = await this.repository.create({ ownerId: authUser.id });
let result: BulkIdResponseDto;
try {
const mergePerson = await this.repository.getById(oldPerson.id);
if (!mergePerson) {
result = { id: oldPerson.id, success: false, error: BulkIdErrorReason.NOT_FOUND };
}
this.logger.log(`un-merging ${dto.assetId} from ${oldPerson.id}`);
await this.repository.removeFaceFromPerson(oldPerson.id, newPerson.id, dto.assetId);
await this.repository.update({
id: newPerson.id,
faceAssetId: dto.assetId,
});
const assetId = dto.assetId;
const face = await this.repository.getFaceById({ personId: newPerson.id, assetId });
if (!face) {
throw new BadRequestException('Invalid assetId for feature face');
}
await this.jobRepository.queue({
name: JobName.GENERATE_FACE_THUMBNAIL,
data: {
personId: newPerson.id,
assetId: dto.assetId,
boundingBox: {
x1: face.boundingBoxX1,
x2: face.boundingBoxX2,
y1: face.boundingBoxY1,
y2: face.boundingBoxY2,
},
imageHeight: face.imageHeight,
imageWidth: face.imageWidth,
},
});
result = { id: oldPerson.id, success: true };
} catch (error: Error | any) {
this.logger.error(`Unable to un-merge asset ${dto.assetId} from ${oldPerson.id}`, error?.stack);
result = { id: oldPerson.id, success: false, error: BulkIdErrorReason.UNKNOWN };
}
return result;
}
async mergePerson(authUser: AuthUserDto, id: string, dto: MergePersonDto): Promise<BulkIdResponseDto[]> {
const mergeIds = dto.ids;
await this.access.requirePermission(authUser, Permission.PERSON_WRITE, id);

View file

@ -10,6 +10,7 @@ import {
PersonSearchDto,
PersonService,
PersonUpdateDto,
UnMergePersonDto,
} from '@app/domain';
import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
@ -28,6 +29,11 @@ function asStreamableFile({ stream, type, length }: ImmichReadStream) {
export class PersonController {
constructor(private service: PersonService) {}
@Post('/unmerge')
unMergePerson(@AuthUser() authUser: AuthUserDto, @Body() dto: UnMergePersonDto): Promise<BulkIdResponseDto> {
return this.service.unMergePerson(authUser, dto);
}
@Get()
getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(authUser, withHidden);

View file

@ -40,6 +40,18 @@ export class PersonRepository implements IPersonRepository {
return result.affected ?? 0;
}
async removeFaceFromPerson(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;
}
delete(entity: PersonEntity): Promise<PersonEntity | null> {
return this.personRepository.remove(entity);
}

View file

@ -9,6 +9,8 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
getAssets: jest.fn(),
getAllWithoutFaces: jest.fn(),
removeFaceFromPerson: jest.fn(),
create: jest.fn(),
update: jest.fn(),
deleteAll: jest.fn(),

View file

@ -3663,6 +3663,25 @@ export const TranscodePolicy = {
export type TranscodePolicy = typeof TranscodePolicy[keyof typeof TranscodePolicy];
/**
*
* @export
* @interface UnMergePersonDto
*/
export interface UnMergePersonDto {
/**
*
* @type {string}
* @memberof UnMergePersonDto
*/
'assetId': string;
/**
*
* @type {string}
* @memberof UnMergePersonDto
*/
'personId': string;
}
/**
*
* @export
@ -11080,6 +11099,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
options: localVarRequestOptions,
};
},
/**
*
* @param {UnMergePersonDto} unMergePersonDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unMergePerson: async (unMergePersonDto: UnMergePersonDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'unMergePersonDto' is not null or undefined
assertParamExists('unMergePerson', 'unMergePersonDto', unMergePersonDto)
const localVarPath = `/person/unmerge`;
// 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(unMergePersonDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -11233,6 +11296,16 @@ export const PersonApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {UnMergePersonDto} unMergePersonDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async unMergePerson(unMergePersonDto: UnMergePersonDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<BulkIdResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.unMergePerson(unMergePersonDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {PeopleUpdateDto} peopleUpdateDto
@ -11309,6 +11382,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUnMergePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
unMergePerson(requestParameters: PersonApiUnMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<BulkIdResponseDto> {
return localVarFp.unMergePerson(requestParameters.unMergePersonDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
@ -11407,6 +11489,20 @@ export interface PersonApiMergePersonRequest {
readonly mergePersonDto: MergePersonDto
}
/**
* Request parameters for unMergePerson operation in PersonApi.
* @export
* @interface PersonApiUnMergePersonRequest
*/
export interface PersonApiUnMergePersonRequest {
/**
*
* @type {UnMergePersonDto}
* @memberof PersonApiUnMergePerson
*/
readonly unMergePersonDto: UnMergePersonDto
}
/**
* Request parameters for updatePeople operation in PersonApi.
* @export
@ -11504,6 +11600,17 @@ export class PersonApi extends BaseAPI {
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUnMergePersonRequest} requestParameters Request parameters.
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof PersonApi
*/
public unMergePerson(requestParameters: PersonApiUnMergePersonRequest, options?: AxiosRequestConfig) {
return PersonApiFp(this.configuration).unMergePerson(requestParameters.unMergePersonDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.

View file

@ -6,7 +6,6 @@
import { createEventDispatcher } from 'svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import AlertOutline from 'svelte-material-icons/AlertOutline.svelte';
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
@ -47,6 +46,7 @@
asProfileImage: void;
runJob: AssetJobName;
playSlideShow: void;
unMergePerson: void;
}>();
let contextMenuPosition = { x: 0, y: 0 };
@ -75,14 +75,6 @@
<CircleIconButton isOpacity={true} logo={ArrowLeft} on:click={() => dispatch('goBack')} />
</div>
<div class="flex w-[calc(100%-3rem)] justify-end gap-2 overflow-hidden text-white">
{#if asset.isOffline}
<CircleIconButton
isOpacity={true}
logo={AlertOutline}
on:click={() => dispatch('showDetail')}
title="Asset Offline"
/>
{/if}
{#if showMotionPlayButton}
{#if isMotionPhotoPlaying}
<CircleIconButton
@ -143,9 +135,7 @@
{/if}
{#if isOwner}
{#if !asset.isReadOnly && !asset.isExternal}
<CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
{/if}
<CircleIconButton isOpacity={true} logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
<div use:clickOutside on:outclick={() => (isShowAssetOptions = false)}>
<CircleIconButton isOpacity={true} logo={DotsVertical} on:click={showOptionsMenu} title="More" />
{#if isShowAssetOptions}
@ -162,6 +152,7 @@
text={asset.isArchived ? 'Unarchive' : 'Archive'}
/>
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
<MenuOption on:click={() => dispatch('unMergePerson')} text="Unmerge person" />
<MenuOption
on:click={() => onJobClick(AssetJobName.RefreshMetadata)}
text={api.getAssetJobName(AssetJobName.RefreshMetadata)}

View file

@ -1,6 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { AlbumResponseDto, api, AssetJobName, AssetResponseDto, AssetTypeEnum, SharedLinkResponseDto } from '@api';
import {
AlbumResponseDto,
api,
AssetJobName,
AssetResponseDto,
AssetTypeEnum,
PersonResponseDto,
SharedLinkResponseDto,
} from '@api';
import { createEventDispatcher, onDestroy, onMount } from 'svelte';
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
@ -28,6 +36,8 @@
import Close from 'svelte-material-icons/Close.svelte';
import ProgressBar, { ProgressBarStatus } from '../shared-components/progress-bar/progress-bar.svelte';
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto;
@ -50,9 +60,12 @@
let addToSharedAlbum = true;
let shouldPlayMotionPhoto = false;
let isShowProfileImageCrop = false;
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
let showUnMergeModal = false;
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : true;
let canCopyImagesToClipboard: boolean;
$: people = asset.people || [];
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo);
onMount(async () => {
@ -89,6 +102,27 @@
}
};
const handleUnMergePerson = async (person: PersonResponseDto) => {
try {
const { data } = await api.personApi.unMergePerson({
unMergePersonDto: { assetId: asset.id, personId: person.id },
});
if (data.success) {
notificationController.show({
type: NotificationType.Info,
message: `Asset un-merged from ${person.name ? person.name : person.id}`,
});
}
people = people.filter((item) => item.id !== person.id);
if (people.length < 1) {
showUnMergeModal = false;
}
} catch (error) {
handleError(error, `Unable to un-merge asset for person ${person.name ? person.name : person.id}`);
}
};
const handleKeyboardPress = (event: KeyboardEvent) => {
if (shouldIgnoreShortcut(event)) {
return;
@ -382,9 +416,43 @@
on:asProfileImage={() => (isShowProfileImageCrop = true)}
on:runJob={({ detail: job }) => handleRunJob(job)}
on:playSlideShow={handlePlaySlideshow}
on:unMergePerson={() => (showUnMergeModal = true)}
/>
{/if}
</div>
{#if showUnMergeModal}
<FullScreenModal on:clickOutside={() => (showUnMergeModal = false)}>
<div
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<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)}
<button class="w-[90px]" on:click={() => handleUnMergePerson(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>
{/each}
</div>
</section>
</div>
</div>
</FullScreenModal>
{/if}
{#if !isSlideshowMode && showNavigation}
<div class="column-span-1 z-[999] col-start-1 row-span-1 row-start-2 mb-[60px] justify-self-start">

View file

@ -25,10 +25,6 @@
$: isOwner = $page?.data?.user?.id === asset.ownerId;
$: {
if (textarea) {
textarea.value = asset?.exifInfo?.description || '';
}
// Get latest description from server
if (asset.id && !api.isSharedLink) {
api.assetApi.getAssetById({ id: asset.id }).then((res) => {
@ -101,20 +97,6 @@
<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>
</div>
</div>
</section>
{/if}
<section class="mx-4 mt-10" style:display={!isOwner && textarea?.value == '' ? 'none' : 'block'}>
<textarea
bind:this={textarea}
@ -170,16 +152,8 @@
{/if}
<div class="px-4 py-4">
{#if !asset.exifInfo && !asset.isExternal}
{#if !asset.exifInfo}
<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}