chore(web): migration svelte 5 syntax (#13883)

This commit is contained in:
Alex
2024-11-14 08:43:25 -06:00
committed by GitHub
parent 9203a61709
commit 0b3742cf13
310 changed files with 6435 additions and 4176 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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%'}

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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}