chore: migrate away from event dispatcher (#12820)

This commit is contained in:
Daniel Dietzler 2024-09-20 23:02:58 +02:00 committed by GitHub
parent 529d49471f
commit 124eb8251b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
72 changed files with 360 additions and 656 deletions

View File

@ -2,7 +2,6 @@
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk'; import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js'; import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@ -16,6 +15,9 @@
export let order: AssetOrder | undefined; export let order: AssetOrder | undefined;
export let user: UserResponseDto; export let user: UserResponseDto;
export let onChangeOrder: (order: AssetOrder) => void; export let onChangeOrder: (order: AssetOrder) => void;
export let onClose: () => void;
export let onToggleEnabledActivity: () => void;
export let onShowSelectSharedUser: () => void;
const options: Record<AssetOrder, RenderedOption> = { const options: Record<AssetOrder, RenderedOption> = {
[AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') }, [AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') },
@ -24,12 +26,6 @@
$: selectedOption = order ? options[order] : options[AssetOrder.Desc]; $: selectedOption = order ? options[order] : options[AssetOrder.Desc];
const dispatch = createEventDispatcher<{
close: void;
toggleEnableActivity: void;
showSelectSharedUser: void;
}>();
const handleToggle = async (returnedOption: RenderedOption) => { const handleToggle = async (returnedOption: RenderedOption) => {
if (selectedOption === returnedOption) { if (selectedOption === returnedOption) {
return; return;
@ -51,7 +47,7 @@
}; };
</script> </script>
<FullScreenModal title={$t('options')} onClose={() => dispatch('close')}> <FullScreenModal title={$t('options')} {onClose}>
<div class="items-center justify-center"> <div class="items-center justify-center">
<div class="py-2"> <div class="py-2">
<h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2> <h2 class="text-gray text-sm mb-2">{$t('settings').toUpperCase()}</h2>
@ -68,14 +64,14 @@
title={$t('comments_and_likes')} title={$t('comments_and_likes')}
subtitle={$t('let_others_respond')} subtitle={$t('let_others_respond')}
checked={album.isActivityEnabled} checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')} onToggle={onToggleEnabledActivity}
/> />
</div> </div>
</div> </div>
<div class="py-2"> <div class="py-2">
<div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div> <div class="text-gray text-sm mb-3">{$t('people').toUpperCase()}</div>
<div class="p-2"> <div class="p-2">
<button type="button" class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}> <button type="button" class="flex items-center gap-2" on:click={onShowSelectSharedUser}>
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center"> <div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
<div><Icon path={mdiPlus} size="25" /></div> <div><Icon path={mdiPlus} size="25" /></div>
</div> </div>

View File

@ -154,7 +154,7 @@
title={$t('sort_albums_by')} title={$t('sort_albums_by')}
options={Object.values(sortOptionsMetadata)} options={Object.values(sortOptionsMetadata)}
selectedOption={selectedSortOption} selectedOption={selectedSortOption}
on:select={({ detail }) => handleChangeSortBy(detail)} onSelect={handleChangeSortBy}
render={({ id }) => ({ render={({ id }) => ({
title: albumSortByNames[id], title: albumSortByNames[id],
icon: sortIcon, icon: sortIcon,
@ -166,7 +166,7 @@
title={$t('group_albums_by')} title={$t('group_albums_by')}
options={Object.values(groupOptionsMetadata)} options={Object.values(groupOptionsMetadata)}
selectedOption={selectedGroupOption} selectedOption={selectedGroupOption}
on:select={({ detail }) => handleChangeGroupBy(detail)} onSelect={handleChangeGroupBy}
render={({ id, isDisabled }) => ({ render={({ id, isDisabled }) => ({
title: albumGroupByNames[id], title: albumGroupByNames[id],
icon: groupIcon, icon: groupIcon,

View File

@ -394,13 +394,13 @@
<CreateSharedLinkModal <CreateSharedLinkModal
albumId={albumToShare.id} albumId={albumToShare.id}
onClose={() => closeShareModal()} onClose={() => closeShareModal()}
on:created={() => albumToShare && handleSharedLinkCreated(albumToShare)} onCreated={() => albumToShare && handleSharedLinkCreated(albumToShare)}
/> />
{:else} {:else}
<UserSelectionModal <UserSelectionModal
album={albumToShare} album={albumToShare}
on:select={({ detail: users }) => handleAddUsers(users)} onSelect={handleAddUsers}
on:share={() => (showShareByURLModal = true)} onShare={() => (showShareByURLModal = true)}
onClose={() => closeShareModal()} onClose={() => closeShareModal()}
/> />
{/if} {/if}

View File

@ -8,7 +8,7 @@
AlbumUserRole, AlbumUserRole,
} from '@immich/sdk'; } from '@immich/sdk';
import { mdiDotsVertical } from '@mdi/js'; import { mdiDotsVertical } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte'; import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte';
@ -20,11 +20,8 @@
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let onClose: () => void; export let onClose: () => void;
export let onRemove: (userId: string) => void;
const dispatch = createEventDispatcher<{ export let onRefreshAlbum: () => void;
remove: string;
refreshAlbum: void;
}>();
let currentUser: UserResponseDto; let currentUser: UserResponseDto;
let selectedRemoveUser: UserResponseDto | null = null; let selectedRemoveUser: UserResponseDto | null = null;
@ -52,7 +49,7 @@
try { try {
await removeUserFromAlbum({ id: album.id, userId }); await removeUserFromAlbum({ id: album.id, userId });
dispatch('remove', userId); onRemove(userId);
const message = const message =
userId === 'me' userId === 'me'
? $t('album_user_left', { values: { album: album.albumName } }) ? $t('album_user_left', { values: { album: album.albumName } })
@ -71,7 +68,7 @@
const message = $t('user_role_set', { const message = $t('user_role_set', {
values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') }, values: { user: user.name, role: role == AlbumUserRole.Viewer ? $t('role_viewer') : $t('role_editor') },
}); });
dispatch('refreshAlbum'); onRefreshAlbum();
notificationController.show({ type: NotificationType.Info, message }); notificationController.show({ type: NotificationType.Info, message });
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_change_album_user_role')); handleError(error, $t('errors.unable_to_change_album_user_role'));

View File

@ -13,13 +13,16 @@
type UserResponseDto, type UserResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { mdiCheck, mdiEye, mdiLink, mdiPencil, mdiShareCircle } from '@mdi/js'; import { mdiCheck, mdiEye, mdiLink, mdiPencil, mdiShareCircle } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let onClose: () => void; export let onClose: () => void;
export let onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
export let onShare: () => void;
let users: UserResponseDto[] = []; let users: UserResponseDto[] = [];
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {}; let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = {};
@ -29,10 +32,6 @@
{ title: $t('remove_user'), value: 'none' }, { title: $t('remove_user'), value: 'none' },
]; ];
const dispatch = createEventDispatcher<{
select: AlbumUserAddDto[];
share: void;
}>();
let sharedLinks: SharedLinkResponseDto[] = []; let sharedLinks: SharedLinkResponseDto[] = [];
onMount(async () => { onMount(async () => {
await getSharedLinks(); await getSharedLinks();
@ -99,7 +98,7 @@
title={$t('role')} title={$t('role')}
options={roleOptions} options={roleOptions}
render={({ title, icon }) => ({ title, icon })} render={({ title, icon }) => ({ title, icon })}
on:select={({ detail: { value } }) => handleChangeRole(user, value)} onSelect={({ value }) => handleChangeRole(user, value)}
/> />
</div> </div>
{/key} {/key}
@ -152,10 +151,8 @@
rounded="full" rounded="full"
disabled={Object.keys(selectedUsers).length === 0} disabled={Object.keys(selectedUsers).length === 0}
on:click={() => on:click={() =>
dispatch( onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
'select', >{$t('add')}</Button
Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
)}>{$t('add')}</Button
> >
</div> </div>
{/if} {/if}
@ -166,7 +163,7 @@
<button <button
type="button" type="button"
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer" class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
on:click={() => dispatch('share')} on:click={onShare}
> >
<Icon path={mdiLink} size={24} /> <Icon path={mdiLink} size={24} />
<p class="text-sm">{$t('create_link')}</p> <p class="text-sm">{$t('create_link')}</p>

View File

@ -40,8 +40,8 @@
<Portal target="body"> <Portal target="body">
<AlbumSelectionModal <AlbumSelectionModal
{shared} {shared}
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)} onNewAlbum={handleAddToNewAlbum}
on:album={({ detail }) => handleAddToAlbum(detail)} onAlbumClick={handleAddToAlbum}
onClose={() => (showSelectionModal = false)} onClose={() => (showSelectionModal = false)}
/> />
</Portal> </Portal>

View File

@ -82,6 +82,6 @@
{#if showConfirmModal} {#if showConfirmModal}
<Portal target="body"> <Portal target="body">
<DeleteAssetDialog size={1} on:cancel={() => (showConfirmModal = false)} on:confirm={() => deleteAsset()} /> <DeleteAssetDialog size={1} onCancel={() => (showConfirmModal = false)} onConfirm={deleteAsset} />
</Portal> </Portal>
{/if} {/if}

View File

@ -2,26 +2,22 @@
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import type { ActivityResponseDto } from '@immich/sdk'; import type { ActivityResponseDto } from '@immich/sdk';
import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js'; import { mdiCommentOutline, mdiHeart, mdiHeartOutline } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Icon from '../elements/icon.svelte'; import Icon from '../elements/icon.svelte';
export let isLiked: ActivityResponseDto | null; export let isLiked: ActivityResponseDto | null;
export let numberOfComments: number | undefined; export let numberOfComments: number | undefined;
export let disabled: boolean; export let disabled: boolean;
export let onOpenActivityTab: () => void;
const dispatch = createEventDispatcher<{ export let onFavorite: () => void;
openActivityTab: void;
favorite: void;
}>();
</script> </script>
<div class="w-full flex p-4 text-white items-center justify-center rounded-full gap-5 bg-immich-dark-bg bg-opacity-60"> <div class="w-full flex p-4 text-white items-center justify-center rounded-full gap-5 bg-immich-dark-bg bg-opacity-60">
<button type="button" class={disabled ? 'cursor-not-allowed' : ''} on:click={() => dispatch('favorite')} {disabled}> <button type="button" class={disabled ? 'cursor-not-allowed' : ''} on:click={onFavorite} {disabled}>
<div class="items-center justify-center"> <div class="items-center justify-center">
<Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} /> <Icon path={isLiked ? mdiHeart : mdiHeartOutline} size={24} />
</div> </div>
</button> </button>
<button type="button" on:click={() => dispatch('openActivityTab')}> <button type="button" on:click={onOpenActivityTab}>
<div class="flex gap-2 items-center justify-center"> <div class="flex gap-2 items-center justify-center">
<Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} /> <Icon path={mdiCommentOutline} class="scale-x-[-1]" size={24} />
{#if numberOfComments} {#if numberOfComments}

View File

@ -17,7 +17,7 @@
} from '@immich/sdk'; } from '@immich/sdk';
import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend, mdiDeleteOutline } from '@mdi/js'; import { mdiClose, mdiDotsVertical, mdiHeart, mdiSend, mdiDeleteOutline } from '@mdi/js';
import * as luxon from 'luxon'; import * as luxon from 'luxon';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
@ -55,6 +55,10 @@
export let albumOwnerId: string; export let albumOwnerId: string;
export let disabled: boolean; export let disabled: boolean;
export let isLiked: ActivityResponseDto | null; export let isLiked: ActivityResponseDto | null;
export let onDeleteComment: () => void;
export let onDeleteLike: () => void;
export let onAddComment: () => void;
export let onClose: () => void;
let textArea: HTMLTextAreaElement; let textArea: HTMLTextAreaElement;
let innerHeight: number; let innerHeight: number;
@ -65,13 +69,6 @@
let message = ''; let message = '';
let isSendingMessage = false; let isSendingMessage = false;
const dispatch = createEventDispatcher<{
deleteComment: void;
deleteLike: void;
addComment: void;
close: void;
}>();
$: { $: {
if (innerHeight && activityHeight) { if (innerHeight && activityHeight) {
divHeight = innerHeight - activityHeight; divHeight = innerHeight - activityHeight;
@ -111,9 +108,9 @@
reactions.splice(index, 1); reactions.splice(index, 1);
reactions = reactions; reactions = reactions;
if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) { if (isLiked && reaction.type === ReactionType.Like && reaction.id == isLiked.id) {
dispatch('deleteLike'); onDeleteLike();
} else { } else {
dispatch('deleteComment'); onDeleteComment();
} }
const deleteMessages: Record<ReactionType, string> = { const deleteMessages: Record<ReactionType, string> = {
@ -141,7 +138,7 @@
reactions.push(data); reactions.push(data);
textArea.style.height = '18px'; textArea.style.height = '18px';
message = ''; message = '';
dispatch('addComment'); onAddComment();
// Re-render the activity feed // Re-render the activity feed
reactions = reactions; reactions = reactions;
} catch (error) { } catch (error) {
@ -160,7 +157,7 @@
bind:clientHeight={activityHeight} bind:clientHeight={activityHeight}
> >
<div class="flex place-items-center gap-2"> <div class="flex place-items-center gap-2">
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} title={$t('close')} /> <CircleIconButton on:click={onClose} icon={mdiClose} title={$t('close')} />
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('activity')}</p> <p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('activity')}</p>
</div> </div>

View File

@ -1,16 +1,13 @@
<script lang="ts"> <script lang="ts">
import { getAssetThumbnailUrl } from '$lib/utils'; import { getAssetThumbnailUrl } from '$lib/utils';
import { type AlbumResponseDto } from '@immich/sdk'; import { type AlbumResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { normalizeSearchString } from '$lib/utils/string-utils.js'; import { normalizeSearchString } from '$lib/utils/string-utils.js';
import AlbumListItemDetails from './album-list-item-details.svelte'; import AlbumListItemDetails from './album-list-item-details.svelte';
const dispatch = createEventDispatcher<{
album: void;
}>();
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let searchQuery = ''; export let searchQuery = '';
export let onAlbumClick: () => void;
let albumNameArray: string[] = ['', '', '']; let albumNameArray: string[] = ['', '', ''];
// This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query // This part of the code is responsible for splitting album name into 3 parts where part 2 is the search query
@ -29,7 +26,7 @@
<button <button
type="button" type="button"
on:click={() => dispatch('album')} on:click={onAlbumClick}
class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl" class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
> >
<span class="h-12 w-12 shrink-0 rounded-xl bg-slate-300"> <span class="h-12 w-12 shrink-0 rounded-xl bg-slate-300">

View File

@ -32,7 +32,7 @@
type AssetResponseDto, type AssetResponseDto,
type StackResponseDto, type StackResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
@ -56,8 +56,10 @@
export let isShared = false; export let isShared = false;
export let album: AlbumResponseDto | null = null; export let album: AlbumResponseDto | null = null;
export let onAction: OnAction | undefined = undefined; export let onAction: OnAction | undefined = undefined;
export let reactions: ActivityResponseDto[] = [];
let reactions: ActivityResponseDto[] = []; export let onClose: (dto: { asset: AssetResponseDto }) => void;
export let onNext: () => void;
export let onPrevious: () => void;
const { setAsset } = assetViewingStore; const { setAsset } = assetViewingStore;
const { const {
@ -67,13 +69,6 @@
slideshowState, slideshowState,
} = slideshowStore; } = slideshowStore;
const dispatch = createEventDispatcher<{
action: { type: AssetAction; asset: AssetResponseDto };
close: { asset: AssetResponseDto };
next: void;
previous: void;
}>();
let appearsInAlbums: AlbumResponseDto[] = []; let appearsInAlbums: AlbumResponseDto[] = [];
let shouldPlayMotionPhoto = false; let shouldPlayMotionPhoto = false;
let sharedLink = getSharedLink(); let sharedLink = getSharedLink();
@ -267,7 +262,7 @@
}; };
const closeViewer = () => { const closeViewer = () => {
dispatch('close', { asset }); onClose({ asset });
}; };
const closeEditor = () => { const closeEditor = () => {
@ -316,7 +311,8 @@
} }
e?.stopPropagation(); e?.stopPropagation();
dispatch(order); // eslint-disable-next-line @typescript-eslint/no-unused-expressions
order === 'previous' ? onPrevious() : onNext();
}; };
// const showEditorHandler = () => { // const showEditorHandler = () => {
@ -533,8 +529,8 @@
disabled={!album?.isActivityEnabled} disabled={!album?.isActivityEnabled}
{isLiked} {isLiked}
{numberOfComments} {numberOfComments}
on:favorite={handleFavorite} onFavorite={handleFavorite}
on:openActivityTab={handleOpenActivity} onOpenActivityTab={handleOpenActivity}
/> />
</div> </div>
{/if} {/if}
@ -555,7 +551,7 @@
class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg" class="z-[1002] row-start-1 row-span-4 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg"
translate="yes" translate="yes"
> >
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} on:close={() => ($isShowDetail = false)} /> <DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
</div> </div>
{/if} {/if}
@ -625,10 +621,10 @@
assetId={asset.id} assetId={asset.id}
{isLiked} {isLiked}
bind:reactions bind:reactions
on:addComment={handleAddComment} onAddComment={handleAddComment}
on:deleteComment={handleRemoveComment} onDeleteComment={handleRemoveComment}
on:deleteLike={() => (isLiked = null)} onDeleteLike={() => (isLiked = null)}
on:close={() => (isShowActivity = false)} onClose={() => (isShowActivity = false)}
/> />
</div> </div>
{/if} {/if}

View File

@ -81,10 +81,6 @@
{#if isShowChangeLocation} {#if isShowChangeLocation}
<Portal> <Portal>
<ChangeLocation <ChangeLocation {asset} onConfirm={handleConfirmChangeLocation} onCancel={() => (isShowChangeLocation = false)} />
{asset}
on:confirm={({ detail: gps }) => handleConfirmChangeLocation(gps)}
on:cancel={() => (isShowChangeLocation = false)}
/>
</Portal> </Portal>
{/if} {/if}

View File

@ -36,7 +36,6 @@
mdiPencil, mdiPencil,
} from '@mdi/js'; } from '@mdi/js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
@ -49,6 +48,7 @@
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
export let currentAlbum: AlbumResponseDto | null = null; export let currentAlbum: AlbumResponseDto | null = null;
export let onClose: () => void;
const getDimensions = (exifInfo: ExifResponseDto) => { const getDimensions = (exifInfo: ExifResponseDto) => {
const { exifImageWidth: width, exifImageHeight: height } = exifInfo; const { exifImageWidth: width, exifImageHeight: height } = exifInfo;
@ -106,10 +106,6 @@
? fromDateTimeOriginal(asset.exifInfo.dateTimeOriginal, timeZone) ? fromDateTimeOriginal(asset.exifInfo.dateTimeOriginal, timeZone)
: fromLocalDateTime(asset.localDateTime); : fromLocalDateTime(asset.localDateTime);
const dispatch = createEventDispatcher<{
close: void;
}>();
const getMegapixel = (width: number, height: number): number | undefined => { const getMegapixel = (width: number, height: number): number | undefined => {
const megapixel = Math.round((height * width) / 1_000_000); const megapixel = Math.round((height * width) / 1_000_000);
@ -144,7 +140,7 @@
<section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> <section class="relative p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg">
<div class="flex place-items-center gap-2"> <div class="flex place-items-center gap-2">
<CircleIconButton icon={mdiClose} title={$t('close')} on:click={() => dispatch('close')} /> <CircleIconButton icon={mdiClose} title={$t('close')} on:click={onClose} />
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p> <p class="text-lg text-immich-fg dark:text-immich-dark-fg">{$t('info')}</p>
</div> </div>
@ -332,8 +328,8 @@
<ChangeDate <ChangeDate
initialDate={dateTime} initialDate={dateTime}
initialTimeZone={timeZone ?? ''} initialTimeZone={timeZone ?? ''}
on:confirm={({ detail: date }) => handleConfirmChangeDate(date)} onConfirm={handleConfirmChangeDate}
on:cancel={() => (isShowChangeDate = false)} onCancel={() => (isShowChangeDate = false)}
/> />
{/if} {/if}
@ -511,9 +507,7 @@
<PersonSidePanel <PersonSidePanel
assetId={asset.id} assetId={asset.id}
assetType={asset.type} assetType={asset.type}
on:close={() => { onClose={() => (showEditFaces = false)}
showEditFaces = false; onRefresh={handleRefreshPeople}
}}
on:refresh={handleRefreshPeople}
/> />
{/if} {/if}

View File

@ -139,5 +139,5 @@
duration={$slideshowDelay} duration={$slideshowDelay}
bind:this={progressBar} bind:this={progressBar}
bind:status={progressBarStatus} bind:status={progressBarStatus}
on:done={handleDone} onDone={handleDone}
/> />

View File

@ -4,7 +4,7 @@
import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { AssetMediaSize } from '@immich/sdk'; import { AssetMediaSize } from '@immich/sdk';
import { createEventDispatcher, tick } from 'svelte'; import { tick } from 'svelte';
import { swipe } from 'svelte-gestures'; import { swipe } from 'svelte-gestures';
import type { SwipeCustomEvent } from 'svelte-gestures'; import type { SwipeCustomEvent } from 'svelte-gestures';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
@ -13,8 +13,10 @@
export let assetId: string; export let assetId: string;
export let loopVideo: boolean; export let loopVideo: boolean;
export let checksum: string; export let checksum: string;
export let onPreviousAsset: () => void; export let onPreviousAsset: () => void = () => {};
export let onNextAsset: () => void; export let onNextAsset: () => void = () => {};
export let onVideoEnded: () => void = () => {};
export let onVideoStarted: () => void = () => {};
let element: HTMLVideoElement | undefined = undefined; let element: HTMLVideoElement | undefined = undefined;
let isVideoLoading = true; let isVideoLoading = true;
@ -27,12 +29,10 @@
element.load(); element.load();
} }
const dispatch = createEventDispatcher<{ onVideoEnded: void; onVideoStarted: void }>();
const handleCanPlay = async (video: HTMLVideoElement) => { const handleCanPlay = async (video: HTMLVideoElement) => {
try { try {
await video.play(); await video.play();
dispatch('onVideoStarted'); onVideoStarted();
} catch (error) { } catch (error) {
if (error instanceof DOMException && error.name === 'NotAllowedError' && !forceMuted) { if (error instanceof DOMException && error.name === 'NotAllowedError' && !forceMuted) {
await tryForceMutedPlay(video); await tryForceMutedPlay(video);
@ -75,7 +75,7 @@
use:swipe use:swipe
on:swipe={onSwipe} on:swipe={onSwipe}
on:canplay={(e) => handleCanPlay(e.currentTarget)} on:canplay={(e) => handleCanPlay(e.currentTarget)}
on:ended={() => dispatch('onVideoEnded')} on:ended={onVideoEnded}
on:volumechange={(e) => { on:volumechange={(e) => {
if (!forceMuted) { if (!forceMuted) {
$videoViewerMuted = e.currentTarget.muted; $videoViewerMuted = e.currentTarget.muted;

View File

@ -15,13 +15,5 @@
{#if projectionType === ProjectionType.EQUIRECTANGULAR} {#if projectionType === ProjectionType.EQUIRECTANGULAR}
<PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} /> <PanoramaViewer asset={{ id: assetId, type: AssetTypeEnum.Video }} />
{:else} {:else}
<VideoNativeViewer <VideoNativeViewer {loopVideo} {checksum} {assetId} {onPreviousAsset} {onNextAsset} />
{loopVideo}
{checksum}
{assetId}
{onPreviousAsset}
{onNextAsset}
on:onVideoEnded
on:onVideoStarted
/>
{/if} {/if}

View File

@ -19,22 +19,18 @@
import LinkButton from './buttons/link-button.svelte'; import LinkButton from './buttons/link-button.svelte';
import { clickOutside } from '$lib/actions/click-outside'; import { clickOutside } from '$lib/actions/click-outside';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
let className = ''; let className = '';
export { className as class }; export { className as class };
const dispatch = createEventDispatcher<{
select: T;
'click-outside': void;
}>();
export let options: T[]; export let options: T[];
export let selectedOption = options[0]; export let selectedOption = options[0];
export let showMenu = false; export let showMenu = false;
export let controlable = false; export let controlable = false;
export let hideTextOnSmallScreen = true; export let hideTextOnSmallScreen = true;
export let title: string | undefined = undefined; export let title: string | undefined = undefined;
export let onSelect: (option: T) => void;
export let onClickOutside: () => void = () => {};
export let render: (item: T) => string | RenderedOption = String; export let render: (item: T) => string | RenderedOption = String;
@ -43,11 +39,11 @@
showMenu = false; showMenu = false;
} }
dispatch('click-outside'); onClickOutside();
}; };
const handleSelectOption = (option: T) => { const handleSelectOption = (option: T) => {
dispatch('select', option); onSelect(option);
selectedOption = option; selectedOption = option;
showMenu = false; showMenu = false;

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { mdiClose, mdiMagnify } from '@mdi/js'; import { mdiClose, mdiMagnify } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import type { SearchOptions } from '$lib/utils/dipatch'; import type { SearchOptions } from '$lib/utils/dipatch';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@ -10,20 +9,20 @@
export let roundedBottom = true; export let roundedBottom = true;
export let showLoadingSpinner: boolean; export let showLoadingSpinner: boolean;
export let placeholder: string; export let placeholder: string;
export let onSearch: (options: SearchOptions) => void = () => {};
export let onReset: () => void = () => {};
let inputRef: HTMLElement; let inputRef: HTMLElement;
const dispatch = createEventDispatcher<{ search: SearchOptions; reset: void }>();
const resetSearch = () => { const resetSearch = () => {
name = ''; name = '';
dispatch('reset'); onReset();
inputRef?.focus(); inputRef?.focus();
}; };
const handleSearch = (event: KeyboardEvent) => { const handleSearch = (event: KeyboardEvent) => {
if (event.key === 'Enter') { if (event.key === 'Enter') {
dispatch('search', { force: true }); onSearch({ force: true });
} }
}; };
</script> </script>
@ -38,7 +37,7 @@
title={$t('search')} title={$t('search')}
size="16" size="16"
padding="2" padding="2"
on:click={() => dispatch('search', { force: true })} on:click={() => onSearch({ force: true })}
/> />
<input <input
class="w-full gap-2 bg-gray-200 dark:bg-immich-dark-gray dark:text-white" class="w-full gap-2 bg-gray-200 dark:bg-immich-dark-gray dark:text-white"
@ -47,7 +46,7 @@
bind:value={name} bind:value={name}
bind:this={inputRef} bind:this={inputRef}
on:keydown={handleSearch} on:keydown={handleSearch}
on:input={() => dispatch('search', { force: false })} on:input={() => onSearch({ force: false })}
/> />
{#if showLoadingSpinner} {#if showLoadingSpinner}
<div class="flex place-items-center"> <div class="flex place-items-center">

View File

@ -4,7 +4,6 @@
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk'; import { AssetTypeEnum, type AssetFaceResponseDto, type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js'; import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { linear } from 'svelte/easing'; import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { photoViewer } from '$lib/stores/assets.store'; import { photoViewer } from '$lib/stores/assets.store';
@ -19,6 +18,9 @@
export let editedFace: AssetFaceResponseDto; export let editedFace: AssetFaceResponseDto;
export let assetId: string; export let assetId: string;
export let assetType: AssetTypeEnum; export let assetType: AssetTypeEnum;
export let onClose: () => void;
export let onCreatePerson: (featurePhoto: string | null) => void;
export let onReassign: (person: PersonResponseDto) => void;
// loading spinners // loading spinners
let isShowLoadingNewPerson = false; let isShowLoadingNewPerson = false;
@ -31,25 +33,16 @@
$: showPeople = searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden); $: showPeople = searchName ? searchedPeople : allPeople.filter((person) => !person.isHidden);
const dispatch = createEventDispatcher<{
close: void;
createPerson: string | null;
reassign: PersonResponseDto;
}>();
const handleBackButton = () => {
dispatch('close');
};
const handleCreatePerson = async () => { const handleCreatePerson = async () => {
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner); const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, $photoViewer); const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, $photoViewer);
dispatch('createPerson', newFeaturePhoto); onCreatePerson(newFeaturePhoto);
clearTimeout(timeout); clearTimeout(timeout);
isShowLoadingNewPerson = false; isShowLoadingNewPerson = false;
dispatch('createPerson', newFeaturePhoto); onCreatePerson(newFeaturePhoto);
}; };
</script> </script>
@ -60,7 +53,7 @@
<div class="flex place-items-center justify-between gap-2"> <div class="flex place-items-center justify-between gap-2">
{#if !searchFaces} {#if !searchFaces}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} /> <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p> <p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('select_face')}</p>
</div> </div>
<div class="flex justify-end gap-2"> <div class="flex justify-end gap-2">
@ -80,7 +73,7 @@
{/if} {/if}
</div> </div>
{:else} {:else}
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} /> <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
<div class="w-full flex"> <div class="w-full flex">
<SearchPeople <SearchPeople
type="input" type="input"
@ -103,7 +96,7 @@
{#each showPeople as person (person.id)} {#each showPeople as person (person.id)}
{#if !editedFace.person || person.id !== editedFace.person.id} {#if !editedFace.person || person.id !== editedFace.person.id}
<div class="w-fit"> <div class="w-fit">
<button type="button" class="w-[90px]" on:click={() => dispatch('reassign', person)}> <button type="button" class="w-[90px]" on:click={() => onReassign(person)}>
<div class="relative"> <div class="relative">
<ImageThumbnail <ImageThumbnail
curve curve

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { type PersonResponseDto } from '@immich/sdk'; import { type PersonResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte'; import SearchPeople from '$lib/components/faces-page/people-search.svelte';
@ -11,10 +10,7 @@
export let suggestedPeople: PersonResponseDto[]; export let suggestedPeople: PersonResponseDto[];
export let thumbnailData: string; export let thumbnailData: string;
export let isSearchingPeople: boolean; export let isSearchingPeople: boolean;
export let onChange: (name: string) => void;
const dispatch = createEventDispatcher<{
change: string;
}>();
</script> </script>
<div <div
@ -26,7 +22,7 @@
<form <form
class="ml-4 flex w-full justify-between gap-16" class="ml-4 flex w-full justify-between gap-16"
autocomplete="off" autocomplete="off"
on:submit|preventDefault={() => dispatch('change', name)} on:submit|preventDefault={() => onChange(name)}
> >
<SearchPeople <SearchPeople
bind:searchName={name} bind:searchName={name}

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk'; import { type PersonResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
export let person: PersonResponseDto; export let person: PersonResponseDto;
@ -10,20 +9,13 @@
export let thumbnailSize: number | null = null; export let thumbnailSize: number | null = null;
export let circle = false; export let circle = false;
export let border = false; export let border = false;
export let onClick: (person: PersonResponseDto) => void = () => {};
let dispatch = createEventDispatcher<{
click: PersonResponseDto;
}>();
const handleOnClicked = () => {
dispatch('click', person);
};
</script> </script>
<button <button
type="button" type="button"
class="relative rounded-lg transition-all" class="relative rounded-lg transition-all"
on:click={handleOnClicked} on:click={() => onClick(person)}
disabled={!selectable} disabled={!selectable}
style:width={thumbnailSize ? thumbnailSize + 'px' : '100%'} style:width={thumbnailSize ? thumbnailSize + 'px' : '100%'}
style:height={thumbnailSize ? thumbnailSize + 'px' : '100%'} style:height={thumbnailSize ? thumbnailSize + 'px' : '100%'}

View File

@ -6,7 +6,7 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk'; import { getAllPeople, getPerson, mergePerson, type PersonResponseDto } from '@immich/sdk';
import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
@ -20,15 +20,13 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let person: PersonResponseDto; export let person: PersonResponseDto;
export let onBack: () => void;
export let onMerge: (mergedPerson: PersonResponseDto) => void;
let people: PersonResponseDto[] = []; let people: PersonResponseDto[] = [];
let selectedPeople: PersonResponseDto[] = []; let selectedPeople: PersonResponseDto[] = [];
let screenHeight: number; let screenHeight: number;
let dispatch = createEventDispatcher<{
back: void;
merge: PersonResponseDto;
}>();
$: hasSelection = selectedPeople.length > 0; $: hasSelection = selectedPeople.length > 0;
$: peopleToNotShow = [...selectedPeople, person]; $: peopleToNotShow = [...selectedPeople, person];
@ -37,10 +35,6 @@
people = data.people; people = data.people;
}); });
const onClose = () => {
dispatch('back');
};
const handleSwapPeople = async () => { const handleSwapPeople = async () => {
[person, selectedPeople[0]] = [selectedPeople[0], person]; [person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE); $page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
@ -88,7 +82,7 @@
message: $t('merged_people_count', { values: { count } }), message: $t('merged_people_count', { values: { count } }),
type: NotificationType.Info, type: NotificationType.Info,
}); });
dispatch('merge', mergedPerson); onMerge(mergedPerson);
} catch (error) { } catch (error) {
handleError(error, $t('cannot_merge_people')); handleError(error, $t('cannot_merge_people'));
} }
@ -101,7 +95,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }} transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg" class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
> >
<ControlAppBar on:close={onClose}> <ControlAppBar onClose={onBack}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
{#if hasSelection} {#if hasSelection}
{$t('selected_count', { values: { count: selectedPeople.length } })} {$t('selected_count', { values: { count: selectedPeople.length } })}
@ -125,7 +119,7 @@
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4"> <div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
{#each selectedPeople as person (person.id)} {#each selectedPeople as person (person.id)}
<div animate:flip={{ duration: 250, easing: quintOut }}> <div animate:flip={{ duration: 250, easing: quintOut }}>
<FaceThumbnail border circle {person} selectable thumbnailSize={120} on:click={() => onSelect(person)} /> <FaceThumbnail border circle {person} selectable thumbnailSize={120} onClick={() => onSelect(person)} />
</div> </div>
{/each} {/each}
@ -152,7 +146,7 @@
</div> </div>
</div> </div>
<PeopleList {people} {peopleToNotShow} {screenHeight} on:select={({ detail }) => onSelect(detail)} /> <PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} />
</section> </section>
</section> </section>
</section> </section>

View File

@ -4,7 +4,6 @@
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk'; import { type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeft, mdiMerge } from '@mdi/js'; import { mdiArrowLeft, mdiMerge } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
@ -13,25 +12,22 @@
export let personMerge1: PersonResponseDto; export let personMerge1: PersonResponseDto;
export let personMerge2: PersonResponseDto; export let personMerge2: PersonResponseDto;
export let potentialMergePeople: PersonResponseDto[]; export let potentialMergePeople: PersonResponseDto[];
export let onReject: () => void;
export let onConfirm: ([personMerge1, personMerge2]: [PersonResponseDto, PersonResponseDto]) => void;
export let onClose: () => void;
let choosePersonToMerge = false; let choosePersonToMerge = false;
const title = personMerge2.name; const title = personMerge2.name;
const dispatch = createEventDispatcher<{ const changePersonToMerge = (newPerson: PersonResponseDto) => {
reject: void; const index = potentialMergePeople.indexOf(newPerson);
confirm: [PersonResponseDto, PersonResponseDto];
close: void;
}>();
const changePersonToMerge = (newperson: PersonResponseDto) => {
const index = potentialMergePeople.indexOf(newperson);
[potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]]; [potentialMergePeople[index], personMerge2] = [personMerge2, potentialMergePeople[index]];
choosePersonToMerge = false; choosePersonToMerge = false;
}; };
</script> </script>
<FullScreenModal title="{$t('merge_people')} - {title}" onClose={() => dispatch('close')}> <FullScreenModal title="{$t('merge_people')} - {title}" {onClose}>
<div class="flex items-center justify-center py-4 md:h-36 md:py-4"> <div class="flex items-center justify-center py-4 md:h-36 md:py-4">
{#if !choosePersonToMerge} {#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2"> <div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
@ -105,7 +101,7 @@
<p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p> <p class="text-sm text-gray-500 dark:text-gray-300">{$t('they_will_be_merged_together')}</p>
</div> </div>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>{$t('no')}</Button> <Button fullwidth color="gray" on:click={onReject}>{$t('no')}</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>{$t('yes')}</Button> <Button fullwidth on:click={() => onConfirm([personMerge1, personMerge2])}>{$t('yes')}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -9,7 +9,6 @@
mdiDotsVertical, mdiDotsVertical,
mdiEyeOffOutline, mdiEyeOffOutline,
} from '@mdi/js'; } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte'; import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -18,19 +17,12 @@
export let person: PersonResponseDto; export let person: PersonResponseDto;
export let preload = false; export let preload = false;
export let onChangeName: () => void;
type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-people' | 'hide-person'; export let onSetBirthDate: () => void;
let dispatch = createEventDispatcher<{ export let onMergePeople: () => void;
'change-name': void; export let onHidePerson: () => void;
'set-birth-date': void;
'merge-people': void;
'hide-person': void;
}>();
let showVerticalDots = false; let showVerticalDots = false;
const onMenuClick = (event: MenuItemEvent) => {
dispatch(event);
};
</script> </script>
<div <div
@ -76,18 +68,10 @@
icon={mdiDotsVertical} icon={mdiDotsVertical}
title={$t('show_person_options')} title={$t('show_person_options')}
> >
<MenuOption onClick={() => onMenuClick('hide-person')} icon={mdiEyeOffOutline} text={$t('hide_person')} /> <MenuOption onClick={onHidePerson} icon={mdiEyeOffOutline} text={$t('hide_person')} />
<MenuOption onClick={() => onMenuClick('change-name')} icon={mdiAccountEditOutline} text={$t('change_name')} /> <MenuOption onClick={onChangeName} icon={mdiAccountEditOutline} text={$t('change_name')} />
<MenuOption <MenuOption onClick={onSetBirthDate} icon={mdiCalendarEditOutline} text={$t('set_date_of_birth')} />
onClick={() => onMenuClick('set-birth-date')} <MenuOption onClick={onMergePeople} icon={mdiAccountMultipleCheckOutline} text={$t('merge_people')} />
icon={mdiCalendarEditOutline}
text={$t('set_date_of_birth')}
/>
<MenuOption
onClick={() => onMenuClick('merge-people')}
icon={mdiAccountMultipleCheckOutline}
text={$t('merge_people')}
/>
</ButtonContextMenu> </ButtonContextMenu>
</div> </div>
{/if} {/if}

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { type PersonResponseDto } from '@immich/sdk'; import { type PersonResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import FaceThumbnail from './face-thumbnail.svelte'; import FaceThumbnail from './face-thumbnail.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte'; import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -8,15 +7,13 @@
export let screenHeight: number; export let screenHeight: number;
export let people: PersonResponseDto[]; export let people: PersonResponseDto[];
export let peopleToNotShow: PersonResponseDto[]; export let peopleToNotShow: PersonResponseDto[];
export let onSelect: (person: PersonResponseDto) => void;
let searchedPeopleLocal: PersonResponseDto[] = []; let searchedPeopleLocal: PersonResponseDto[] = [];
let name = ''; let name = '';
let showPeople: PersonResponseDto[]; let showPeople: PersonResponseDto[];
let dispatch = createEventDispatcher<{
select: PersonResponseDto;
}>();
$: { $: {
showPeople = name ? searchedPeopleLocal : people; showPeople = name ? searchedPeopleLocal : people;
showPeople = showPeople.filter( showPeople = showPeople.filter(
@ -35,15 +32,7 @@
> >
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10"> <div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">
{#each showPeople as person (person.id)} {#each showPeople as person (person.id)}
<FaceThumbnail <FaceThumbnail {person} on:click={() => onSelect(person)} circle border selectable />
{person}
on:click={() => {
dispatch('select', person);
}}
circle
border
selectable
/>
{/each} {/each}
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@
import { mdiAccountOff } from '@mdi/js'; import { mdiAccountOff } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { mdiArrowLeftThin, mdiMinus, mdiRestart } from '@mdi/js'; import { mdiArrowLeftThin, mdiMinus, mdiRestart } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { linear } from 'svelte/easing'; import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
@ -31,6 +31,8 @@
export let assetId: string; export let assetId: string;
export let assetType: AssetTypeEnum; export let assetType: AssetTypeEnum;
export let onClose: () => void;
export let onRefresh: () => void;
// keep track of the changes // keep track of the changes
let peopleToCreate: string[] = []; let peopleToCreate: string[] = [];
@ -56,11 +58,6 @@
const thumbnailWidth = '90px'; const thumbnailWidth = '90px';
const dispatch = createEventDispatcher<{
close: void;
refresh: void;
}>();
async function loadPeople() { async function loadPeople() {
const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner); const timeout = setTimeout(() => (isShowLoadingPeople = true), timeBeforeShowLoadingSpinner);
try { try {
@ -85,7 +82,7 @@
) { ) {
clearTimeout(loaderLoadingDoneTimeout); clearTimeout(loaderLoadingDoneTimeout);
clearTimeout(automaticRefreshTimeout); clearTimeout(automaticRefreshTimeout);
dispatch('refresh'); onRefresh();
} }
}; };
@ -98,10 +95,6 @@
return b.every((valueB) => a.includes(valueB)); return b.every((valueB) => a.includes(valueB));
}; };
const handleBackButton = () => {
dispatch('close');
};
const handleReset = (id: string) => { const handleReset = (id: string) => {
if (selectedPersonToReassign[id]) { if (selectedPersonToReassign[id]) {
delete selectedPersonToReassign[id]; delete selectedPersonToReassign[id];
@ -153,9 +146,9 @@
isShowLoadingDone = false; isShowLoadingDone = false;
if (peopleToCreate.length === 0) { if (peopleToCreate.length === 0) {
clearTimeout(loaderLoadingDoneTimeout); clearTimeout(loaderLoadingDoneTimeout);
dispatch('refresh'); onRefresh();
} else { } else {
automaticRefreshTimeout = setTimeout(() => dispatch('refresh'), 15_000); automaticRefreshTimeout = setTimeout(onRefresh, 15_000);
} }
}; };
@ -185,7 +178,7 @@
> >
<div class="flex place-items-center justify-between gap-2"> <div class="flex place-items-center justify-between gap-2">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={handleBackButton} /> <CircleIconButton icon={mdiArrowLeftThin} title={$t('back')} on:click={onClose} />
<p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('edit_faces')}</p> <p class="flex text-lg text-immich-fg dark:text-immich-dark-fg">{$t('edit_faces')}</p>
</div> </div>
{#if !isShowLoadingDone} {#if !isShowLoadingDone}
@ -336,8 +329,8 @@
{editedFace} {editedFace}
{assetId} {assetId}
{assetType} {assetType}
on:close={() => (showSelectedFaces = false)} onClose={() => (showSelectedFaces = false)}
on:createPerson={(event) => handleCreatePerson(event.detail)} onCreatePerson={handleCreatePerson}
on:reassign={(event) => handleReassignFace(event.detail)} onReassign={handleReassignFace}
/> />
{/if} {/if}

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiCake } from '@mdi/js'; import { mdiCake } from '@mdi/js';
@ -7,28 +6,20 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let birthDate: string; export let birthDate: string;
export let onClose: () => void;
const dispatch = createEventDispatcher<{ export let onUpdate: (birthDate: string) => void;
close: void;
updated: string;
}>();
const todayFormatted = new Date().toISOString().split('T')[0]; const todayFormatted = new Date().toISOString().split('T')[0];
const handleCancel = () => dispatch('close');
const handleSubmit = () => {
dispatch('updated', birthDate);
};
</script> </script>
<FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} onClose={handleCancel}> <FullScreenModal title={$t('set_date_of_birth')} icon={mdiCake} {onClose}>
<div class="text-immich-primary dark:text-immich-dark-primary"> <div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
{$t('birthdate_set_description')} {$t('birthdate_set_description')}
</p> </p>
</div> </div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="set-birth-date-form"> <form on:submit|preventDefault={() => onUpdate(birthDate)} autocomplete="off" id="set-birth-date-form">
<div class="my-4 flex flex-col gap-2"> <div class="my-4 flex flex-col gap-2">
<DateInput <DateInput
class="immich-form-input" class="immich-form-input"
@ -41,7 +32,7 @@
</div> </div>
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button> <Button color="gray" fullwidth on:click={onClose}>{$t('cancel')}</Button>
<Button type="submit" fullwidth form="set-birth-date-form">{$t('set')}</Button> <Button type="submit" fullwidth form="set-birth-date-form">{$t('set')}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -10,7 +10,7 @@
type PersonResponseDto, type PersonResponseDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { mdiMerge, mdiPlus } from '@mdi/js'; import { mdiMerge, mdiPlus } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
@ -23,6 +23,8 @@
export let assetIds: string[]; export let assetIds: string[];
export let personAssets: PersonResponseDto; export let personAssets: PersonResponseDto;
export let onConfirm: () => void;
export let onClose: () => void;
let people: PersonResponseDto[] = []; let people: PersonResponseDto[] = [];
let selectedPerson: PersonResponseDto | null = null; let selectedPerson: PersonResponseDto | null = null;
@ -34,11 +36,6 @@
$: peopleToNotShow = selectedPerson ? [personAssets, selectedPerson] : [personAssets]; $: peopleToNotShow = selectedPerson ? [personAssets, selectedPerson] : [personAssets];
let dispatch = createEventDispatcher<{
confirm: void;
close: void;
}>();
const selectedPeople: AssetFaceUpdateItem[] = []; const selectedPeople: AssetFaceUpdateItem[] = [];
for (const assetId of assetIds) { for (const assetId of assetIds) {
@ -50,10 +47,6 @@
people = data.people; people = data.people;
}); });
const onClose = () => {
dispatch('close');
};
const handleSelectedPerson = (person: PersonResponseDto) => { const handleSelectedPerson = (person: PersonResponseDto) => {
if (selectedPerson && selectedPerson.id === person.id) { if (selectedPerson && selectedPerson.id === person.id) {
handleRemoveSelectedPerson(); handleRemoveSelectedPerson();
@ -87,7 +80,7 @@
} }
showLoadingSpinnerCreate = false; showLoadingSpinnerCreate = false;
dispatch('confirm'); onConfirm();
}; };
const handleReassign = async () => { const handleReassign = async () => {
@ -113,7 +106,7 @@
} }
showLoadingSpinnerReassign = false; showLoadingSpinnerReassign = false;
dispatch('confirm'); onConfirm();
}; };
</script> </script>
@ -123,7 +116,7 @@
transition:fly={{ y: 500, duration: 100, easing: quintOut }} transition:fly={{ y: 500, duration: 100, easing: quintOut }}
class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg" class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
> >
<ControlAppBar on:close={onClose}> <ControlAppBar {onClose}>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<slot name="header" /> <slot name="header" />
<div /> <div />
@ -180,7 +173,7 @@
</div> </div>
</div> </div>
{/if} {/if}
<PeopleList {people} {peopleToNotShow} {screenHeight} on:select={({ detail }) => handleSelectedPerson(detail)} /> <PeopleList {people} {peopleToNotShow} {screenHeight} onSelect={handleSelectedPerson} />
</section> </section>
</section> </section>
</section> </section>

View File

@ -1,20 +1,15 @@
<script lang="ts"> <script lang="ts">
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import { mdiKeyVariant } from '@mdi/js'; import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let secret = ''; export let secret = '';
export let onDone: () => void;
const dispatch = createEventDispatcher<{
done: void;
}>();
const handleDone = () => dispatch('done');
</script> </script>
<FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={() => handleDone()}> <FullScreenModal title={$t('api_key')} icon={mdiKeyVariant} onClose={onDone}>
<div class="text-immich-primary dark:text-immich-dark-primary"> <div class="text-immich-primary dark:text-immich-dark-primary">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
{$t('api_key_description')} {$t('api_key_description')}
@ -28,6 +23,6 @@
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button on:click={() => copyToClipboard(secret)} fullwidth>{$t('copy_to_clipboard')}</Button> <Button on:click={() => copyToClipboard(secret)} fullwidth>{$t('copy_to_clipboard')}</Button>
<Button on:click={() => handleDone()} fullwidth>{$t('done')}</Button> <Button on:click={onDone} fullwidth>{$t('done')}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -1,10 +1,11 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import PasswordField from '../shared-components/password-field.svelte'; import PasswordField from '../shared-components/password-field.svelte';
import { updateMyUser } from '@immich/sdk'; import { updateMyUser } from '@immich/sdk';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let onSuccess: () => void;
let errorMessage: string; let errorMessage: string;
let success: string; let success: string;
@ -23,17 +24,13 @@
} }
} }
const dispatch = createEventDispatcher<{
success: void;
}>();
async function changePassword() { async function changePassword() {
if (valid) { if (valid) {
errorMessage = ''; errorMessage = '';
await updateMyUser({ userUpdateMeDto: { password: String(password) } }); await updateMyUser({ userUpdateMeDto: { password: String(password) } });
dispatch('success'); onSuccess();
} }
} }
</script> </script>

View File

@ -5,13 +5,14 @@
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units'; import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { createUserAdmin } from '@immich/sdk'; import { createUserAdmin } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import Slider from '../elements/slider.svelte'; import Slider from '../elements/slider.svelte';
import PasswordField from '../shared-components/password-field.svelte'; import PasswordField from '../shared-components/password-field.svelte';
export let onClose: () => void; export let onClose: () => void;
export let onSubmit: () => void;
export let onCancel: () => void;
let error: string; let error: string;
let success: string; let success: string;
@ -39,10 +40,6 @@
canCreateUser = true; canCreateUser = true;
} }
} }
const dispatch = createEventDispatcher<{
submit: void;
cancel: void;
}>();
async function registerUser() { async function registerUser() {
if (canCreateUser && !isCreatingUser) { if (canCreateUser && !isCreatingUser) {
@ -63,7 +60,7 @@
success = $t('new_user_created'); success = $t('new_user_created');
dispatch('submit'); onSubmit();
return; return;
} catch (error) { } catch (error) {
@ -132,7 +129,7 @@
{/if} {/if}
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>{$t('cancel')}</Button> <Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button> <Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">{$t('create')}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -5,7 +5,6 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk'; import { updateUserAdmin, type UserAdminResponseDto } from '@immich/sdk';
import { mdiAccountEditOutline } from '@mdi/js'; import { mdiAccountEditOutline } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -15,6 +14,8 @@
export let canResetPassword = true; export let canResetPassword = true;
export let newPassword: string; export let newPassword: string;
export let onClose: () => void; export let onClose: () => void;
export let onResetPasswordSuccess: () => void;
export let onEditSuccess: () => void;
let error: string; let error: string;
let success: string; let success: string;
@ -27,12 +28,6 @@
!!quotaSize && !!quotaSize &&
convertToBytes(Number(quotaSize), ByteUnit.GiB) > $serverInfo.diskSizeRaw; convertToBytes(Number(quotaSize), ByteUnit.GiB) > $serverInfo.diskSizeRaw;
const dispatch = createEventDispatcher<{
close: void;
resetPasswordSuccess: void;
editSuccess: void;
}>();
const editUser = async () => { const editUser = async () => {
try { try {
const { id, email, name, storageLabel } = user; const { id, email, name, storageLabel } = user;
@ -46,7 +41,7 @@
}, },
}); });
dispatch('editSuccess'); onEditSuccess();
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_update_user')); handleError(error, $t('errors.unable_to_update_user'));
} }
@ -72,7 +67,7 @@
}, },
}); });
dispatch('resetPasswordSuccess'); onResetPasswordSuccess();
} catch (error) { } catch (error) {
handleError(error, $t('errors.unable_to_reset_password')); handleError(error, $t('errors.unable_to_reset_password'));
} }

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderRemove } from '@mdi/js'; import { mdiFolderRemove } from '@mdi/js';
@ -10,6 +9,9 @@
export let exclusionPatterns: string[] = []; export let exclusionPatterns: string[] = [];
export let isEditing = false; export let isEditing = false;
export let submitText = $t('submit'); export let submitText = $t('submit');
export let onCancel: () => void;
export let onSubmit: (exclusionPattern: string) => void;
export let onDelete: () => void = () => {};
onMount(() => { onMount(() => {
if (isEditing) { if (isEditing) {
@ -19,18 +21,10 @@
$: isDuplicate = exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern); $: isDuplicate = exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern);
$: canSubmit = exclusionPattern && !exclusionPatterns.includes(exclusionPattern); $: canSubmit = exclusionPattern && !exclusionPatterns.includes(exclusionPattern);
const dispatch = createEventDispatcher<{
cancel: void;
submit: { excludePattern: string };
delete: void;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
</script> </script>
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}> <FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={onCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form"> <form on:submit|preventDefault={() => onSubmit(exclusionPattern)} autocomplete="off" id="add-exclusion-pattern-form">
<p class="py-5 text-sm"> <p class="py-5 text-sm">
{$t('admin.exclusion_pattern_description')} {$t('admin.exclusion_pattern_description')}
<br /><br /> <br /><br />
@ -53,9 +47,9 @@
</div> </div>
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button> <Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
{#if isEditing} {#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button> <Button color="red" fullwidth on:click={onDelete}>{$t('delete')}</Button>
{/if} {/if}
<Button type="submit" disabled={!canSubmit} fullwidth form="add-exclusion-pattern-form">{submitText}</Button> <Button type="submit" disabled={!canSubmit} fullwidth form="add-exclusion-pattern-form">{submitText}</Button>
</svelte:fragment> </svelte:fragment>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js'; import { mdiFolderSync } from '@mdi/js';
@ -12,6 +11,9 @@
export let cancelText = $t('cancel'); export let cancelText = $t('cancel');
export let submitText = $t('save'); export let submitText = $t('save');
export let isEditing = false; export let isEditing = false;
export let onCancel: () => void;
export let onSubmit: (importPath: string | null) => void;
export let onDelete: () => void = () => {};
onMount(() => { onMount(() => {
if (isEditing) { if (isEditing) {
@ -21,18 +23,10 @@
$: isDuplicate = importPath !== null && importPaths.includes(importPath); $: isDuplicate = importPath !== null && importPaths.includes(importPath);
$: canSubmit = importPath !== '' && importPath !== null && !importPaths.includes(importPath); $: canSubmit = importPath !== '' && importPath !== null && !importPaths.includes(importPath);
const dispatch = createEventDispatcher<{
cancel: void;
submit: { importPath: string | null };
delete: void;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { importPath });
</script> </script>
<FullScreenModal {title} icon={mdiFolderSync} onClose={handleCancel}> <FullScreenModal {title} icon={mdiFolderSync} onClose={onCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form"> <form on:submit|preventDefault={() => onSubmit(importPath)} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p> <p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
<div class="my-4 flex flex-col gap-2"> <div class="my-4 flex flex-col gap-2">
@ -47,9 +41,9 @@
</div> </div>
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button> <Button color="gray" fullwidth on:click={onCancel}>{cancelText}</Button>
{#if isEditing} {#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>{$t('delete')}</Button> <Button color="red" fullwidth on:click={onDelete}>{$t('delete')}</Button>
{/if} {/if}
<Button type="submit" disabled={!canSubmit} fullwidth form="library-import-path-form">{submitText}</Button> <Button type="submit" disabled={!canSubmit} fullwidth form="library-import-path-form">{submitText}</Button>
</svelte:fragment> </svelte:fragment>

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import LibraryImportPathForm from './library-import-path-form.svelte'; import LibraryImportPathForm from './library-import-path-form.svelte';
@ -12,6 +12,8 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let library: LibraryResponseDto; export let library: LibraryResponseDto;
export let onCancel: () => void;
export let onSubmit: (library: LibraryResponseDto) => void;
let addImportPath = false; let addImportPath = false;
let editImportPath: number | null = null; let editImportPath: number | null = null;
@ -65,19 +67,6 @@
} }
}; };
const dispatch = createEventDispatcher<{
cancel: void;
submit: Partial<LibraryResponseDto>;
}>();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library });
};
const handleAddImportPath = async () => { const handleAddImportPath = async () => {
if (!addImportPath || !importPathToAdd) { if (!addImportPath || !importPathToAdd) {
return; return;
@ -153,8 +142,8 @@
submitText={$t('add')} submitText={$t('add')}
bind:importPath={importPathToAdd} bind:importPath={importPathToAdd}
{importPaths} {importPaths}
on:submit={handleAddImportPath} onSubmit={handleAddImportPath}
on:cancel={() => { onCancel={() => {
addImportPath = false; addImportPath = false;
importPathToAdd = null; importPathToAdd = null;
}} }}
@ -168,15 +157,13 @@
isEditing={true} isEditing={true}
bind:importPath={editedImportPath} bind:importPath={editedImportPath}
{importPaths} {importPaths}
on:submit={handleEditImportPath} onSubmit={handleEditImportPath}
on:delete={handleDeleteImportPath} onDelete={handleDeleteImportPath}
on:cancel={() => { onCancel={() => (editImportPath = null)}
editImportPath = null;
}}
/> />
{/if} {/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4"> <form on:submit|preventDefault={() => onSubmit({ ...library })} autocomplete="off" class="m-4 flex flex-col gap-4">
<table class="text-left"> <table class="text-left">
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray"> <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each validatedPaths as validatedPath, listIndex} {#each validatedPaths as validatedPath, listIndex}
@ -251,7 +238,7 @@
> >
</div> </div>
<div class="justify-end gap-2"> <div class="justify-end gap-2">
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button> <Button size="sm" color="gray" on:click={onCancel}>{$t('cancel')}</Button>
<Button size="sm" type="submit">{$t('save')}</Button> <Button size="sm" type="submit">{$t('save')}</Button>
</div> </div>
</div> </div>

View File

@ -1,31 +1,20 @@
<script lang="ts"> <script lang="ts">
import type { LibraryResponseDto } from '@immich/sdk'; import type { LibraryResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let library: Partial<LibraryResponseDto>; export let library: Partial<LibraryResponseDto>;
export let onCancel: () => void;
const dispatch = createEventDispatcher<{ export let onSubmit: (library: Partial<LibraryResponseDto>) => void;
cancel: void;
submit: Partial<LibraryResponseDto>;
}>();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', { ...library });
};
</script> </script>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-2"> <form on:submit|preventDefault={() => onSubmit({ ...library })} autocomplete="off" class="m-4 flex flex-col gap-2">
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<label class="immich-form-label" for="path">{$t('name')}</label> <label class="immich-form-label" for="path">{$t('name')}</label>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} /> <input class="immich-form-input" id="name" name="name" type="text" bind:value={library.name} />
</div> </div>
<div class="flex w-full justify-end gap-2 pt-2"> <div class="flex w-full justify-end gap-2 pt-2">
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button> <Button size="sm" color="gray" on:click={onCancel}>{$t('cancel')}</Button>
<Button size="sm" type="submit">{$t('save')}</Button> <Button size="sm" type="submit">{$t('save')}</Button>
</div> </div>
</form> </form>

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { type LibraryResponseDto } from '@immich/sdk'; import { type LibraryResponseDto } from '@immich/sdk';
import { mdiPencilOutline } from '@mdi/js'; import { mdiPencilOutline } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte'; import LibraryExclusionPatternForm from './library-exclusion-pattern-form.svelte';
@ -9,6 +9,8 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let library: Partial<LibraryResponseDto>; export let library: Partial<LibraryResponseDto>;
export let onCancel: () => void;
export let onSubmit: (library: Partial<LibraryResponseDto>) => void;
let addExclusionPattern = false; let addExclusionPattern = false;
let editExclusionPattern: number | null = null; let editExclusionPattern: number | null = null;
@ -26,18 +28,6 @@
} }
}); });
const dispatch = createEventDispatcher<{
cancel: void;
submit: Partial<LibraryResponseDto>;
}>();
const handleCancel = () => {
dispatch('cancel');
};
const handleSubmit = () => {
dispatch('submit', library);
};
const handleAddExclusionPattern = () => { const handleAddExclusionPattern = () => {
if (!addExclusionPattern) { if (!addExclusionPattern) {
return; return;
@ -106,10 +96,8 @@
submitText={$t('add')} submitText={$t('add')}
bind:exclusionPattern={exclusionPatternToAdd} bind:exclusionPattern={exclusionPatternToAdd}
{exclusionPatterns} {exclusionPatterns}
on:submit={handleAddExclusionPattern} onSubmit={handleAddExclusionPattern}
on:cancel={() => { onCancel={() => (addExclusionPattern = false)}
addExclusionPattern = false;
}}
/> />
{/if} {/if}
@ -119,15 +107,13 @@
isEditing={true} isEditing={true}
bind:exclusionPattern={editedExclusionPattern} bind:exclusionPattern={editedExclusionPattern}
{exclusionPatterns} {exclusionPatterns}
on:submit={handleEditExclusionPattern} onSubmit={handleEditExclusionPattern}
on:delete={handleDeleteExclusionPattern} onDelete={handleDeleteExclusionPattern}
on:cancel={() => { onCancel={() => (editExclusionPattern = null)}
editExclusionPattern = null;
}}
/> />
{/if} {/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" class="m-4 flex flex-col gap-4"> <form on:submit|preventDefault={() => onSubmit(library)} autocomplete="off" class="m-4 flex flex-col gap-4">
<table class="w-full text-left"> <table class="w-full text-left">
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray"> <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
{#each exclusionPatterns as exclusionPattern, listIndex} {#each exclusionPatterns as exclusionPattern, listIndex}
@ -178,7 +164,7 @@
</table> </table>
<div class="flex w-full justify-end gap-4"> <div class="flex w-full justify-end gap-4">
<Button size="sm" color="gray" on:click={() => handleCancel()}>{$t('cancel')}</Button> <Button size="sm" color="gray" on:click={onCancel}>{$t('cancel')}</Button>
<Button size="sm" type="submit">{$t('save')}</Button> <Button size="sm" type="submit">{$t('save')}</Button>
</div> </div>
</form> </form>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte'; import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js'; import { mdiFolderSync } from '@mdi/js';
@ -9,6 +8,9 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
export let onCancel: () => void;
export let onSubmit: (ownerId: string) => void;
let ownerId: string = $user.id; let ownerId: string = $user.id;
let userOptions: { value: string; text: string }[] = []; let userOptions: { value: string; text: string }[] = [];
@ -17,25 +19,16 @@
const users = await searchUsersAdmin({}); const users = await searchUsersAdmin({});
userOptions = users.map((user) => ({ value: user.id, text: user.name })); userOptions = users.map((user) => ({ value: user.id, text: user.name }));
}); });
const dispatch = createEventDispatcher<{
cancel: void;
submit: { ownerId: string };
delete: void;
}>();
const handleCancel = () => dispatch('cancel');
const handleSubmit = () => dispatch('submit', { ownerId });
</script> </script>
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}> <FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={onCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form"> <form on:submit|preventDefault={() => onSubmit(ownerId)} autocomplete="off" id="select-library-owner-form">
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p> <p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" /> <SettingSelect bind:value={ownerId} options={userOptions} name="user" />
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{$t('cancel')}</Button> <Button color="gray" fullwidth on:click={onCancel}>{$t('cancel')}</Button>
<Button type="submit" fullwidth form="select-library-owner-form">{$t('create')}</Button> <Button type="submit" fullwidth form="select-library-owner-form">{$t('create')}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -21,7 +21,7 @@
<header> <header>
{#if !hideNavbar} {#if !hideNavbar}
<NavigationBar {showUploadButton} on:uploadClicked={() => openFileUploadDialog()} /> <NavigationBar {showUploadButton} onUploadClick={() => openFileUploadDialog()} />
{/if} {/if}
<slot name="header" /> <slot name="header" />

View File

@ -4,7 +4,6 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import type { MapSettings } from '$lib/stores/preferences.store'; import type { MapSettings } from '$lib/stores/preferences.store';
import { Duration } from 'luxon'; import { Duration } from 'luxon';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
@ -12,19 +11,15 @@
import DateInput from '../elements/date-input.svelte'; import DateInput from '../elements/date-input.svelte';
export let settings: MapSettings; export let settings: MapSettings;
export let onClose: () => void;
export let onSave: (settings: MapSettings) => void;
let customDateRange = !!settings.dateAfter || !!settings.dateBefore; let customDateRange = !!settings.dateAfter || !!settings.dateBefore;
const dispatch = createEventDispatcher<{
close: void;
save: MapSettings;
}>();
const handleClose = () => dispatch('close');
</script> </script>
<FullScreenModal title={$t('map_settings')} onClose={handleClose}> <FullScreenModal title={$t('map_settings')} {onClose}>
<form <form
on:submit|preventDefault={() => dispatch('save', settings)} on:submit|preventDefault={() => onSave(settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary" class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
id="map-settings-form" id="map-settings-form"
> >
@ -108,7 +103,7 @@
{/if} {/if}
</form> </form>
<svelte:fragment slot="sticky-bottom"> <svelte:fragment slot="sticky-bottom">
<Button color="gray" size="sm" fullwidth on:click={handleClose}>{$t('cancel')}</Button> <Button color="gray" size="sm" fullwidth on:click={onClose}>{$t('cancel')}</Button>
<Button type="submit" size="sm" fullwidth form="map-settings-form">{$t('save')}</Button> <Button type="submit" size="sm" fullwidth form="map-settings-form">{$t('save')}</Button>
</svelte:fragment> </svelte:fragment>
</FullScreenModal> </FullScreenModal>

View File

@ -250,7 +250,7 @@
<section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}> <section id="memory-viewer" class="w-full bg-immich-dark-gray" bind:this={memoryWrapper}>
{#if current && current.memory.assets.length > 0} {#if current && current.memory.assets.length > 0}
<ControlAppBar on:close={() => goto(AppRoute.PHOTOS)} forceDark> <ControlAppBar onClose={() => goto(AppRoute.PHOTOS)} forceDark>
<svelte:fragment slot="leading"> <svelte:fragment slot="leading">
<p class="text-lg"> <p class="text-lg">
{$memoryLaneTitle(current.memory.yearsAgo)} {$memoryLaneTitle(current.memory.yearsAgo)}

View File

@ -40,8 +40,8 @@
{#if showAlbumPicker} {#if showAlbumPicker}
<AlbumSelectionModal <AlbumSelectionModal
{shared} {shared}
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)} onNewAlbum={handleAddToNewAlbum}
on:album={({ detail }) => handleAddToAlbum(detail)} onAlbumClick={handleAddToAlbum}
onClose={handleHideAlbumPicker} onClose={handleHideAlbumPicker}
/> />
{/if} {/if}

View File

@ -31,9 +31,5 @@
<MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} onClick={() => (isShowChangeDate = true)} /> <MenuOption text={$t('change_date')} icon={mdiCalendarEditOutline} onClick={() => (isShowChangeDate = true)} />
{/if} {/if}
{#if isShowChangeDate} {#if isShowChangeDate}
<ChangeDate <ChangeDate initialDate={DateTime.now()} onConfirm={handleConfirm} onCancel={() => (isShowChangeDate = false)} />
initialDate={DateTime.now()}
on:confirm={({ detail: date }) => handleConfirm(date)}
on:cancel={() => (isShowChangeDate = false)}
/>
{/if} {/if}

View File

@ -35,8 +35,5 @@
/> />
{/if} {/if}
{#if isShowChangeLocation} {#if isShowChangeLocation}
<ChangeLocation <ChangeLocation onConfirm={handleConfirm} onCancel={() => (isShowChangeLocation = false)} />
on:confirm={({ detail: point }) => handleConfirm(point)}
on:cancel={() => (isShowChangeLocation = false)}
/>
{/if} {/if}

View File

@ -49,7 +49,7 @@
{#if isShowConfirmation} {#if isShowConfirmation}
<DeleteAssetDialog <DeleteAssetDialog
size={getOwnedAssets().size} size={getOwnedAssets().size}
on:confirm={handleDelete} onConfirm={handleDelete}
on:cancel={() => (isShowConfirmation = false)} onCancel={() => (isShowConfirmation = false)}
/> />
{/if} {/if}

View File

@ -8,7 +8,7 @@
import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util'; import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk'; import type { AssetResponseDto } from '@immich/sdk';
import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js'; import { mdiCheckCircle, mdiCircleOutline } from '@mdi/js';
import { createEventDispatcher, onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; import Thumbnail from '../assets/thumbnail/thumbnail.svelte';
import { TUNABLES } from '$lib/utils/tunables'; import { TUNABLES } from '$lib/utils/tunables';
@ -29,6 +29,9 @@
export let onScrollTarget: ScrollTargetListener | undefined = undefined; export let onScrollTarget: ScrollTargetListener | undefined = undefined;
export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined; export let onAssetInGrid: ((asset: AssetResponseDto) => void) | undefined = undefined;
export let onSelect: ({ title, assets }: { title: string; assets: AssetResponseDto[] }) => void;
export let onSelectAssets: (asset: AssetResponseDto) => void;
export let onSelectAssetCandidates: (asset: AssetResponseDto | null) => void;
const componentId = generateId(); const componentId = generateId();
$: bucketDate = bucket.bucketDate; $: bucketDate = bucket.bucketDate;
@ -41,11 +44,6 @@
const TITLE_HEIGHT = 51; const TITLE_HEIGHT = 51;
const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore; const { selectedGroup, selectedAssets, assetSelectionCandidates, isMultiSelectState } = assetInteractionStore;
const dispatch = createEventDispatcher<{
select: { title: string; assets: AssetResponseDto[] };
selectAssets: AssetResponseDto;
selectAssetCandidates: AssetResponseDto | null;
}>();
let isMouseOverGroup = false; let isMouseOverGroup = false;
let hoveredDateGroup = ''; let hoveredDateGroup = '';
@ -65,10 +63,10 @@
} }
}; };
const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets }); const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => onSelect({ title, assets });
const assetSelectHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => { const assetSelectHandler = (asset: AssetResponseDto, assetsInDateGroup: AssetResponseDto[], groupTitle: string) => {
dispatch('selectAssets', asset); onSelectAssets(asset);
// Check if all assets are selected in a group to toggle the group selection's icon // Check if all assets are selected in a group to toggle the group selection's icon
let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length; let selectedAssetsInGroupCount = assetsInDateGroup.filter((asset) => $selectedAssets.has(asset)).length;
@ -86,7 +84,7 @@
hoveredDateGroup = groupTitle; hoveredDateGroup = groupTitle;
if ($isMultiSelectState) { if ($isMultiSelectState) {
dispatch('selectAssetCandidates', asset); onSelectAssetCandidates(asset);
} }
}; };

View File

@ -28,7 +28,7 @@
import { TUNABLES } from '$lib/utils/tunables'; import { TUNABLES } from '$lib/utils/tunables';
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
import { throttle } from 'lodash-es'; import { throttle } from 'lodash-es';
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import Portal from '../shared-components/portal/portal.svelte'; import Portal from '../shared-components/portal/portal.svelte';
import Scrubber from '../shared-components/scrubber/scrubber.svelte'; import Scrubber from '../shared-components/scrubber/scrubber.svelte';
import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte';
@ -64,6 +64,8 @@
export let isShared = false; export let isShared = false;
export let album: AlbumResponseDto | null = null; export let album: AlbumResponseDto | null = null;
export let isShowDeleteConfirmation = false; export let isShowDeleteConfirmation = false;
export let onSelect: (asset: AssetResponseDto) => void = () => {};
export let onEscape: () => void = () => {};
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore; let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
@ -127,8 +129,6 @@
}, },
} = TUNABLES; } = TUNABLES;
const dispatch = createEventDispatcher<{ select: AssetResponseDto; escape: void }>();
const isViewportOrigin = () => { const isViewportOrigin = () => {
return viewport.height === 0 && viewport.width === 0; return viewport.height === 0 && viewport.width === 0;
}; };
@ -447,7 +447,7 @@
const ids = await stackAssets(Array.from($selectedAssets)); const ids = await stackAssets(Array.from($selectedAssets));
if (ids) { if (ids) {
$assetStore.removeAssets(ids); $assetStore.removeAssets(ids);
dispatch('escape'); onEscape();
} }
}; };
@ -471,7 +471,7 @@
} }
const shortcuts: ShortcutOptions[] = [ const shortcuts: ShortcutOptions[] = [
{ shortcut: { key: 'Escape' }, onShortcut: () => dispatch('escape') }, { shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) }, { shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) }, { shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) }, { shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteractionStore) },
@ -539,7 +539,7 @@
return !!nextAsset; return !!nextAsset;
}; };
const handleClose = async ({ detail: { asset } }: { detail: { asset: AssetResponseDto } }) => { const handleClose = async ({ asset }: { asset: AssetResponseDto }) => {
assetViewingStore.showAssetViewer(false); assetViewingStore.showAssetViewer(false);
showSkeleton = true; showSkeleton = true;
$gridScrollTarget = { at: asset.id }; $gridScrollTarget = { at: asset.id };
@ -554,7 +554,7 @@
case AssetAction.DELETE: { case AssetAction.DELETE: {
// find the next asset to show or close the viewer // find the next asset to show or close the viewer
// eslint-disable-next-line @typescript-eslint/no-unused-expressions // eslint-disable-next-line @typescript-eslint/no-unused-expressions
(await handleNext()) || (await handlePrevious()) || (await handleClose({ detail: { asset: action.asset } })); (await handleNext()) || (await handlePrevious()) || (await handleClose({ asset: action.asset }));
// delete after find the next one // delete after find the next one
assetStore.removeAssets([action.asset.id]); assetStore.removeAssets([action.asset.id]);
@ -649,7 +649,7 @@
return; return;
} }
dispatch('select', asset); onSelect(asset);
if (singleSelect) { if (singleSelect) {
element.scrollTop = 0; element.scrollTop = 0;
@ -754,8 +754,8 @@
{#if isShowDeleteConfirmation} {#if isShowDeleteConfirmation}
<DeleteAssetDialog <DeleteAssetDialog
size={idsSelectedAssets.length} size={idsSelectedAssets.length}
on:cancel={() => (isShowDeleteConfirmation = false)} onCancel={() => (isShowDeleteConfirmation = false)}
on:confirm={() => handlePromiseError(trashOrDelete(true))} onConfirm={() => handlePromiseError(trashOrDelete(true))}
/> />
{/if} {/if}
@ -847,9 +847,9 @@
{onAssetInGrid} {onAssetInGrid}
{bucket} {bucket}
viewport={safeViewport} viewport={safeViewport}
on:select={({ detail: group }) => handleGroupSelect(group.title, group.assets)} onSelect={({ title, assets }) => handleGroupSelect(title, assets)}
on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)} onSelectAssetCandidates={handleSelectAssetCandidates}
on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)} onSelectAssets={handleSelectAssets}
/> />
{/if} {/if}
</div> </div>
@ -869,9 +869,9 @@
{isShared} {isShared}
{album} {album}
onAction={handleAction} onAction={handleAction}
on:previous={handlePrevious} onPrevious={handlePrevious}
on:next={handleNext} onNext={handleNext}
on:close={handleClose} onClose={handleClose}
/> />
{/await} {/await}
{/if} {/if}

View File

@ -30,7 +30,7 @@
}); });
</script> </script>
<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md"> <ControlAppBar onClose={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
<div class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading"> <div class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
<p class="block sm:hidden">{assets.size}</p> <p class="block sm:hidden">{assets.size}</p>
<p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p> <p class="hidden sm:block">{$t('selected_count', { values: { count: assets.size } })}</p>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte'; import ConfirmDialog from '../shared-components/dialog/confirm-dialog.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store'; import { showDeleteModal } from '$lib/stores/preferences.store';
import Checkbox from '$lib/components/elements/checkbox.svelte'; import Checkbox from '$lib/components/elements/checkbox.svelte';
@ -7,19 +6,16 @@
import FormatMessage from '$lib/components/i18n/format-message.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte';
export let size: number; export let size: number;
export let onConfirm: () => void;
export let onCancel: () => void;
let checked = false; let checked = false;
const dispatch = createEventDispatcher<{
confirm: void;
cancel: void;
}>();
const handleConfirm = () => { const handleConfirm = () => {
if (checked) { if (checked) {
$showDeleteModal = false; $showDeleteModal = false;
} }
dispatch('confirm'); onConfirm();
}; };
</script> </script>
@ -27,7 +23,7 @@
title={$t('permanently_delete_assets_count', { values: { count: size } })} title={$t('permanently_delete_assets_count', { values: { count: size } })}
confirmText={$t('delete')} confirmText={$t('delete')}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onCancel={() => dispatch('cancel')} {onCancel}
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<p> <p>

View File

@ -2,7 +2,7 @@
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk'; import { getAllAlbums, type AlbumResponseDto } from '@immich/sdk';
import { mdiPlus } from '@mdi/js'; import { mdiPlus } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import AlbumListItem from '../asset-viewer/album-list-item.svelte'; import AlbumListItem from '../asset-viewer/album-list-item.svelte';
import { normalizeSearchString } from '$lib/utils/string-utils'; import { normalizeSearchString } from '$lib/utils/string-utils';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
@ -11,17 +11,15 @@
import { sortAlbums } from '$lib/utils/album-utils'; import { sortAlbums } from '$lib/utils/album-utils';
import { albumViewSettings } from '$lib/stores/preferences.store'; import { albumViewSettings } from '$lib/stores/preferences.store';
export let onNewAlbum: (search: string) => void;
export let onAlbumClick: (album: AlbumResponseDto) => void;
let albums: AlbumResponseDto[] = []; let albums: AlbumResponseDto[] = [];
let recentAlbums: AlbumResponseDto[] = []; let recentAlbums: AlbumResponseDto[] = [];
let filteredAlbums: AlbumResponseDto[] = []; let filteredAlbums: AlbumResponseDto[] = [];
let loading = true; let loading = true;
let search = ''; let search = '';
const dispatch = createEventDispatcher<{
newAlbum: string;
album: AlbumResponseDto;
}>();
export let shared: boolean; export let shared: boolean;
export let onClose: () => void; export let onClose: () => void;
@ -40,14 +38,6 @@
{ sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder }, { sortBy: $albumViewSettings.sortBy, orderBy: $albumViewSettings.sortOrder },
); );
const handleSelect = (album: AlbumResponseDto) => {
dispatch('album', album);
};
const handleNew = () => {
dispatch('newAlbum', search.length > 0 ? search : '');
};
const getTitle = () => { const getTitle = () => {
if (shared) { if (shared) {
return $t('add_to_shared_album'); return $t('add_to_shared_album');
@ -81,7 +71,7 @@
<div class="immich-scrollbar overflow-y-auto"> <div class="immich-scrollbar overflow-y-auto">
<button <button
type="button" type="button"
on:click={handleNew} on:click={() => onNewAlbum(search)}
class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl" class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
> >
<div class="flex h-12 w-12 items-center justify-center"> <div class="flex h-12 w-12 items-center justify-center">
@ -96,7 +86,7 @@
{#if !shared && search.length === 0} {#if !shared && search.length === 0}
<p class="px-5 py-3 text-xs">{$t('recent').toUpperCase()}</p> <p class="px-5 py-3 text-xs">{$t('recent').toUpperCase()}</p>
{#each recentAlbums as album (album.id)} {#each recentAlbums as album (album.id)}
<AlbumListItem {album} on:album={() => handleSelect(album)} /> <AlbumListItem {album} onAlbumClick={() => onAlbumClick(album)} />
{/each} {/each}
{/if} {/if}
@ -106,7 +96,7 @@
</p> </p>
{/if} {/if}
{#each filteredAlbums as album (album.id)} {#each filteredAlbums as album (album.id)}
<AlbumListItem {album} searchQuery={search} on:album={() => handleSelect(album)} /> <AlbumListItem {album} searchQuery={search} onAlbumClick={() => onAlbumClick(album)} />
{/each} {/each}
{:else if albums.length > 0} {:else if albums.length > 0}
<p class="px-5 py-1 text-sm">{$t('no_albums_with_name_yet')}</p> <p class="px-5 py-1 text-sm">{$t('no_albums_with_name_yet')}</p>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import ConfirmDialog from './dialog/confirm-dialog.svelte'; import ConfirmDialog from './dialog/confirm-dialog.svelte';
import Combobox from './combobox.svelte'; import Combobox from './combobox.svelte';
@ -8,6 +7,8 @@
export let initialDate: DateTime = DateTime.now(); export let initialDate: DateTime = DateTime.now();
export let initialTimeZone: string = ''; export let initialTimeZone: string = '';
export let onCancel: () => void;
export let onConfirm: (date: string) => void;
type ZoneOption = { type ZoneOption = {
/** /**
@ -118,17 +119,10 @@
return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' }); return zoneA.value.localeCompare(zoneB.value, undefined, { sensitivity: 'base' });
} }
const dispatch = createEventDispatcher<{
cancel: void;
confirm: string;
}>();
const handleCancel = () => dispatch('cancel');
const handleConfirm = () => { const handleConfirm = () => {
const value = date.toISO(); const value = date.toISO();
if (value) { if (value) {
dispatch('confirm', value); onConfirm(value);
} }
}; };
</script> </script>
@ -139,7 +133,7 @@
prompt="Please select a new date:" prompt="Please select a new date:"
disabled={!date.isValid} disabled={!date.isValid}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onCancel={handleCancel} {onCancel}
> >
<div class="flex flex-col text-left gap-2" slot="prompt"> <div class="flex flex-col text-left gap-2" slot="prompt">
<div class="flex flex-col"> <div class="flex flex-col">

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte';
import ConfirmDialog from './dialog/confirm-dialog.svelte'; import ConfirmDialog from './dialog/confirm-dialog.svelte';
import { timeDebounceOnSearch } from '$lib/constants'; import { timeDebounceOnSearch } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@ -14,13 +13,15 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte'; import CoordinatesInput from '$lib/components/shared-components/coordinates-input.svelte';
export let asset: AssetResponseDto | undefined = undefined;
interface Point { interface Point {
lng: number; lng: number;
lat: number; lat: number;
} }
export let asset: AssetResponseDto | undefined = undefined;
export let onCancel: () => void;
export let onConfirm: (point: Point) => void;
let places: PlacesResponseDto[] = []; let places: PlacesResponseDto[] = [];
let suggestedPlaces: PlacesResponseDto[] = []; let suggestedPlaces: PlacesResponseDto[] = [];
let searchWord: string; let searchWord: string;
@ -30,11 +31,6 @@
let hideSuggestion = false; let hideSuggestion = false;
let addClipMapMarker: (long: number, lat: number) => void; let addClipMapMarker: (long: number, lat: number) => void;
const dispatch = createEventDispatcher<{
cancel: void;
confirm: Point;
}>();
$: lat = asset?.exifInfo?.latitude ?? undefined; $: lat = asset?.exifInfo?.latitude ?? undefined;
$: lng = asset?.exifInfo?.longitude ?? undefined; $: lng = asset?.exifInfo?.longitude ?? undefined;
$: zoom = lat !== undefined && lng !== undefined ? 12.5 : 1; $: zoom = lat !== undefined && lng !== undefined ? 12.5 : 1;
@ -50,17 +46,11 @@
let point: Point | null = null; let point: Point | null = null;
const handleCancel = () => dispatch('cancel');
const handleSelect = (selected: Point) => {
point = selected;
};
const handleConfirm = () => { const handleConfirm = () => {
if (point) { if (point) {
dispatch('confirm', point); onConfirm(point);
} else { } else {
dispatch('cancel'); onCancel();
} }
}; };
@ -108,13 +98,7 @@
}; };
</script> </script>
<ConfirmDialog <ConfirmDialog confirmColor="primary" title={$t('change_location')} width="wide" onConfirm={handleConfirm} {onCancel}>
confirmColor="primary"
title={$t('change_location')}
width="wide"
onConfirm={handleConfirm}
onCancel={handleCancel}
>
<div slot="prompt" class="flex flex-col w-full h-full gap-2"> <div slot="prompt" class="flex flex-col w-full h-full gap-2">
<div <div
class="relative w-64 sm:w-96" class="relative w-64 sm:w-96"
@ -126,10 +110,8 @@
placeholder={$t('search_places')} placeholder={$t('search_places')}
bind:name={searchWord} bind:name={searchWord}
{showLoadingSpinner} {showLoadingSpinner}
on:reset={() => { onReset={() => (suggestedPlaces = [])}
suggestedPlaces = []; onSearch={handleSearchPlaces}
}}
on:search={handleSearchPlaces}
roundedBottom={suggestedPlaces.length === 0 || hideSuggestion} roundedBottom={suggestedPlaces.length === 0 || hideSuggestion}
/> />
</button> </button>
@ -180,7 +162,7 @@
center={lat && lng ? { lat, lng } : undefined} center={lat && lng ? { lat, lng } : undefined}
simplified={true} simplified={true}
clickable={true} clickable={true}
on:clickedPoint={({ detail: point }) => handleSelect(point)} onClickPoint={(selected) => (point = selected)}
/> />
{/await} {/await}
</div> </div>

View File

@ -21,7 +21,7 @@
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js'; import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js';
import { createEventDispatcher, tick } from 'svelte'; import { tick } from 'svelte';
import type { FormEventHandler } from 'svelte/elements'; import type { FormEventHandler } from 'svelte/elements';
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import { focusOutside } from '$lib/actions/focus-outside'; import { focusOutside } from '$lib/actions/focus-outside';
@ -35,6 +35,7 @@
export let options: ComboBoxOption[] = []; export let options: ComboBoxOption[] = [];
export let selectedOption: ComboBoxOption | undefined = undefined; export let selectedOption: ComboBoxOption | undefined = undefined;
export let placeholder = ''; export let placeholder = '';
export let onSelect: (option: ComboBoxOption | undefined) => void = () => {};
/** /**
* Unique identifier for the combobox. * Unique identifier for the combobox.
@ -61,10 +62,6 @@
searchQuery = selectedOption ? selectedOption.label : ''; searchQuery = selectedOption ? selectedOption.label : '';
} }
const dispatch = createEventDispatcher<{
select: ComboBoxOption | undefined;
}>();
const activate = () => { const activate = () => {
isActive = true; isActive = true;
searchQuery = ''; searchQuery = '';
@ -105,10 +102,10 @@
optionRefs[0]?.scrollIntoView({ block: 'nearest' }); optionRefs[0]?.scrollIntoView({ block: 'nearest' });
}; };
let onSelect = (option: ComboBoxOption) => { let handleSelect = (option: ComboBoxOption) => {
selectedOption = option; selectedOption = option;
searchQuery = option.label; searchQuery = option.label;
dispatch('select', option); onSelect(option);
closeDropdown(); closeDropdown();
}; };
@ -117,7 +114,7 @@
selectedIndex = undefined; selectedIndex = undefined;
selectedOption = undefined; selectedOption = undefined;
searchQuery = ''; searchQuery = '';
dispatch('select', selectedOption); onSelect(selectedOption);
}; };
</script> </script>
@ -188,7 +185,7 @@
shortcut: { key: 'Enter' }, shortcut: { key: 'Enter' },
onShortcut: () => { onShortcut: () => {
if (selectedIndex !== undefined && filteredOptions.length > 0) { if (selectedIndex !== undefined && filteredOptions.length > 0) {
onSelect(filteredOptions[selectedIndex]); handleSelect(filteredOptions[selectedIndex]);
} }
closeDropdown(); closeDropdown();
}, },
@ -245,7 +242,7 @@
bind:this={optionRefs[index]} bind:this={optionRefs[index]}
class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700" class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 transition-all cursor-pointer aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${index}`} id={`${listboxId}-${index}`}
on:click={() => onSelect(option)} on:click={() => handleSelect(option)}
role="option" role="option"
> >
{option.label} {option.label}

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { mdiClose } from '@mdi/js'; import { mdiClose } from '@mdi/js';
@ -12,13 +12,10 @@
export let backIcon = mdiClose; export let backIcon = mdiClose;
export let tailwindClasses = ''; export let tailwindClasses = '';
export let forceDark = false; export let forceDark = false;
export let onClose: () => void = () => {};
let appBarBorder = 'bg-immich-bg border border-transparent'; let appBarBorder = 'bg-immich-bg border border-transparent';
const dispatch = createEventDispatcher<{
close: void;
}>();
const onScroll = () => { const onScroll = () => {
if (window.pageYOffset > 80) { if (window.pageYOffset > 80) {
appBarBorder = 'border border-gray-200 bg-gray-50 dark:border-gray-600'; appBarBorder = 'border border-gray-200 bg-gray-50 dark:border-gray-600';
@ -33,7 +30,7 @@
const handleClose = () => { const handleClose = () => {
$isSelectingAllAssets = false; $isSelectingAllAssets = false;
dispatch('close'); onClose();
}; };
onMount(() => { onMount(() => {

View File

@ -7,7 +7,6 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk'; import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiContentCopy, mdiLink } from '@mdi/js'; import { mdiContentCopy, mdiLink } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { NotificationType, notificationController } from '../notification/notification'; import { NotificationType, notificationController } from '../notification/notification';
import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte'; import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte';
import SettingSwitch from '../settings/setting-switch.svelte'; import SettingSwitch from '../settings/setting-switch.svelte';
@ -21,6 +20,7 @@
export let albumId: string | undefined = undefined; export let albumId: string | undefined = undefined;
export let assetIds: string[] = []; export let assetIds: string[] = [];
export let editingLink: SharedLinkResponseDto | undefined = undefined; export let editingLink: SharedLinkResponseDto | undefined = undefined;
export let onCreated: () => void = () => {};
let sharedLink: string | null = null; let sharedLink: string | null = null;
let description = ''; let description = '';
@ -32,10 +32,6 @@
let shouldChangeExpirationTime = false; let shouldChangeExpirationTime = false;
let enablePassword = false; let enablePassword = false;
const dispatch = createEventDispatcher<{
created: void;
}>();
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [ const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
[30, 'minutes'], [30, 'minutes'],
[1, 'hour'], [1, 'hour'],
@ -97,7 +93,7 @@
}, },
}); });
sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key); sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
dispatch('created'); onCreated();
} catch (error) { } catch (error) {
handleError(error, $t('errors.failed_to_create_shared_link')); handleError(error, $t('errors.failed_to_create_shared_link'));
} }

View File

@ -163,9 +163,9 @@
<AssetViewer <AssetViewer
asset={$viewingAsset} asset={$viewingAsset}
onAction={handleAction} onAction={handleAction}
on:previous={handlePrevious} onPrevious={handlePrevious}
on:next={handleNext} onNext={handleNext}
on:close={() => { onClose={() => {
assetViewingStore.showAssetViewer(false); assetViewingStore.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}} }}

View File

@ -13,7 +13,6 @@
import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson'; import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson';
import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl'; import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl';
import maplibregl from 'maplibre-gl'; import maplibregl from 'maplibre-gl';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { import {
AttributionControl, AttributionControl,
@ -52,6 +51,8 @@
} }
export let onOpenInMapView: (() => Promise<void> | void) | undefined = undefined; export let onOpenInMapView: (() => Promise<void> | void) | undefined = undefined;
export let onSelect: (assetIds: string[]) => void = () => {};
export let onClickPoint: ({ lat, lng }: { lat: number; lng: number }) => void = () => {};
let map: maplibregl.Map; let map: maplibregl.Map;
let marker: maplibregl.Marker | null = null; let marker: maplibregl.Marker | null = null;
@ -62,16 +63,11 @@
key: getKey(), key: getKey(),
}) as Promise<StyleSpecification>)(); }) as Promise<StyleSpecification>)();
const dispatch = createEventDispatcher<{
selected: string[];
clickedPoint: { lat: number; lng: number };
}>();
function handleAssetClick(assetId: string, map: Map | null) { function handleAssetClick(assetId: string, map: Map | null) {
if (!map) { if (!map) {
return; return;
} }
dispatch('selected', [assetId]); onSelect([assetId]);
} }
async function handleClusterClick(clusterId: number, map: Map | null) { async function handleClusterClick(clusterId: number, map: Map | null) {
@ -82,13 +78,13 @@
const mapSource = map?.getSource('geojson') as GeoJSONSource; const mapSource = map?.getSource('geojson') as GeoJSONSource;
const leaves = await mapSource.getClusterLeaves(clusterId, 10_000, 0); const leaves = await mapSource.getClusterLeaves(clusterId, 10_000, 0);
const ids = leaves.map((leaf) => leaf.properties?.id); const ids = leaves.map((leaf) => leaf.properties?.id);
dispatch('selected', ids); onSelect(ids);
} }
function handleMapClick(event: maplibregl.MapMouseEvent) { function handleMapClick(event: maplibregl.MapMouseEvent) {
if (clickable) { if (clickable) {
const { lng, lat } = event.lngLat; const { lng, lat } = event.lngLat;
dispatch('clickedPoint', { lng, lat }); onClickPoint({ lng, lat });
if (marker) { if (marker) {
marker.remove(); marker.remove();

View File

@ -9,19 +9,16 @@
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk'; import { deleteProfileImage, updateMyPreferences, type UserAvatarColor } from '@immich/sdk';
import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js'; import { mdiCog, mdiLogout, mdiPencil, mdiWrench } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { NotificationType, notificationController } from '../notification/notification'; import { NotificationType, notificationController } from '../notification/notification';
import UserAvatar from '../user-avatar.svelte'; import UserAvatar from '../user-avatar.svelte';
import AvatarSelector from './avatar-selector.svelte'; import AvatarSelector from './avatar-selector.svelte';
let isShowSelectAvatar = false; export let onLogout: () => void;
export let onClose: () => void = () => {};
const dispatch = createEventDispatcher<{ let isShowSelectAvatar = false;
logout: void;
close: void;
}>();
const handleSaveProfile = async (color: UserAvatarColor) => { const handleSaveProfile = async (color: UserAvatarColor) => {
try { try {
@ -75,14 +72,7 @@
</div> </div>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<Button <Button href={AppRoute.USER_SETTINGS} on:click={onClose} color="dark-gray" size="sm" shadow={false} border>
href={AppRoute.USER_SETTINGS}
on:click={() => dispatch('close')}
color="dark-gray"
size="sm"
shadow={false}
border
>
<div class="flex place-content-center place-items-center text-center gap-2 px-2"> <div class="flex place-content-center place-items-center text-center gap-2 px-2">
<Icon path={mdiCog} size="18" ariaHidden /> <Icon path={mdiCog} size="18" ariaHidden />
{$t('account_settings')} {$t('account_settings')}
@ -91,7 +81,7 @@
{#if $user.isAdmin} {#if $user.isAdmin}
<Button <Button
href={AppRoute.ADMIN_USER_MANAGEMENT} href={AppRoute.ADMIN_USER_MANAGEMENT}
on:click={() => dispatch('close')} on:click={onClose}
color="dark-gray" color="dark-gray"
size="sm" size="sm"
shadow={false} shadow={false}
@ -111,7 +101,7 @@
<button <button
type="button" type="button"
class="flex w-full place-content-center place-items-center gap-2 py-3 font-medium text-gray-500 hover:bg-immich-primary/10 dark:text-gray-300" class="flex w-full place-content-center place-items-center gap-2 py-3 font-medium text-gray-500 hover:bg-immich-primary/10 dark:text-gray-300"
on:click={() => dispatch('logout')} on:click={onLogout}
> >
<Icon path={mdiLogout} size={24} /> <Icon path={mdiLogout} size={24} />
{$t('sign_out')}</button {$t('sign_out')}</button

View File

@ -10,7 +10,6 @@
import { handleLogout } from '$lib/utils/auth'; import { handleLogout } from '$lib/utils/auth';
import { logout } from '@immich/sdk'; import { logout } from '@immich/sdk';
import { mdiMagnify, mdiTrayArrowUp } from '@mdi/js'; import { mdiMagnify, mdiTrayArrowUp } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import { AppRoute } from '../../../constants'; import { AppRoute } from '../../../constants';
@ -21,13 +20,11 @@
import AccountInfoPanel from './account-info-panel.svelte'; import AccountInfoPanel from './account-info-panel.svelte';
export let showUploadButton = true; export let showUploadButton = true;
export let onUploadClick: () => void;
let shouldShowAccountInfo = false; let shouldShowAccountInfo = false;
let shouldShowAccountInfoPanel = false; let shouldShowAccountInfoPanel = false;
let innerWidth: number; let innerWidth: number;
const dispatch = createEventDispatcher<{
uploadClicked: void;
}>();
const onLogout = async () => { const onLogout = async () => {
const { redirectUri } = await logout(); const { redirectUri } = await logout();
@ -67,14 +64,14 @@
<ThemeButton padding="2" /> <ThemeButton padding="2" />
{#if !$page.url.pathname.includes('/admin') && showUploadButton} {#if !$page.url.pathname.includes('/admin') && showUploadButton}
<LinkButton on:click={() => dispatch('uploadClicked')} class="hidden lg:block"> <LinkButton on:click={onUploadClick} class="hidden lg:block">
<div class="flex gap-2"> <div class="flex gap-2">
<Icon path={mdiTrayArrowUp} size="1.5em" /> <Icon path={mdiTrayArrowUp} size="1.5em" />
<span>{$t('upload')}</span> <span>{$t('upload')}</span>
</div> </div>
</LinkButton> </LinkButton>
<CircleIconButton <CircleIconButton
on:click={() => dispatch('uploadClicked')} on:click={onUploadClick}
title={$t('upload')} title={$t('upload')}
icon={mdiTrayArrowUp} icon={mdiTrayArrowUp}
class="lg:hidden" class="lg:hidden"
@ -114,7 +111,7 @@
{/if} {/if}
{#if shouldShowAccountInfoPanel} {#if shouldShowAccountInfoPanel}
<AccountInfoPanel on:logout={onLogout} /> <AccountInfoPanel {onLogout} />
{/if} {/if}
</div> </div>
</section> </section>

View File

@ -8,7 +8,7 @@
<script lang="ts"> <script lang="ts">
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { createEventDispatcher, onMount } from 'svelte'; import { onMount } from 'svelte';
import { tweened } from 'svelte/motion'; import { tweened } from 'svelte/motion';
/** /**
@ -26,6 +26,10 @@
export let duration = 5; export let duration = 5;
export let onDone: () => void;
export let onPlaying: () => void = () => {};
export let onPaused: () => void = () => {};
const onChange = async () => { const onChange = async () => {
progress = setDuration(duration); progress = setDuration(duration);
await play(); await play();
@ -39,16 +43,10 @@
$: { $: {
if ($progress === 1) { if ($progress === 1) {
dispatch('done'); onDone();
} }
} }
const dispatch = createEventDispatcher<{
done: void;
playing: void;
paused: void;
}>();
onMount(async () => { onMount(async () => {
if (autoplay) { if (autoplay) {
await play(); await play();
@ -57,13 +55,13 @@
export const play = async () => { export const play = async () => {
status = ProgressBarStatus.Playing; status = ProgressBarStatus.Playing;
dispatch('playing'); onPlaying();
await progress.set(1); await progress.set(1);
}; };
export const pause = async () => { export const pause = async () => {
status = ProgressBarStatus.Paused; status = ProgressBarStatus.Paused;
dispatch('paused'); onPaused();
await progress.set($progress); await progress.set($progress);
}; };

View File

@ -43,7 +43,7 @@
icon: option.icon, icon: option.icon,
}; };
}} }}
on:select={({ detail }) => onToggle(detail)} onSelect={onToggle}
/> />
</div> </div>
</div> </div>

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import { mdiChevronDown } from '@mdi/js'; import { mdiChevronDown } from '@mdi/js';
@ -14,15 +13,14 @@
export let isEdited = false; export let isEdited = false;
export let number = false; export let number = false;
export let disabled = false; export let disabled = false;
export let onSelect: (setting: string | number) => void = () => {};
const dispatch = createEventDispatcher<{ select: string | number }>();
const handleChange = (e: Event) => { const handleChange = (e: Event) => {
value = (e.target as HTMLInputElement).value; value = (e.target as HTMLInputElement).value;
if (number) { if (number) {
value = Number.parseInt(value); value = Number.parseInt(value);
} }
dispatch('select', value); onSelect(value);
}; };
</script> </script>

View File

@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import { createEventDispatcher } from 'svelte';
import Slider from '$lib/components/elements/slider.svelte'; import Slider from '$lib/components/elements/slider.svelte';
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -11,14 +10,12 @@
export let checked = false; export let checked = false;
export let disabled = false; export let disabled = false;
export let isEdited = false; export let isEdited = false;
export let onToggle: (isChecked: boolean) => void = () => {};
let id: string = generateId(); let id: string = generateId();
$: sliderId = `${id}-slider`; $: sliderId = `${id}-slider`;
$: subtitleId = subtitle ? `${id}-subtitle` : undefined; $: subtitleId = subtitle ? `${id}-subtitle` : undefined;
const dispatch = createEventDispatcher<{ toggle: boolean }>();
const onToggle = (isChecked: boolean) => dispatch('toggle', isChecked);
</script> </script>
<div class="flex place-items-center justify-between"> <div class="flex place-items-center justify-between">

View File

@ -102,7 +102,7 @@
{/if} {/if}
{#if secret} {#if secret}
<APIKeySecret {secret} on:done={() => (secret = '')} /> <APIKeySecret {secret} onDone={() => (secret = '')} />
{/if} {/if}
{#if editKey} {#if editKey}

View File

@ -151,15 +151,15 @@
<AssetViewer <AssetViewer
asset={$viewingAsset} asset={$viewingAsset}
showNavigation={assets.length > 1} showNavigation={assets.length > 1}
on:next={() => { onNext={() => {
const index = getAssetIndex($viewingAsset.id) + 1; const index = getAssetIndex($viewingAsset.id) + 1;
setAsset(assets[index % assets.length]); setAsset(assets[index % assets.length]);
}} }}
on:previous={() => { onPrevious={() => {
const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; const index = getAssetIndex($viewingAsset.id) - 1 + assets.length;
setAsset(assets[index % assets.length]); setAsset(assets[index % assets.length]);
}} }}
on:close={() => { onClose={() => {
assetViewingStore.showAssetViewer(false); assetViewingStore.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}} }}

View File

@ -674,8 +674,8 @@
disabled={!album.isActivityEnabled} disabled={!album.isActivityEnabled}
{isLiked} {isLiked}
numberOfComments={$numberOfComments} numberOfComments={$numberOfComments}
on:favorite={handleFavorite} onFavorite={handleFavorite}
on:openActivityTab={handleOpenAndCloseActivityTab} onOpenActivityTab={handleOpenAndCloseActivityTab}
/> />
</div> </div>
{/if} {/if}
@ -697,10 +697,10 @@
albumId={album.id} albumId={album.id}
{isLiked} {isLiked}
bind:reactions bind:reactions
on:addComment={() => updateNumberOfComments(1)} onAddComment={() => updateNumberOfComments(1)}
on:deleteComment={() => updateNumberOfComments(-1)} onDeleteComment={() => updateNumberOfComments(-1)}
on:deleteLike={() => (isLiked = null)} onDeleteLike={() => (isLiked = null)}
on:close={handleOpenAndCloseActivityTab} onClose={handleOpenAndCloseActivityTab}
/> />
</div> </div>
</div> </div>
@ -709,8 +709,8 @@
{#if viewMode === ViewMode.SELECT_USERS} {#if viewMode === ViewMode.SELECT_USERS}
<UserSelectionModal <UserSelectionModal
{album} {album}
on:select={({ detail: users }) => handleAddUsers(users)} onSelect={handleAddUsers}
on:share={() => (viewMode = ViewMode.LINK_SHARING)} onShare={() => (viewMode = ViewMode.LINK_SHARING)}
onClose={() => (viewMode = ViewMode.VIEW)} onClose={() => (viewMode = ViewMode.VIEW)}
/> />
{/if} {/if}
@ -723,8 +723,8 @@
<ShareInfoModal <ShareInfoModal
onClose={() => (viewMode = ViewMode.VIEW)} onClose={() => (viewMode = ViewMode.VIEW)}
{album} {album}
on:remove={({ detail: userId }) => handleRemoveUser(userId)} onRemove={handleRemoveUser}
on:refreshAlbum={refreshAlbum} onRefreshAlbum={refreshAlbum}
/> />
{/if} {/if}
@ -737,9 +737,9 @@
albumOrder = order; albumOrder = order;
await setModeToView(); await setModeToView();
}} }}
on:close={() => (viewMode = ViewMode.VIEW)} onClose={() => (viewMode = ViewMode.VIEW)}
on:toggleEnableActivity={handleToggleEnableActivity} onToggleEnabledActivity={handleToggleEnableActivity}
on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} onShowSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)}
/> />
{/if} {/if}

View File

@ -122,9 +122,9 @@
<AssetViewer <AssetViewer
asset={$viewingAsset} asset={$viewingAsset}
showNavigation={viewingAssets.length > 1} showNavigation={viewingAssets.length > 1}
on:next={navigateNext} onNext={navigateNext}
on:previous={navigatePrevious} onPrevious={navigatePrevious}
on:close={() => { onClose={() => {
assetViewingStore.showAssetViewer(false); assetViewingStore.showAssetViewer(false);
handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}} }}
@ -137,11 +137,11 @@
{#if showSettingsModal} {#if showSettingsModal}
<MapSettingsModal <MapSettingsModal
settings={{ ...$mapSettings }} settings={{ ...$mapSettings }}
on:close={() => (showSettingsModal = false)} onClose={() => (showSettingsModal = false)}
on:save={async ({ detail }) => { onSave={async (settings) => {
const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode'));
showSettingsModal = false; showSettingsModal = false;
$mapSettings = detail; $mapSettings = settings;
if (shouldUpdate) { if (shouldUpdate) {
mapMarkers = await loadMapMarkers(); mapMarkers = await loadMapMarkers();

View File

@ -302,9 +302,9 @@
{personMerge1} {personMerge1}
{personMerge2} {personMerge2}
{potentialMergePeople} {potentialMergePeople}
on:close={() => (showMergeModal = false)} onClose={() => (showMergeModal = false)}
on:reject={() => changeName()} onReject={changeName}
on:confirm={(event) => handleMergeSamePerson(event.detail)} onConfirm={handleMergeSamePerson}
/> />
{/if} {/if}
@ -349,10 +349,10 @@
<PeopleCard <PeopleCard
{person} {person}
preload={index < 20} preload={index < 20}
on:change-name={() => handleChangeName(person)} onChangeName={() => handleChangeName(person)}
on:set-birth-date={() => handleSetBirthDate(person)} onSetBirthDate={() => handleSetBirthDate(person)}
on:merge-people={() => handleMergePeople(person)} onMergePeople={() => handleMergePeople(person)}
on:hide-person={() => handleHidePerson(person)} onHidePerson={() => handleHidePerson(person)}
/> />
</PeopleInfiniteScroll> </PeopleInfiniteScroll>
{:else} {:else}
@ -397,8 +397,8 @@
{#if showSetBirthDateModal} {#if showSetBirthDateModal}
<SetBirthDateModal <SetBirthDateModal
birthDate={edittingPerson?.birthDate ?? ''} birthDate={edittingPerson?.birthDate ?? ''}
on:close={() => (showSetBirthDateModal = false)} onClose={() => (showSetBirthDateModal = false)}
on:updated={(event) => submitBirthDateChange(event.detail)} onUpdate={submitBirthDateChange}
/> />
{/if} {/if}
</UserPageLayout> </UserPageLayout>

View File

@ -347,8 +347,8 @@
<UnMergeFaceSelector <UnMergeFaceSelector
assetIds={[...$selectedAssets].map((a) => a.id)} assetIds={[...$selectedAssets].map((a) => a.id)}
personAssets={person} personAssets={person}
on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}
on:confirm={handleUnmerge} onConfirm={handleUnmerge}
/> />
{/if} {/if}
@ -357,22 +357,22 @@
{personMerge1} {personMerge1}
{personMerge2} {personMerge2}
{potentialMergePeople} {potentialMergePeople}
on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}
on:reject={() => changeName()} onReject={changeName}
on:confirm={(event) => handleMergeSamePerson(event.detail)} onConfirm={handleMergeSamePerson}
/> />
{/if} {/if}
{#if viewMode === ViewMode.BIRTH_DATE} {#if viewMode === ViewMode.BIRTH_DATE}
<SetBirthDateModal <SetBirthDateModal
birthDate={person.birthDate ?? ''} birthDate={person.birthDate ?? ''}
on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} onClose={() => (viewMode = ViewMode.VIEW_ASSETS)}
on:updated={(event) => handleSetBirthDate(event.detail)} onUpdate={handleSetBirthDate}
/> />
{/if} {/if}
{#if viewMode === ViewMode.MERGE_PEOPLE} {#if viewMode === ViewMode.MERGE_PEOPLE}
<MergeFaceSelector {person} on:back={handleGoBack} on:merge={({ detail }) => handleMerge(detail)} /> <MergeFaceSelector {person} onBack={handleGoBack} onMerge={handleMerge} />
{/if} {/if}
<header> <header>
@ -464,7 +464,7 @@
bind:suggestedPeople bind:suggestedPeople
name={person.name} name={person.name}
bind:isSearchingPeople bind:isSearchingPeople
on:change={(event) => handleNameChange(event.detail)} onChange={handleNameChange}
{thumbnailData} {thumbnailData}
/> />
{:else} {:else}

View File

@ -267,10 +267,7 @@
</script> </script>
{#if toCreateLibrary} {#if toCreateLibrary}
<LibraryUserPickerForm <LibraryUserPickerForm onSubmit={handleCreate} onCancel={() => (toCreateLibrary = false)} />
on:submit={({ detail }) => handleCreate(detail.ownerId)}
on:cancel={() => (toCreateLibrary = false)}
/>
{/if} {/if}
<UserPageLayout title={data.meta.title} admin> <UserPageLayout title={data.meta.title} admin>
@ -385,28 +382,20 @@
</tr> </tr>
{#if renameLibrary === index} {#if renameLibrary === index}
<div transition:slide={{ duration: 250 }}> <div transition:slide={{ duration: 250 }}>
<LibraryRenameForm <LibraryRenameForm {library} onSubmit={handleUpdate} onCancel={() => (renameLibrary = null)} />
{library}
on:submit={({ detail }) => handleUpdate(detail)}
on:cancel={() => (renameLibrary = null)}
/>
</div> </div>
{/if} {/if}
{#if editImportPaths === index} {#if editImportPaths === index}
<div transition:slide={{ duration: 250 }}> <div transition:slide={{ duration: 250 }}>
<LibraryImportPathsForm <LibraryImportPathsForm {library} onSubmit={handleUpdate} onCancel={() => (editImportPaths = null)} />
{library}
on:submit={({ detail }) => handleUpdate(detail)}
on:cancel={() => (editImportPaths = null)}
/>
</div> </div>
{/if} {/if}
{#if editScanSettings === index} {#if editScanSettings === index}
<div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4"> <div transition:slide={{ duration: 250 }} class="mb-4 ml-4 mr-4">
<LibraryScanSettingsForm <LibraryScanSettingsForm
{library} {library}
on:submit={({ detail: library }) => handleUpdate(library)} onSubmit={handleUpdate}
on:cancel={() => (editScanSettings = null)} onCancel={() => (editScanSettings = null)}
/> />
</div> </div>
{/if} {/if}

View File

@ -110,8 +110,8 @@
<section class="w-full pb-28 lg:w-[850px]"> <section class="w-full pb-28 lg:w-[850px]">
{#if shouldShowCreateUserForm} {#if shouldShowCreateUserForm}
<CreateUserForm <CreateUserForm
on:submit={onUserCreated} onSubmit={onUserCreated}
on:cancel={() => (shouldShowCreateUserForm = false)} onCancel={() => (shouldShowCreateUserForm = false)}
onClose={() => (shouldShowCreateUserForm = false)} onClose={() => (shouldShowCreateUserForm = false)}
/> />
{/if} {/if}
@ -121,8 +121,8 @@
user={selectedUser} user={selectedUser}
bind:newPassword bind:newPassword
canResetPassword={selectedUser?.id !== $user.id} canResetPassword={selectedUser?.id !== $user.id}
on:editSuccess={onEditUserSuccess} onEditSuccess={onEditUserSuccess}
on:resetPasswordSuccess={onEditPasswordSuccess} onResetPasswordSuccess={onEditPasswordSuccess}
onClose={() => (shouldShowEditUserForm = false)} onClose={() => (shouldShowEditUserForm = false)}
/> />
{/if} {/if}

View File

@ -25,5 +25,5 @@
{$t('change_password_description')} {$t('change_password_description')}
</p> </p>
<ChangePasswordForm on:success={onSuccess} /> <ChangePasswordForm {onSuccess} />
</FullscreenContainer> </FullscreenContainer>