refactor: asset face queries (#32679)
(cherry picked from commit 5077a85d5e6775c48b38de7fcf25f2a3864b6a83)
This commit is contained in:
+2
-2
@@ -93,7 +93,7 @@
|
||||
@layer utilities {
|
||||
@font-face {
|
||||
font-family: 'GoogleSans';
|
||||
src: url('$lib/assets/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
|
||||
src: url('#lib/assets/fonts/GoogleSans/GoogleSans.ttf') format('truetype-variations');
|
||||
font-weight: 410 900;
|
||||
font-style: normal;
|
||||
ascent-override: 106.25%;
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
@font-face {
|
||||
font-family: 'GoogleSansCode';
|
||||
src: url('$lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
|
||||
src: url('#lib/assets/fonts/GoogleSansCode/GoogleSansCode.ttf') format('truetype-variations');
|
||||
font-weight: 1 900;
|
||||
font-style: monospace;
|
||||
}
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
import ImageThumbnail from '#lib/components/assets/thumbnail/ImageThumbnail.svelte';
|
||||
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
|
||||
import FaceCreateTagModal from '#lib/modals/CreateFaceModal.svelte';
|
||||
import { executeMutation } from '#lib/queries/client.ts';
|
||||
import { createFaceMutation } from '#lib/queries/face.query.ts';
|
||||
import { faceManager } from '#lib/stores/face.svelte.ts';
|
||||
import { getPeopleThumbnailUrl } from '#lib/utils.ts';
|
||||
import { getNaturalSize, scaleToFit } from '#lib/utils/container-utils.ts';
|
||||
import { handleError } from '#lib/utils/handle-error.ts';
|
||||
import { normalizeSearchString } from '#lib/utils/string-utils.ts';
|
||||
import { createFace, getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
||||
import { getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Button, Input, modalManager, toastManager } from '@immich/ui';
|
||||
import { Canvas, InteractiveFabricObject, Rect } from 'fabric';
|
||||
import { clamp } from 'lodash-es';
|
||||
@@ -304,38 +306,29 @@
|
||||
};
|
||||
|
||||
const tagFace = async (person: PersonResponseDto) => {
|
||||
try {
|
||||
const data = getFaceCroppedCoordinates();
|
||||
if (!data) {
|
||||
toastManager.warning($t('error_tag_face_bounding_box'));
|
||||
return;
|
||||
}
|
||||
const data = getFaceCroppedCoordinates();
|
||||
if (!data) {
|
||||
toastManager.warning($t('error_tag_face_bounding_box'));
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: person.name
|
||||
? $t('confirm_tag_face', { values: { name: person.name } })
|
||||
: $t('confirm_tag_face_unnamed'),
|
||||
});
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: person.name ? $t('confirm_tag_face', { values: { name: person.name } }) : $t('confirm_tag_face_unnamed'),
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await createFace({
|
||||
assetFaceCreateDto: {
|
||||
assetId,
|
||||
personId: person.id,
|
||||
...data,
|
||||
},
|
||||
});
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onSuccess = async () => {
|
||||
await assetViewerManager.setAssetId(assetId);
|
||||
faceManager.clear();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
handleError(error, 'Error tagging face');
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
await executeMutation(createFaceMutation, [{ assetFaceCreateDto: { assetId, personId: person.id, ...data } }], {
|
||||
onSuccess: () => void onSuccess(),
|
||||
onSettled: onClose,
|
||||
});
|
||||
};
|
||||
|
||||
const showCreateFaceModal = async () => {
|
||||
|
||||
@@ -5,22 +5,16 @@
|
||||
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
|
||||
import { eventManager } from '#lib/managers/event-manager.svelte.ts';
|
||||
import { languageManager } from '#lib/managers/language-manager.svelte.ts';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '#lib/utils.ts';
|
||||
import { executeMutation } from '#lib/queries/client.ts';
|
||||
import { deleteFaceMutation, getFacesQuery, reassignFacesByIdMutation } from '#lib/queries/face.query.ts';
|
||||
import { getPeopleThumbnailUrl } from '#lib/utils.ts';
|
||||
import { handleError } from '#lib/utils/handle-error.ts';
|
||||
import { zoomImageToBase64 } from '#lib/utils/people-utils.ts';
|
||||
import { getPersonNameWithHiddenValue } from '#lib/utils/person.ts';
|
||||
import {
|
||||
AssetTypeEnum,
|
||||
createPerson,
|
||||
deleteFace,
|
||||
getFaces,
|
||||
reassignFacesById,
|
||||
type AssetFaceResponseDto,
|
||||
type PersonResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { AssetTypeEnum, createPerson, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||
import { Icon, IconButton, LoadingSpinner, modalManager, toastManager } from '@immich/ui';
|
||||
import { mdiAccountOff, mdiArrowLeftThin, mdiArrowRightThin, mdiPencil, mdiRestart, mdiTrashCan } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { createQuery } from '@tanstack/svelte-query';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { linear } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
@@ -40,15 +34,14 @@
|
||||
let peopleToCreate: string[] = [];
|
||||
let assetFaceGenerated: string[] = [];
|
||||
|
||||
// faces
|
||||
let peopleWithFaces: AssetFaceResponseDto[] = $state([]);
|
||||
const query = createQuery(() => getFacesQuery(assetId));
|
||||
const peopleWithFaces = $derived(query.data ?? []);
|
||||
let selectedPersonToReassign: Record<string, PersonResponseDto> = $state({});
|
||||
let selectedPersonToCreate: Record<string, string> = $state({});
|
||||
let editedFace: AssetFaceResponseDto | undefined = $state();
|
||||
|
||||
// loading spinners
|
||||
let isShowLoadingDone = $state(false);
|
||||
let isShowLoadingPeople = $state(false);
|
||||
|
||||
// search people
|
||||
let showSelectedFaces = $state(false);
|
||||
@@ -59,18 +52,6 @@
|
||||
|
||||
const thumbnailWidth = '90px';
|
||||
|
||||
async function loadPeople() {
|
||||
const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
|
||||
try {
|
||||
peopleWithFaces = await getFaces({ id: assetId });
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.cant_get_faces'));
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
isShowLoadingPeople = false;
|
||||
}
|
||||
|
||||
const onPersonThumbnailReady = ({ id }: { id: string }) => {
|
||||
assetFaceGenerated.push(id);
|
||||
if (
|
||||
@@ -85,10 +66,6 @@
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
handlePromiseError(loadPeople());
|
||||
});
|
||||
|
||||
const isEqual = (a: string[], b: string[]): boolean => {
|
||||
return b.every((valueB) => a.includes(valueB));
|
||||
};
|
||||
@@ -107,28 +84,36 @@
|
||||
const numberOfChanges = Object.keys(selectedPersonToCreate).length + Object.keys(selectedPersonToReassign).length;
|
||||
|
||||
if (numberOfChanges > 0) {
|
||||
let failed = false;
|
||||
try {
|
||||
for (const personWithFace of peopleWithFaces) {
|
||||
const personId = selectedPersonToReassign[personWithFace.id]?.id;
|
||||
let personId = selectedPersonToReassign[personWithFace.id]?.id;
|
||||
|
||||
if (personId) {
|
||||
await reassignFacesById({
|
||||
id: personId,
|
||||
faceDto: { id: personWithFace.id },
|
||||
});
|
||||
} else if (Object.hasOwn(selectedPersonToCreate, personWithFace.id)) {
|
||||
if (!personId && Object.hasOwn(selectedPersonToCreate, personWithFace.id)) {
|
||||
const data = await createPerson({ personCreateDto: {} });
|
||||
peopleToCreate.push(data.id);
|
||||
await reassignFacesById({
|
||||
id: data.id,
|
||||
faceDto: { id: personWithFace.id },
|
||||
});
|
||||
personId = data.id;
|
||||
}
|
||||
|
||||
if (!personId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await executeMutation(reassignFacesByIdMutation, [{ id: personId, faceDto: { id: personWithFace.id } }], {
|
||||
onError: () => (failed = true),
|
||||
});
|
||||
|
||||
if (failed) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
toastManager.primary($t('people_edits_count', { values: { count: numberOfChanges } }));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.cant_apply_changes'));
|
||||
failed = true;
|
||||
}
|
||||
|
||||
if (!failed) {
|
||||
toastManager.primary($t('people_edits_count', { values: { count: numberOfChanges } }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,28 +146,24 @@
|
||||
};
|
||||
|
||||
const deleteAssetFace = async (face: AssetFaceResponseDto) => {
|
||||
try {
|
||||
if (!face.person) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('confirm_delete_face', { values: { name: face.person.name } }),
|
||||
});
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteFace({ id: face.id, assetFaceDeleteDto: { force: false } });
|
||||
|
||||
eventManager.emit('PersonAssetDelete', { id: face.person.id, assetId });
|
||||
|
||||
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
|
||||
|
||||
await assetViewerManager.setAssetId(assetId);
|
||||
} catch (error) {
|
||||
handleError(error, $t('error_delete_face'));
|
||||
const person = face.person;
|
||||
if (!person) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isConfirmed = await modalManager.showDialog({
|
||||
prompt: $t('confirm_delete_face', { values: { name: person.name } }),
|
||||
});
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await executeMutation(deleteFaceMutation, [{ id: face.id, assetFaceDeleteDto: { force: false } }], {
|
||||
onSuccess: () => {
|
||||
eventManager.emit('PersonAssetDelete', { id: person.id, assetId });
|
||||
void assetViewerManager.setAssetId(assetId);
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -232,7 +213,7 @@
|
||||
|
||||
<div class="p-4 text-sm">
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{#if isShowLoadingPeople}
|
||||
{#if query.isPending}
|
||||
<div class="flex w-full justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { assetViewerManager } from '#lib/managers/asset-viewer-manager.svelte.ts';
|
||||
import { executeMutation } from '#lib/queries/client.ts';
|
||||
import { createFaceMutation } from '#lib/queries/face.query.ts';
|
||||
import { delay } from '#lib/utils/asset-utils.ts';
|
||||
import { handleError } from '#lib/utils/handle-error.ts';
|
||||
import { createFace, createPerson } from '@immich/sdk';
|
||||
import { createPerson } from '@immich/sdk';
|
||||
import { Field, FormModal, Input, LoadingSpinner, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
@@ -30,36 +32,30 @@
|
||||
return;
|
||||
}
|
||||
|
||||
isSubmitting = true;
|
||||
|
||||
let personId: string;
|
||||
try {
|
||||
isSubmitting = true;
|
||||
|
||||
const person = await createPerson({
|
||||
personCreateDto: {
|
||||
name,
|
||||
},
|
||||
});
|
||||
|
||||
await createFace({
|
||||
assetFaceCreateDto: {
|
||||
assetId,
|
||||
imageHeight,
|
||||
imageWidth,
|
||||
personId: person.id,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
},
|
||||
});
|
||||
const person = await createPerson({ personCreateDto: { name } });
|
||||
personId = person.id;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
isSubmitting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const onSuccess = async () => {
|
||||
await delay(1500);
|
||||
await assetViewerManager.setAssetId(assetId);
|
||||
onClose(true);
|
||||
} catch (error) {
|
||||
handleError(error, 'Error creating and tagging face');
|
||||
} finally {
|
||||
isSubmitting = false;
|
||||
}
|
||||
};
|
||||
|
||||
const assetFaceCreateDto = { assetId, imageHeight, imageWidth, personId, x, y, width, height };
|
||||
await executeMutation(createFaceMutation, [{ assetFaceCreateDto }], {
|
||||
onSuccess: () => void onSuccess(),
|
||||
onError: () => (isSubmitting = false),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { asMutation, queryClient } from '#lib/queries/client.ts';
|
||||
import { createFace, deleteFace, getFaces, reassignFacesById, type AssetFaceResponseDto } from '@immich/sdk';
|
||||
import { queryOptions } from '@tanstack/svelte-query';
|
||||
|
||||
export const getFacesQuery = (assetId: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['faces', assetId],
|
||||
queryFn: ({ signal }) => getFaces({ id: assetId }, { signal }),
|
||||
});
|
||||
|
||||
export const createFaceMutation = () =>
|
||||
asMutation(createFace, {
|
||||
onSuccess: (_, [{ assetFaceCreateDto }]) =>
|
||||
queryClient.invalidateQueries({ queryKey: getFacesQuery(assetFaceCreateDto.assetId).queryKey }),
|
||||
});
|
||||
|
||||
export const deleteFaceMutation = () =>
|
||||
asMutation(deleteFace, {
|
||||
onSuccess: (_, [{ id }]) =>
|
||||
queryClient.setQueriesData<AssetFaceResponseDto[]>({ queryKey: ['faces'] }, (faces) =>
|
||||
faces?.filter((face) => face.id !== id),
|
||||
),
|
||||
meta: { error: { toast: 'error_delete_face' } },
|
||||
});
|
||||
|
||||
export const reassignFacesByIdMutation = () =>
|
||||
asMutation(reassignFacesById, {
|
||||
onSuccess: (person, [{ faceDto }]) =>
|
||||
queryClient.setQueriesData<AssetFaceResponseDto[]>({ queryKey: ['faces'] }, (faces) =>
|
||||
faces?.map((face) => (face.id === faceDto.id ? { ...face, person } : face)),
|
||||
),
|
||||
meta: { error: { toast: 'errors.cant_apply_changes' } },
|
||||
});
|
||||
Reference in New Issue
Block a user