diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index bd74c0675..91e4d2429 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -4061,6 +4061,12 @@ export interface UnassignedFacesResponseDto { * @memberof UnassignedFacesResponseDto */ 'assetFaceId': string; + /** + * + * @type {string} + * @memberof UnassignedFacesResponseDto + */ + 'assetId': string; /** * * @type {AssetFaceBoxDto} diff --git a/mobile/openapi/doc/UnassignedFacesResponseDto.md b/mobile/openapi/doc/UnassignedFacesResponseDto.md index ac122ee76..d512352f3 100644 --- a/mobile/openapi/doc/UnassignedFacesResponseDto.md +++ b/mobile/openapi/doc/UnassignedFacesResponseDto.md @@ -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) diff --git a/mobile/openapi/lib/model/unassigned_faces_response_dto.dart b/mobile/openapi/lib/model/unassigned_faces_response_dto.dart index e30a3a94b..3c6e60c23 100644 --- a/mobile/openapi/lib/model/unassigned_faces_response_dto.dart +++ b/mobile/openapi/lib/model/unassigned_faces_response_dto.dart @@ -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 toJson() { final json = {}; 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(json, r'assetFaceId')!, + assetId: mapValueOfType(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 = { 'assetFaceId', + 'assetId', 'boudinxBox', }; } diff --git a/mobile/openapi/test/unassigned_faces_response_dto_test.dart b/mobile/openapi/test/unassigned_faces_response_dto_test.dart index ad5dfde7d..2a99786ff 100644 --- a/mobile/openapi/test/unassigned_faces_response_dto_test.dart +++ b/mobile/openapi/test/unassigned_faces_response_dto_test.dart @@ -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 diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 2ddc8d632..158e91366 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -8770,12 +8770,16 @@ "assetFaceId": { "type": "string" }, + "assetId": { + "type": "string" + }, "boudinxBox": { "$ref": "#/components/schemas/AssetFaceBoxDto" } }, "required": [ "assetFaceId", + "assetId", "boudinxBox" ], "type": "object" diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index c3b6de7a1..10f4b5cde 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -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, diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index bd74c0675..91e4d2429 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -4061,6 +4061,12 @@ export interface UnassignedFacesResponseDto { * @memberof UnassignedFacesResponseDto */ 'assetFaceId': string; + /** + * + * @type {string} + * @memberof UnassignedFacesResponseDto + */ + 'assetId': string; /** * * @type {AssetFaceBoxDto} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 7e7db85a2..436fb491d 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -413,6 +413,7 @@ {#if showEditFaces} (showEditFaces = false)} diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 190e20478..f3d1f83fa 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -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 => { + 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}
-
+
+
+ +
{:else} {/if}
{/each} + {#each peopleToAdd as face, index} +
+
+ +
+ +
+ +
+
+ {/each}
@@ -322,15 +388,23 @@

Select face

- + {#if isSearchingPeople} + + {:else} +
+ +
+ {/if}