add available person

This commit is contained in:
martabal 2023-10-25 15:44:11 +02:00
parent c8d173697f
commit 9247eb4c1a
No known key found for this signature in database
GPG key ID: C00196E3148A52BD
9 changed files with 133 additions and 25 deletions

View file

@ -4061,6 +4061,12 @@ export interface UnassignedFacesResponseDto {
* @memberof UnassignedFacesResponseDto
*/
'assetFaceId': string;
/**
*
* @type {string}
* @memberof UnassignedFacesResponseDto
*/
'assetId': string;
/**
*
* @type {AssetFaceBoxDto}

View file

@ -9,6 +9,7 @@ import 'package:openapi/api.dart';
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

@ -14,30 +14,36 @@ 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, boudinxBox=$boudinxBox]';
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;
}
@ -51,6 +57,7 @@ class UnassignedFacesResponseDto {
return UnassignedFacesResponseDto(
assetFaceId: mapValueOfType<String>(json, r'assetFaceId')!,
assetId: mapValueOfType<String>(json, r'assetId')!,
boudinxBox: AssetFaceBoxDto.fromJson(json[r'boudinxBox'])!,
);
}
@ -100,6 +107,7 @@ class UnassignedFacesResponseDto {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'assetFaceId',
'assetId',
'boudinxBox',
};
}

View file

@ -21,6 +21,11 @@ void main() {
// TODO
});
// String assetId
test('to test the property `assetId`', () async {
// TODO
});
// AssetFaceBoxDto boudinxBox
test('to test the property `boudinxBox`', () async {
// TODO

View file

@ -8770,12 +8770,16 @@
"assetFaceId": {
"type": "string"
},
"assetId": {
"type": "string"
},
"boudinxBox": {
"$ref": "#/components/schemas/AssetFaceBoxDto"
}
},
"required": [
"assetFaceId",
"assetId",
"boudinxBox"
],
"type": "object"

View file

@ -117,6 +117,7 @@ export class PeopleAssetResponseDto {
export class UnassignedFacesResponseDto {
assetFaceId!: string;
assetId!: string;
boudinxBox!: AssetFaceBoxDto;
}
@ -162,6 +163,7 @@ export function mapUnassignedFace(face: AssetFaceEntity): UnassignedFacesRespons
} else {
return {
assetFaceId: face.id,
assetId: face.assetId,
boudinxBox: {
imageWidth: face.imageWidth,
imageHeight: face.imageHeight,

View file

@ -4061,6 +4061,12 @@ export interface UnassignedFacesResponseDto {
* @memberof UnassignedFacesResponseDto
*/
'assetFaceId': string;
/**
*
* @type {string}
* @memberof UnassignedFacesResponseDto
*/
'assetId': string;
/**
*
* @type {AssetFaceBoxDto}

View file

@ -413,6 +413,7 @@
{#if showEditFaces}
<PersonSidePanel
bind:people
bind:unassignedFaces
bind:selectedPersonToCreate={customFeaturePhoto}
assetId={asset.id}
on:close={() => (showEditFaces = false)}

View file

@ -11,20 +11,24 @@
import Magnify from 'svelte-material-icons/Magnify.svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
import { linear } from 'svelte/easing';
import { api, ThumbnailFormat, type PersonResponseDto } from '@api';
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 Close from 'svelte-material-icons/Close.svelte';
import { createEventDispatcher, onMount } from 'svelte';
import Minus from 'svelte-material-icons/Minus.svelte';
import Account from 'svelte-material-icons/Account.svelte';
import Restart from 'svelte-material-icons/Restart.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { browser } from '$app/environment';
export let people: PersonResponseDto[];
export let unassignedFaces: UnassignedFacesResponseDto[];
export let assetId: string;
let peopleToAdd: (PersonResponseDto | string)[] = [];
let searchedPeople: PersonResponseDto[] = [];
let searchWord: string;
let maxPeople = false;
@ -33,6 +37,7 @@
let searchFaces = false;
let searchName = '';
let isSearchingPeople = false;
let allPeople: PersonResponseDto[] = [];
let editedPerson: number;
@ -47,10 +52,15 @@
onMount(async () => {
const { data } = await api.personApi.getAllPeople({ withHidden: false });
allPeople = data.people;
peopleToAdd = await initUnassignedFaces();
});
const searchPeople = async () => {
searchedPeople = [];
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);
@ -62,26 +72,45 @@
}
} catch (error) {
handleError(error, "Can't search people");
} finally {
clearTimeout(timeout);
}
};
$: {
if (searchName !== '' && browser) {
if (maxPeople === true || (!searchName.startsWith(searchWord) && maxPeople === false)) searchPeople();
}
}
isSearchingPeople = false;
};
$: {
searchedPeople = !searchName
? allPeople
: allPeople
.filter((person: PersonResponseDto) => person.name.toLowerCase().startsWith(searchName.toLowerCase()))
.filter((person: PersonResponseDto) => {
const nameParts = person.name.split(' ');
return nameParts.some((splitName) => splitName.toLowerCase().startsWith(searchName.toLowerCase()));
})
.slice(0, 5);
}
function initInput(element: HTMLInputElement) {
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 = '';
@ -264,7 +293,7 @@
{#each editedPeople as person, index}
<div class="relative h-[115px] w-[95px]">
<a href="/people/{person.id}">
<div class="absolute left-0 top-0 h-[90px] w-[90px]">
<div class="absolute top-0 left-1/2 transform -translate-x-1/2 h-[90px] w-[90px]">
<ImageThumbnail
curve
shadow
@ -280,6 +309,16 @@
</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">
<Minus size={18} />
</div>
</button>
</div>
<div
transition:blur={{ amount: 10, duration: 50 }}
@ -293,14 +332,41 @@
</button>
{:else}
<button on:click={() => handlePersonPicker(index)} class="flex h-full w-full">
<div
class="absolute left-1/2 top-1/2 h-[2px] w-[14px] translate-x-[-50%] translate-y-[-50%] transform bg-white"
/>
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
<Account 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">
<Minus size={18} />
</div>
</button>
</div>
</div>
{/each}
</div>
</div>
</section>
@ -322,6 +388,7 @@
<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"
@ -331,6 +398,13 @@
>
<Magnify 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"
@ -352,6 +426,7 @@
type="text"
placeholder="Name or nickname"
bind:value={searchName}
on:input={() => searchPeople()}
use:initInput
/>
<button