mirror of
https://github.com/immich-app/immich.git
synced 2025-12-01 09:49:53 +09:00
chore(web): migration svelte 5 syntax (#13883)
This commit is contained in:
@@ -14,24 +14,28 @@
|
||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let allPeople: PersonResponseDto[];
|
||||
export let editedFace: AssetFaceResponseDto;
|
||||
export let assetId: string;
|
||||
export let assetType: AssetTypeEnum;
|
||||
export let onClose: () => void;
|
||||
export let onCreatePerson: (featurePhoto: string | null) => void;
|
||||
export let onReassign: (person: PersonResponseDto) => void;
|
||||
interface Props {
|
||||
allPeople: PersonResponseDto[];
|
||||
editedFace: AssetFaceResponseDto;
|
||||
assetId: string;
|
||||
assetType: AssetTypeEnum;
|
||||
onClose: () => void;
|
||||
onCreatePerson: (featurePhoto: string | null) => void;
|
||||
onReassign: (person: PersonResponseDto) => void;
|
||||
}
|
||||
|
||||
let { allPeople, editedFace, assetId, assetType, onClose, onCreatePerson, onReassign }: Props = $props();
|
||||
|
||||
// loading spinners
|
||||
let isShowLoadingNewPerson = false;
|
||||
let isShowLoadingSearch = false;
|
||||
let isShowLoadingNewPerson = $state(false);
|
||||
let isShowLoadingSearch = $state(false);
|
||||
|
||||
// search people
|
||||
let searchedPeople: PersonResponseDto[] = [];
|
||||
let searchFaces = false;
|
||||
let searchName = '';
|
||||
let searchedPeople: PersonResponseDto[] = $state([]);
|
||||
let searchFaces = $state(false);
|
||||
let searchName = $state('');
|
||||
|
||||
$: showPeople = searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden);
|
||||
let showPeople = $derived(searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden));
|
||||
|
||||
const handleCreatePerson = async () => {
|
||||
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
|
||||
@@ -53,19 +57,19 @@
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
{#if !searchFaces}
|
||||
<div class="flex items-center gap-2">
|
||||
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
|
||||
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} onclick={onClose} />
|
||||
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p>
|
||||
</div>
|
||||
<div class="flex justify-end gap-2">
|
||||
<CircleIconButton
|
||||
icon={mdiMagnify}
|
||||
title={$t('search_for_existing_person')}
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
searchFaces = true;
|
||||
}}
|
||||
/>
|
||||
{#if !isShowLoadingNewPerson}
|
||||
<CircleIconButton icon={mdiPlus} title={$t('create_new_person')} on:click={handleCreatePerson} />
|
||||
<CircleIconButton icon={mdiPlus} title={$t('create_new_person')} onclick={handleCreatePerson} />
|
||||
{:else}
|
||||
<div class="flex place-content-center place-items-center">
|
||||
<LoadingSpinner />
|
||||
@@ -73,7 +77,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
|
||||
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} onclick={onClose} />
|
||||
<div class="w-full flex">
|
||||
<SearchPeople
|
||||
type="input"
|
||||
@@ -87,7 +91,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<CircleIconButton icon={mdiClose} title={$t('cancel_search')} on:click={() => (searchFaces = false)} />
|
||||
<CircleIconButton icon={mdiClose} title={$t('cancel_search')} onclick={() => (searchFaces = false)} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="px-4 py-4 text-sm">
|
||||
@@ -96,7 +100,7 @@
|
||||
{#each showPeople as person (person.id)}
|
||||
{#if !editedFace.person || person.id !== editedFace.person.id}
|
||||
<div class="w-fit">
|
||||
<button type="button" class="w-[90px]" on:click={() => onReassign(person)}>
|
||||
<button type="button" class="w-[90px]" onclick={() => onReassign(person)}>
|
||||
<div class="relative">
|
||||
<ImageThumbnail
|
||||
curve
|
||||
|
||||
@@ -5,25 +5,37 @@
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
export let name: string;
|
||||
export let suggestedPeople: PersonResponseDto[];
|
||||
export let thumbnailData: string;
|
||||
export let isSearchingPeople: boolean;
|
||||
export let onChange: (name: string) => void;
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
name: string;
|
||||
suggestedPeople: PersonResponseDto[];
|
||||
thumbnailData: string;
|
||||
isSearchingPeople: boolean;
|
||||
onChange: (name: string) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
person,
|
||||
name = $bindable(),
|
||||
suggestedPeople = $bindable(),
|
||||
thumbnailData,
|
||||
isSearchingPeople = $bindable(),
|
||||
onChange,
|
||||
}: Props = $props();
|
||||
|
||||
const onsubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
onChange(name);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex w-full h-14 place-items-center {suggestedPeople.length > 0
|
||||
? 'rounded-t-lg dark:border-immich-dark-gray'
|
||||
: 'rounded-lg'} bg-gray-100 p-2 dark:bg-gray-700"
|
||||
: 'rounded-lg'} bg-gray-100 p-2 dark:bg-gray-700 border border-gray-200 dark:border-immich-dark-gray"
|
||||
>
|
||||
<ImageThumbnail circle shadow url={thumbnailData} altText={person.name} widthStyle="2rem" heightStyle="2rem" />
|
||||
<form
|
||||
class="ml-4 flex w-full justify-between gap-16"
|
||||
autocomplete="off"
|
||||
on:submit|preventDefault={() => onChange(name)}
|
||||
>
|
||||
<form class="ml-4 flex w-full justify-between gap-16" autocomplete="off" {onsubmit}>
|
||||
<SearchPeople
|
||||
bind:searchName={name}
|
||||
bind:searchedPeopleLocal={suggestedPeople}
|
||||
|
||||
@@ -3,19 +3,31 @@
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
export let selectable = false;
|
||||
export let selected = false;
|
||||
export let thumbnailSize: number | null = null;
|
||||
export let circle = false;
|
||||
export let border = false;
|
||||
export let onClick: (person: PersonResponseDto) => void = () => {};
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
selectable?: boolean;
|
||||
selected?: boolean;
|
||||
thumbnailSize?: number | null;
|
||||
circle?: boolean;
|
||||
border?: boolean;
|
||||
onClick?: (person: PersonResponseDto) => void;
|
||||
}
|
||||
|
||||
let {
|
||||
person,
|
||||
selectable = false,
|
||||
selected = false,
|
||||
thumbnailSize = null,
|
||||
circle = false,
|
||||
border = false,
|
||||
onClick = () => {},
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="relative rounded-lg transition-all"
|
||||
on:click={() => onClick(person)}
|
||||
onclick={() => onClick(person)}
|
||||
disabled={!selectable}
|
||||
style:width={thumbnailSize ? thumbnailSize + 'px' : '100%'}
|
||||
style:height={thumbnailSize ? thumbnailSize + 'px' : '100%'}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script lang="ts" context="module">
|
||||
<script lang="ts" module>
|
||||
const enum ToggleVisibility {
|
||||
HIDE_ALL = 'hide-all',
|
||||
HIDE_UNNANEMD = 'hide-unnamed',
|
||||
@@ -24,17 +24,18 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
|
||||
export let people: PersonResponseDto[];
|
||||
export let totalPeopleCount: number;
|
||||
export let titleId: string | undefined = undefined;
|
||||
export let onClose: () => void;
|
||||
export let loadNextPage: () => void;
|
||||
interface Props {
|
||||
people: PersonResponseDto[];
|
||||
totalPeopleCount: number;
|
||||
titleId?: string | undefined;
|
||||
onClose: () => void;
|
||||
loadNextPage: () => void;
|
||||
}
|
||||
|
||||
let toggleVisibility = ToggleVisibility.SHOW_ALL;
|
||||
let showLoadingSpinner = false;
|
||||
let { people = $bindable(), totalPeopleCount, titleId = undefined, onClose, loadNextPage }: Props = $props();
|
||||
|
||||
$: personIsHidden = getPersonIsHidden(people);
|
||||
$: toggleButton = toggleButtonOptions[getNextVisibility(toggleVisibility)];
|
||||
let toggleVisibility = $state(ToggleVisibility.SHOW_ALL);
|
||||
let showLoadingSpinner = $state(false);
|
||||
|
||||
const getPersonIsHidden = (people: PersonResponseDto[]) => {
|
||||
const personIsHidden: Record<string, boolean> = {};
|
||||
@@ -44,16 +45,6 @@
|
||||
return personIsHidden;
|
||||
};
|
||||
|
||||
// svelte-ignore reactive_declaration_non_reactive_property
|
||||
// svelte-ignore reactive_declaration_module_script_dependency
|
||||
$: toggleButtonOptions = ((): Record<ToggleVisibility, { icon: string; label: string }> => {
|
||||
return {
|
||||
[ToggleVisibility.HIDE_ALL]: { icon: mdiEyeOff, label: $t('hide_all_people') },
|
||||
[ToggleVisibility.HIDE_UNNANEMD]: { icon: mdiEyeSettings, label: $t('hide_unnamed_people') },
|
||||
[ToggleVisibility.SHOW_ALL]: { icon: mdiEye, label: $t('show_all_people') },
|
||||
};
|
||||
})();
|
||||
|
||||
const getNextVisibility = (toggleVisibility: ToggleVisibility) => {
|
||||
if (toggleVisibility === ToggleVisibility.SHOW_ALL) {
|
||||
return ToggleVisibility.HIDE_UNNANEMD;
|
||||
@@ -115,6 +106,15 @@
|
||||
showLoadingSpinner = false;
|
||||
}
|
||||
};
|
||||
|
||||
let personIsHidden = $state(getPersonIsHidden(people));
|
||||
|
||||
let toggleButtonOptions: Record<ToggleVisibility, { icon: string; label: string }> = $derived({
|
||||
[ToggleVisibility.HIDE_ALL]: { icon: mdiEyeOff, label: $t('hide_all_people') },
|
||||
[ToggleVisibility.HIDE_UNNANEMD]: { icon: mdiEyeSettings, label: $t('hide_unnamed_people') },
|
||||
[ToggleVisibility.SHOW_ALL]: { icon: mdiEye, label: $t('show_all_people') },
|
||||
});
|
||||
let toggleButton = $derived(toggleButtonOptions[getNextVisibility(toggleVisibility)]);
|
||||
</script>
|
||||
|
||||
<svelte:window use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: onClose }} />
|
||||
@@ -123,7 +123,7 @@
|
||||
class="fixed top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
|
||||
<CircleIconButton title={$t('close')} icon={mdiClose} onclick={onClose} />
|
||||
<div class="flex gap-2 items-center">
|
||||
<p id={titleId} class="ml-2">{$t('show_and_hide_people')}</p>
|
||||
<p class="text-sm text-gray-400 dark:text-gray-600">({totalPeopleCount.toLocaleString($locale)})</p>
|
||||
@@ -131,11 +131,11 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-end">
|
||||
<div class="flex items-center md:mr-4">
|
||||
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} on:click={handleResetVisibility} />
|
||||
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} on:click={handleToggleVisibility} />
|
||||
<CircleIconButton title={$t('reset_people_visibility')} icon={mdiRestart} onclick={handleResetVisibility} />
|
||||
<CircleIconButton title={toggleButton.label} icon={toggleButton.icon} onclick={handleToggleVisibility} />
|
||||
</div>
|
||||
{#if !showLoadingSpinner}
|
||||
<Button on:click={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
|
||||
<Button onclick={handleSaveVisibility} size="sm" rounded="lg">{$t('done')}</Button>
|
||||
{:else}
|
||||
<LoadingSpinner />
|
||||
{/if}
|
||||
@@ -143,29 +143,31 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 mt-16">
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage} let:person let:index>
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative w-full h-full"
|
||||
on:click={() => (personIsHidden[person.id] = !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={index < 20}
|
||||
{hidden}
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
<PeopleInfiniteScroll {people} hasNextPage={true} {loadNextPage}>
|
||||
{#snippet children({ person, index })}
|
||||
{@const hidden = personIsHidden[person.id]}
|
||||
<button
|
||||
type="button"
|
||||
class="group relative w-full h-full"
|
||||
onclick={() => (personIsHidden[person.id] = !hidden)}
|
||||
aria-pressed={hidden}
|
||||
aria-label={person.name ? $t('hide_named_person', { values: { name: person.name } }) : $t('hide_person')}
|
||||
>
|
||||
<ImageThumbnail
|
||||
preload={index < 20}
|
||||
{hidden}
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
hiddenIconClass="text-white group-hover:text-black transition-colors"
|
||||
/>
|
||||
{#if person.name}
|
||||
<span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
|
||||
{person.name}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
{/snippet}
|
||||
</PeopleInfiniteScroll>
|
||||
</div>
|
||||
|
||||
@@ -19,16 +19,20 @@
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
export let onBack: () => void;
|
||||
export let onMerge: (mergedPerson: PersonResponseDto) => void;
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
onBack: () => void;
|
||||
onMerge: (mergedPerson: PersonResponseDto) => void;
|
||||
}
|
||||
|
||||
let people: PersonResponseDto[] = [];
|
||||
let selectedPeople: PersonResponseDto[] = [];
|
||||
let screenHeight: number;
|
||||
let { person = $bindable(), onBack, onMerge }: Props = $props();
|
||||
|
||||
$: hasSelection = selectedPeople.length > 0;
|
||||
$: peopleToNotShow = [...selectedPeople, person];
|
||||
let people: PersonResponseDto[] = $state([]);
|
||||
let selectedPeople: PersonResponseDto[] = $state([]);
|
||||
let screenHeight: number = $state(0);
|
||||
|
||||
let hasSelection = $derived(selectedPeople.length > 0);
|
||||
let peopleToNotShow = $derived([...selectedPeople, person]);
|
||||
|
||||
onMount(async () => {
|
||||
const data = await getAllPeople({ withHidden: false });
|
||||
@@ -96,20 +100,20 @@
|
||||
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<ControlAppBar onClose={onBack}>
|
||||
<svelte:fragment slot="leading">
|
||||
{#snippet leading()}
|
||||
{#if hasSelection}
|
||||
{$t('selected_count', { values: { count: selectedPeople.length } })}
|
||||
{:else}
|
||||
{$t('merge_people')}
|
||||
{/if}
|
||||
<div></div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
<Button size={'sm'} disabled={!hasSelection} on:click={handleMerge}>
|
||||
{/snippet}
|
||||
{#snippet trailing()}
|
||||
<Button size={'sm'} disabled={!hasSelection} onclick={handleMerge}>
|
||||
<Icon path={mdiMerge} size={18} />
|
||||
<span class="ml-2">{$t('merge')}</span></Button
|
||||
>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<section id="merge-face-selector relative">
|
||||
@@ -135,7 +139,7 @@
|
||||
title={$t('swap_merge_direction')}
|
||||
icon={mdiSwapHorizontal}
|
||||
size="24"
|
||||
on:click={handleSwapPeople}
|
||||
onclick={handleSwapPeople}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -9,14 +9,25 @@
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let personMerge1: PersonResponseDto;
|
||||
export let personMerge2: PersonResponseDto;
|
||||
export let potentialMergePeople: PersonResponseDto[];
|
||||
export let onReject: () => void;
|
||||
export let onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void;
|
||||
export let onClose: () => void;
|
||||
interface Props {
|
||||
personMerge1: PersonResponseDto;
|
||||
personMerge2: PersonResponseDto;
|
||||
potentialMergePeople: PersonResponseDto[];
|
||||
onReject: () => void;
|
||||
onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
let choosePersonToMerge = false;
|
||||
let {
|
||||
personMerge1 = $bindable(),
|
||||
personMerge2 = $bindable(),
|
||||
potentialMergePeople = $bindable(),
|
||||
onReject,
|
||||
onConfirm,
|
||||
onClose,
|
||||
}: Props = $props();
|
||||
|
||||
let choosePersonToMerge = $state(false);
|
||||
|
||||
const title = personMerge2.name;
|
||||
|
||||
@@ -43,7 +54,7 @@
|
||||
<CircleIconButton
|
||||
title={$t('swap_merge_direction')}
|
||||
icon={mdiMerge}
|
||||
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
||||
onclick={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -51,7 +62,7 @@
|
||||
type="button"
|
||||
disabled={potentialMergePeople.length === 0}
|
||||
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
if (potentialMergePeople.length > 0) {
|
||||
choosePersonToMerge = !choosePersonToMerge;
|
||||
}
|
||||
@@ -69,13 +80,13 @@
|
||||
{:else}
|
||||
<div class="grid w-full grid-cols-1 gap-2">
|
||||
<div class="px-2">
|
||||
<button type="button" on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||
<button type="button" onclick={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
|
||||
{#each potentialMergePeople as person (person.id)}
|
||||
<div class="h-24 w-24 md:h-28 md:w-28">
|
||||
<button type="button" class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
|
||||
<button type="button" class="p-2 w-full" onclick={() => changePersonToMerge(person)}>
|
||||
<ImageThumbnail
|
||||
border={true}
|
||||
circle
|
||||
@@ -83,7 +94,7 @@
|
||||
url={getPeopleThumbnailUrl(person)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
on:click={() => changePersonToMerge(person)}
|
||||
onClick={() => changePersonToMerge(person)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@@ -100,8 +111,9 @@
|
||||
<div class="flex px-4 pt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
|
||||
</div>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button fullwidth color="gray" on:click={onReject}>{$t('no')}</Button>
|
||||
<Button fullwidth on:click={() => onConfirm([personMerge1, personMerge2])}>{$t('yes')}</Button>
|
||||
</svelte:fragment>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button fullwidth color="gray" onclick={onReject}>{$t('no')}</Button>
|
||||
<Button fullwidth onclick={() => onConfirm([personMerge1, personMerge2])}>{$t('yes')}</Button>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -15,28 +15,32 @@
|
||||
import { focusOutside } from '$lib/actions/focus-outside';
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
|
||||
export let person: PersonResponseDto;
|
||||
export let preload = false;
|
||||
export let onChangeName: () => void;
|
||||
export let onSetBirthDate: () => void;
|
||||
export let onMergePeople: () => void;
|
||||
export let onHidePerson: () => void;
|
||||
interface Props {
|
||||
person: PersonResponseDto;
|
||||
preload?: boolean;
|
||||
onChangeName: () => void;
|
||||
onSetBirthDate: () => void;
|
||||
onMergePeople: () => void;
|
||||
onHidePerson: () => void;
|
||||
}
|
||||
|
||||
let showVerticalDots = false;
|
||||
let { person, preload = false, onChangeName, onSetBirthDate, onMergePeople, onHidePerson }: Props = $props();
|
||||
|
||||
let showVerticalDots = $state(false);
|
||||
</script>
|
||||
|
||||
<div
|
||||
id="people-card"
|
||||
class="relative"
|
||||
on:mouseenter={() => (showVerticalDots = true)}
|
||||
on:mouseleave={() => (showVerticalDots = false)}
|
||||
onmouseenter={() => (showVerticalDots = true)}
|
||||
onmouseleave={() => (showVerticalDots = false)}
|
||||
role="group"
|
||||
use:focusOutside={{ onFocusOut: () => (showVerticalDots = false) }}
|
||||
>
|
||||
<a
|
||||
href="{AppRoute.PEOPLE}/{person.id}?{QueryParameter.PREVIOUS_ROUTE}={AppRoute.PEOPLE}"
|
||||
draggable="false"
|
||||
on:focus={() => (showVerticalDots = true)}
|
||||
onfocus={() => (showVerticalDots = true)}
|
||||
>
|
||||
<div class="w-full h-full rounded-xl brightness-95 filter">
|
||||
<ImageThumbnail
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
<script lang="ts">
|
||||
import type { PersonResponseDto } from '@immich/sdk';
|
||||
|
||||
export let people: PersonResponseDto[];
|
||||
export let hasNextPage: boolean | undefined = undefined;
|
||||
export let loadNextPage: () => void;
|
||||
interface Props {
|
||||
people: PersonResponseDto[];
|
||||
hasNextPage?: boolean | undefined;
|
||||
loadNextPage: () => void;
|
||||
children?: import('svelte').Snippet<[{ person: PersonResponseDto; index: number }]>;
|
||||
}
|
||||
|
||||
let lastPersonContainer: HTMLElement | undefined;
|
||||
let { people, hasNextPage = undefined, loadNextPage, children }: Props = $props();
|
||||
|
||||
let lastPersonContainer: HTMLElement | undefined = $state();
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
const entry = entries.find((entry) => entry.target === lastPersonContainer);
|
||||
@@ -14,20 +19,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
$: if (lastPersonContainer) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver.observe(lastPersonContainer);
|
||||
}
|
||||
$effect(() => {
|
||||
if (lastPersonContainer) {
|
||||
intersectionObserver.disconnect();
|
||||
intersectionObserver.observe(lastPersonContainer);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="w-full grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 xl:grid-cols-7 2xl:grid-cols-9 gap-1">
|
||||
{#each people as person, index (person.id)}
|
||||
{#if hasNextPage && index === people.length - 1}
|
||||
<div bind:this={lastPersonContainer}>
|
||||
<slot {person} {index} />
|
||||
{@render children?.({ person, index })}
|
||||
</div>
|
||||
{:else}
|
||||
<slot {person} {index} />
|
||||
{@render children?.({ person, index })}
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -4,22 +4,24 @@
|
||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let screenHeight: number;
|
||||
export let people: PersonResponseDto[];
|
||||
export let peopleToNotShow: PersonResponseDto[];
|
||||
export let onSelect: (person: PersonResponseDto) => void;
|
||||
|
||||
let searchedPeopleLocal: PersonResponseDto[] = [];
|
||||
|
||||
let name = '';
|
||||
let showPeople: PersonResponseDto[];
|
||||
|
||||
$: {
|
||||
showPeople = name ? searchedPeopleLocal : people;
|
||||
showPeople = showPeople.filter(
|
||||
(person) => !peopleToNotShow.some((unselectedPerson) => unselectedPerson.id === person.id),
|
||||
);
|
||||
interface Props {
|
||||
screenHeight: number;
|
||||
people: PersonResponseDto[];
|
||||
peopleToNotShow: PersonResponseDto[];
|
||||
onSelect: (person: PersonResponseDto) => void;
|
||||
}
|
||||
|
||||
let { screenHeight, people, peopleToNotShow, onSelect }: Props = $props();
|
||||
|
||||
let searchedPeopleLocal: PersonResponseDto[] = $state([]);
|
||||
|
||||
let name = $state('');
|
||||
|
||||
const showPeople = $derived(
|
||||
(name ? searchedPeopleLocal : people).filter(
|
||||
(person) => !peopleToNotShow.some((unselectedPerson) => unselectedPerson.id === person.id),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class=" w-40 sm:w-48 md:w-96 h-14 mb-8">
|
||||
|
||||
@@ -7,16 +7,6 @@
|
||||
import { searchPerson, type PersonResponseDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let searchName: string;
|
||||
export let searchedPeopleLocal: PersonResponseDto[];
|
||||
export let type: 'searchBar' | 'input';
|
||||
export let numberPeopleToSearch: number = maximumLengthSearchPeople;
|
||||
export let inputClass: string = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg';
|
||||
export let showLoadingSpinner: boolean = false;
|
||||
export let placeholder: string = $t('name_or_nickname');
|
||||
export let onReset = () => {};
|
||||
export let onSearch = () => {};
|
||||
|
||||
let searchedPeople: PersonResponseDto[] = [];
|
||||
let searchWord: string;
|
||||
let abortController: AbortController | null = null;
|
||||
@@ -43,7 +33,36 @@
|
||||
}
|
||||
};
|
||||
|
||||
export let handleSearch = async (force?: boolean, name?: string) => {
|
||||
interface Props {
|
||||
searchName: string;
|
||||
searchedPeopleLocal: PersonResponseDto[];
|
||||
type: 'searchBar' | 'input';
|
||||
numberPeopleToSearch?: number;
|
||||
inputClass?: string;
|
||||
showLoadingSpinner?: boolean;
|
||||
placeholder?: string;
|
||||
onReset?: () => void;
|
||||
onSearch?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
searchName = $bindable(),
|
||||
searchedPeopleLocal = $bindable(),
|
||||
type,
|
||||
numberPeopleToSearch = maximumLengthSearchPeople,
|
||||
inputClass = 'w-full gap-2 bg-immich-bg dark:bg-immich-dark-bg',
|
||||
showLoadingSpinner = $bindable(false),
|
||||
placeholder = $t('name_or_nickname'),
|
||||
onReset = () => {},
|
||||
onSearch = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
const handleReset = () => {
|
||||
reset();
|
||||
onReset();
|
||||
};
|
||||
|
||||
export async function searchPeople(force?: boolean, name?: string) {
|
||||
searchName = name ?? searchName;
|
||||
onSearch();
|
||||
if (searchName === '') {
|
||||
@@ -70,12 +89,7 @@
|
||||
showLoadingSpinner = false;
|
||||
search();
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
reset();
|
||||
onReset();
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if type === 'searchBar'}
|
||||
@@ -84,7 +98,7 @@
|
||||
{showLoadingSpinner}
|
||||
{placeholder}
|
||||
onReset={handleReset}
|
||||
onSearch={({ force }) => handleSearch(force ?? false)}
|
||||
onSearch={({ force }) => searchPeople(force ?? false)}
|
||||
/>
|
||||
{:else}
|
||||
<input
|
||||
@@ -92,7 +106,7 @@
|
||||
type="text"
|
||||
{placeholder}
|
||||
bind:value={searchName}
|
||||
on:input={() => handleSearch(false)}
|
||||
oninput={() => searchPeople(false)}
|
||||
use:initInput
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -28,28 +28,32 @@
|
||||
import { photoViewer } from '$lib/stores/assets.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let assetId: string;
|
||||
export let assetType: AssetTypeEnum;
|
||||
export let onClose: () => void;
|
||||
export let onRefresh: () => void;
|
||||
interface Props {
|
||||
assetId: string;
|
||||
assetType: AssetTypeEnum;
|
||||
onClose: () => void;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
let { assetId, assetType, onClose, onRefresh }: Props = $props();
|
||||
|
||||
// keep track of the changes
|
||||
let peopleToCreate: string[] = [];
|
||||
let assetFaceGenerated: string[] = [];
|
||||
|
||||
// faces
|
||||
let peopleWithFaces: AssetFaceResponseDto[] = [];
|
||||
let selectedPersonToReassign: Record<string, PersonResponseDto> = {};
|
||||
let selectedPersonToCreate: Record<string, string> = {};
|
||||
let editedFace: AssetFaceResponseDto;
|
||||
let peopleWithFaces: AssetFaceResponseDto[] = $state([]);
|
||||
let selectedPersonToReassign: Record<string, PersonResponseDto> = $state({});
|
||||
let selectedPersonToCreate: Record<string, string> = $state({});
|
||||
let editedFace: AssetFaceResponseDto | undefined = $state();
|
||||
|
||||
// loading spinners
|
||||
let isShowLoadingDone = false;
|
||||
let isShowLoadingPeople = false;
|
||||
let isShowLoadingDone = $state(false);
|
||||
let isShowLoadingPeople = $state(false);
|
||||
|
||||
// search people
|
||||
let showSelectedFaces = false;
|
||||
let allPeople: PersonResponseDto[] = [];
|
||||
let showSelectedFaces = $state(false);
|
||||
let allPeople: PersonResponseDto[] = $state([]);
|
||||
|
||||
// timers
|
||||
let loaderLoadingDoneTimeout: ReturnType<typeof setTimeout>;
|
||||
@@ -152,14 +156,14 @@
|
||||
};
|
||||
|
||||
const handleCreatePerson = (newFeaturePhoto: string | null) => {
|
||||
if (newFeaturePhoto) {
|
||||
if (newFeaturePhoto && editedFace) {
|
||||
selectedPersonToCreate[editedFace.id] = newFeaturePhoto;
|
||||
}
|
||||
showSelectedFaces = false;
|
||||
};
|
||||
|
||||
const handleReassignFace = (person: PersonResponseDto | null) => {
|
||||
if (person) {
|
||||
if (person && editedFace) {
|
||||
selectedPersonToReassign[editedFace.id] = person;
|
||||
}
|
||||
showSelectedFaces = false;
|
||||
@@ -177,14 +181,14 @@
|
||||
>
|
||||
<div class="flex place-items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
|
||||
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} onclick={onClose} />
|
||||
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('edit_faces')}</p>
|
||||
</div>
|
||||
{#if !isShowLoadingDone}
|
||||
<button
|
||||
type="button"
|
||||
class="justify-self-end rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50"
|
||||
on:click={() => handleEditFaces()}
|
||||
onclick={() => handleEditFaces()}
|
||||
>
|
||||
{$t('done')}
|
||||
</button>
|
||||
@@ -207,9 +211,9 @@
|
||||
role="button"
|
||||
tabindex={index}
|
||||
class="absolute left-0 top-0 h-[90px] w-[90px] cursor-default"
|
||||
on:focus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
on:mouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
on:mouseleave={() => ($boundingBoxesArray = [])}
|
||||
onfocus={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
onmouseover={() => ($boundingBoxesArray = [peopleWithFaces[index]])}
|
||||
onmouseleave={() => ($boundingBoxesArray = [])}
|
||||
>
|
||||
<div class="relative">
|
||||
{#if selectedPersonToCreate[face.id]}
|
||||
@@ -291,7 +295,7 @@
|
||||
size="18"
|
||||
padding="1"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
on:click={() => handleReset(face.id)}
|
||||
onclick={() => handleReset(face.id)}
|
||||
/>
|
||||
{:else}
|
||||
<CircleIconButton
|
||||
@@ -301,7 +305,7 @@
|
||||
size="18"
|
||||
padding="1"
|
||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||
on:click={() => handleFacePicker(face)}
|
||||
onclick={() => handleFacePicker(face)}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -322,7 +326,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if showSelectedFaces}
|
||||
{#if showSelectedFaces && editedFace}
|
||||
<AssignFaceSidePanel
|
||||
{allPeople}
|
||||
{editedFace}
|
||||
|
||||
@@ -5,11 +5,20 @@
|
||||
import DateInput from '../elements/date-input.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let birthDate: string;
|
||||
export let onClose: () => void;
|
||||
export let onUpdate: (birthDate: string) => void;
|
||||
interface Props {
|
||||
birthDate: string;
|
||||
onClose: () => void;
|
||||
onUpdate: (birthDate: string) => void;
|
||||
}
|
||||
|
||||
let { birthDate = $bindable(), onClose, onUpdate }: Props = $props();
|
||||
|
||||
const todayFormatted = new Date().toISOString().split('T')[0];
|
||||
|
||||
const onSubmit = (event: Event) => {
|
||||
event.preventDefault();
|
||||
onUpdate(birthDate);
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} {onClose}>
|
||||
@@ -19,7 +28,7 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={() => onUpdate(birthDate)} autocomplete="off" id="set-birth-date-form">
|
||||
<form onsubmit={(e) => onSubmit(e)} autocomplete="off" id="set-birth-date-form">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<DateInput
|
||||
class="immich-form-input"
|
||||
@@ -31,8 +40,9 @@
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button color="gray" fullwidth on:click={onClose}>{$t('cancel')}</Button>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
<Button color="gray" fullwidth onclick={onClose}>{$t('cancel')}</Button>
|
||||
<Button type="submit" fullwidth form="set-birth-date-form">{$t('set')}</Button>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
type PersonResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { mdiMerge, mdiPlus } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
@@ -21,20 +21,26 @@
|
||||
import PeopleList from './people-list.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let assetIds: string[];
|
||||
export let personAssets: PersonResponseDto;
|
||||
export let onConfirm: () => void;
|
||||
export let onClose: () => void;
|
||||
interface Props {
|
||||
assetIds: string[];
|
||||
personAssets: PersonResponseDto;
|
||||
onConfirm: () => void;
|
||||
onClose: () => void;
|
||||
header?: Snippet;
|
||||
merge?: Snippet;
|
||||
}
|
||||
|
||||
let people: PersonResponseDto[] = [];
|
||||
let selectedPerson: PersonResponseDto | null = null;
|
||||
let disableButtons = false;
|
||||
let showLoadingSpinnerCreate = false;
|
||||
let showLoadingSpinnerReassign = false;
|
||||
let hasSelection = false;
|
||||
let screenHeight: number;
|
||||
let { assetIds, personAssets, onConfirm, onClose, header, merge }: Props = $props();
|
||||
|
||||
$: peopleToNotShow = selectedPerson ? [personAssets, selectedPerson] : [personAssets];
|
||||
let people: PersonResponseDto[] = $state([]);
|
||||
let selectedPerson: PersonResponseDto | null = $state(null);
|
||||
let disableButtons = $state(false);
|
||||
let showLoadingSpinnerCreate = $state(false);
|
||||
let showLoadingSpinnerReassign = $state(false);
|
||||
let hasSelection = $state(false);
|
||||
let screenHeight: number = $state(0);
|
||||
|
||||
let peopleToNotShow = $derived(selectedPerson ? [personAssets, selectedPerson] : [personAssets]);
|
||||
|
||||
const selectedPeople: AssetFaceUpdateItem[] = [];
|
||||
|
||||
@@ -117,17 +123,17 @@
|
||||
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
|
||||
>
|
||||
<ControlAppBar {onClose}>
|
||||
<svelte:fragment slot="leading">
|
||||
<slot name="header" />
|
||||
{#snippet leading()}
|
||||
{@render header?.()}
|
||||
<div></div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
{/snippet}
|
||||
{#snippet trailing()}
|
||||
<div class="flex gap-4">
|
||||
<Button
|
||||
title={$t('create_new_person_hint')}
|
||||
size={'sm'}
|
||||
disabled={disableButtons || hasSelection}
|
||||
on:click={handleCreate}
|
||||
onclick={handleCreate}
|
||||
>
|
||||
{#if !showLoadingSpinnerCreate}
|
||||
<Icon path={mdiPlus} size={18} />
|
||||
@@ -140,7 +146,7 @@
|
||||
size={'sm'}
|
||||
title={$t('reassing_hint')}
|
||||
disabled={disableButtons || !hasSelection}
|
||||
on:click={handleReassign}
|
||||
onclick={handleReassign}
|
||||
>
|
||||
{#if !showLoadingSpinnerReassign}
|
||||
<div>
|
||||
@@ -152,9 +158,9 @@
|
||||
<span class="ml-2"> {$t('reassign')}</span></Button
|
||||
>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{/snippet}
|
||||
</ControlAppBar>
|
||||
<slot name="merge" />
|
||||
{@render merge?.()}
|
||||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||
<section id="merge-face-selector relative">
|
||||
{#if selectedPerson !== null}
|
||||
|
||||
Reference in New Issue
Block a user