feat(web,a11y): standardize the FullScreenModal UI (#8566)

* feat(web,a11y): standardize the FullScreenModal look

* consistent header, padding, close button, and radius as BaseModal
* vertically stacking ConfirmDialogue CTA buttons in narrow screens
* adding aria-modal tags for screen reader
* add viewport-specific height limits on modals, to enable scrolling
* prevent focus from being hidden under sticky content in modals
* standardize FullScreenModal widths using a Prop

* wip: consistent padding with header

* fix: alignment on "create user" and "edit user" modals

* fix: horizontal modal content alignment

* fix: create user CTA buttons

* chore: remove unnecessary warning

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Ben 2024-04-08 21:02:09 +00:00 committed by GitHub
parent d43daaee81
commit 796c933fb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
43 changed files with 749 additions and 853 deletions

View File

@ -42,7 +42,8 @@
</script> </script>
<ConfirmDialogue <ConfirmDialogue
title="Delete User" id="delete-user-confirmation-modal"
title="Delete user"
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'} confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
onConfirm={handleDeleteUser} onConfirm={handleDeleteUser}
onClose={() => dispatch('cancel')} onClose={() => dispatch('cancel')}

View File

@ -147,6 +147,7 @@
{#if confirmJob} {#if confirmJob}
<ConfirmDialogue <ConfirmDialogue
id="reprocess-faces-modal"
prompt="Are you sure you want to reprocess all faces? This will also clear named people." prompt="Are you sure you want to reprocess all faces? This will also clear named people."
{onConfirm} {onConfirm}
onClose={() => (confirmJob = null)} onClose={() => (confirmJob = null)}

View File

@ -28,7 +28,8 @@
</script> </script>
<ConfirmDialogue <ConfirmDialogue
title="Restore User" id="restore-user-modal"
title="Restore user"
confirmText="Continue" confirmText="Continue"
confirmColor="green" confirmColor="green"
onConfirm={handleRestoreUser} onConfirm={handleRestoreUser}

View File

@ -5,7 +5,7 @@
export let onConfirm: () => void; export let onConfirm: () => void;
</script> </script>
<ConfirmDialogue title="Disable Login" onClose={onCancel} {onConfirm}> <ConfirmDialogue id="disable-login-modal" title="Disable login" onClose={onCancel} {onConfirm}>
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p> <p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>

View File

@ -1,10 +1,8 @@
<script lang="ts"> <script lang="ts">
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, mdiClose, mdiPlus } from '@mdi/js'; import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.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';
@ -52,67 +50,52 @@
}; };
</script> </script>
<FullScreenModal onClose={() => dispatch('close')}> <FullScreenModal id="album-options-modal" title="Options" onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden p-2 md:p-0"> <div class="items-center justify-center">
<div <div class="py-2">
class="w-[550px] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <h2 class="text-gray text-sm mb-2">SETTINGS</h2>
> <div class="grid p-2 gap-y-2">
<div class="px-2 pt-2"> {#if order}
<div class="flex items-center"> <SettingDropdown
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1> title="Display order"
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
id="comments-likes"
title="Comments & likes"
subtitle="Let others respond"
checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')}
/>
</div>
</div>
<div class="py-2">
<div class="text-gray text-sm mb-3">PEOPLE</div>
<div class="p-2">
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
<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>
<div>Invite People</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<div> <div>
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} /> <UserAvatar {user} size="md" />
</div> </div>
<div class="w-full">{user.name}</div>
<div>Owner</div>
</div> </div>
{#each album.sharedUsers as user (user.id)}
<div class=" items-center justify-center p-4"> <div class="flex items-center gap-2 py-2">
<div class="py-2"> <div>
<h2 class="text-gray text-sm mb-2">SETTINGS</h2> <UserAvatar {user} size="md" />
<div class="grid p-2 gap-y-2">
{#if order}
<SettingDropdown
title="Display order"
options={Object.values(options)}
selectedOption={options[order]}
onToggle={handleToggle}
/>
{/if}
<SettingSwitch
id="comments-likes"
title="Comments & likes"
subtitle="Let others respond"
checked={album.isActivityEnabled}
on:toggle={() => dispatch('toggleEnableActivity')}
/>
</div> </div>
<div class="w-full">{user.name}</div>
</div> </div>
<div class="py-2"> {/each}
<div class="text-gray text-sm mb-3">PEOPLE</div>
<div class="p-2">
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
<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>
<div>Invite People</div>
</button>
<div class="flex items-center gap-2 py-2 mt-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
<div>Owner</div>
</div>
{#each album.sharedUsers as user (user.id)}
<div class="flex items-center gap-2 py-2">
<div>
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
</div>
{/each}
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -432,7 +432,7 @@
{#if allowEdit} {#if allowEdit}
<!-- Edit Modal --> <!-- Edit Modal -->
{#if albumToEdit} {#if albumToEdit}
<FullScreenModal onClose={() => (albumToEdit = null)}> <FullScreenModal id="edit-album-modal" title="Edit album" width="wide" onClose={() => (albumToEdit = null)}>
<EditAlbumForm album={albumToEdit} onEditSuccess={successEditAlbumInfo} onCancel={() => (albumToEdit = null)} /> <EditAlbumForm album={albumToEdit} onEditSuccess={successEditAlbumInfo} onCancel={() => (albumToEdit = null)} />
</FullScreenModal> </FullScreenModal>
{/if} {/if}
@ -458,7 +458,8 @@
<!-- Delete Modal --> <!-- Delete Modal -->
{#if albumToDelete} {#if albumToDelete}
<ConfirmDialogue <ConfirmDialogue
title="Delete Album" id="delete-album-dialogue-modal"
title="Delete album"
confirmText="Delete" confirmText="Delete"
onConfirm={deleteSelectedAlbum} onConfirm={deleteSelectedAlbum}
onClose={() => (albumToDelete = null)} onClose={() => (albumToDelete = null)}

View File

@ -121,7 +121,8 @@
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id} {#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}
<ConfirmDialogue <ConfirmDialogue
title="Leave Album?" id="leave-album-modal"
title="Leave album?"
prompt="Are you sure you want to leave {album.albumName}?" prompt="Are you sure you want to leave {album.albumName}?"
confirmText="Leave" confirmText="Leave"
onConfirm={handleRemoveUser} onConfirm={handleRemoveUser}
@ -131,7 +132,8 @@
{#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id} {#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id}
<ConfirmDialogue <ConfirmDialogue
title="Remove User?" id="remove-user-modal"
title="Remove user?"
prompt="Are you sure you want to remove {selectedRemoveUser.name}" prompt="Are you sure you want to remove {selectedRemoveUser.name}"
confirmText="Remove" confirmText="Remove"
onConfirm={handleRemoveUser} onConfirm={handleRemoveUser}

View File

@ -161,6 +161,7 @@
{#if isShowConfirmation} {#if isShowConfirmation}
<ConfirmDialogue <ConfirmDialogue
id="merge-people-modal"
title="Merge people" title="Merge people"
confirmText="Merge" confirmText="Merge"
onConfirm={handleMerge} onConfirm={handleMerge}

View File

@ -3,7 +3,7 @@
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils'; import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk'; import { type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js'; import { mdiArrowLeft, mdiMerge } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; 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';
@ -30,95 +30,80 @@
}; };
</script> </script>
<FullScreenModal onClose={() => dispatch('close')}> <FullScreenModal id="merge-people-modal" title="Merge people - {title}" onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden"> <div class="flex items-center justify-center py-4 md:h-36 md:py-4">
<div {#if !choosePersonToMerge}
class="w-[250px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[375px]" <div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
> <ImageThumbnail
<div class="relative flex items-center justify-between"> circle
<h1 class="truncate px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary"> shadow
Merge People - {title} url={getPeopleThumbnailUrl(personMerge1.id)}
</h1> altText={personMerge1.name}
<div class="p-2"> widthStyle="100%"
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} /> />
</div>
<div class="mx-0.5 flex md:mx-2">
<CircleIconButton
title="Swap merge direction"
icon={mdiMerge}
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
/>
</div>
<button
disabled={potentialMergePeople.length === 0}
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
on:click={() => {
if (potentialMergePeople.length > 0) {
choosePersonToMerge = !choosePersonToMerge;
}
}}
>
<ImageThumbnail
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personMerge2.id)}
altText={personMerge2.name}
widthStyle="100%"
/>
</button>
{:else}
<div class="grid w-full grid-cols-1 gap-2">
<div class="px-2">
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
</div>
<div class="flex items-center justify-center">
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
{#each potentialMergePeople as person (person.id)}
<div class="h-24 w-24 md:h-28 md:w-28">
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
<ImageThumbnail
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
on:click={() => changePersonToMerge(person)}
/>
</button>
</div>
{/each}
</div>
</div> </div>
</div> </div>
{/if}
</div>
<div class="flex items-center justify-center px-2 py-4 md:h-36 md:px-4 md:py-4"> <div class="flex px-4 md:pt-4">
{#if !choosePersonToMerge} <h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2"> </div>
<ImageThumbnail <div class="flex px-4 pt-2">
circle <p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
shadow </div>
url={getPeopleThumbnailUrl(personMerge1.id)} <div class="mt-8 flex w-full gap-4 pb-4">
altText={personMerge1.name} <Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
widthStyle="100%" <Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
/>
</div>
<div class="mx-0.5 flex md:mx-2">
<CircleIconButton
title="Swap merge direction"
icon={mdiMerge}
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
/>
</div>
<button
disabled={potentialMergePeople.length === 0}
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
on:click={() => {
if (potentialMergePeople.length > 0) {
choosePersonToMerge = !choosePersonToMerge;
}
}}
>
<ImageThumbnail
border={potentialMergePeople.length > 0}
circle
shadow
url={getPeopleThumbnailUrl(personMerge2.id)}
altText={personMerge2.name}
widthStyle="100%"
/>
</button>
{:else}
<div class="grid w-full grid-cols-1 gap-2">
<div class="px-2">
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
</div>
<div class="flex items-center justify-center">
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
{#each potentialMergePeople as person (person.id)}
<div class="h-24 w-24 md:h-28 md:w-28">
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
<ImageThumbnail
border={true}
circle
shadow
url={getPeopleThumbnailUrl(person.id)}
altText={person.name}
widthStyle="100%"
on:click={() => changePersonToMerge(person)}
/>
</button>
</div>
{/each}
</div>
</div>
</div>
{/if}
</div>
<div class="flex px-4 md:px-8 md:pt-4">
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
</div>
<div class="flex px-4 pt-2 md:px-8">
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
</div>
<div class="mt-8 flex w-full gap-4 px-4 pb-4">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
</div>
</div>
</div> </div>
</FullScreenModal> </FullScreenModal>

View File

@ -3,7 +3,6 @@
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';
import Icon from '$lib/components/elements/icon.svelte';
import DateInput from '../elements/date-input.svelte'; import DateInput from '../elements/date-input.svelte';
export let birthDate: string; export let birthDate: string;
@ -21,36 +20,27 @@
}; };
</script> </script>
<FullScreenModal onClose={handleCancel}> <FullScreenModal id="set-birthday-modal" title="Set date of birth" icon={mdiCake} onClose={handleCancel}>
<div <div class="text-immich-primary dark:text-immich-dark-primary">
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <p class="text-sm dark:text-immich-dark-fg">
> Date of birth is used to calculate the age of this person at the time of a photo.
<div </p>
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiCake} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Set date of birth</h1>
<p class="text-sm dark:text-immich-dark-fg">
Date of birth is used to calculate the age of this person at the time of a photo.
</p>
</div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<DateInput
class="immich-form-input"
id="birthDate"
name="birthDate"
type="date"
bind:value={birthDate}
max={todayFormatted}
/>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Set</Button>
</div>
</form>
</div> </div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<div class="my-4 flex flex-col gap-2">
<DateInput
class="immich-form-input"
id="birthDate"
name="birthDate"
type="date"
bind:value={birthDate}
max={todayFormatted}
/>
</div>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Set</Button>
</div>
</form>
</FullScreenModal> </FullScreenModal>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import type { ApiKeyResponseDto } from '@immich/sdk'; import type { ApiKeyResponseDto } from '@immich/sdk';
import { mdiKeyVariant } from '@mdi/js'; import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
@ -8,7 +7,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
export let apiKey: Partial<ApiKeyResponseDto>; export let apiKey: Partial<ApiKeyResponseDto>;
export let title = 'API Key'; export let title: string;
export let cancelText = 'Cancel'; export let cancelText = 'Cancel';
export let submitText = 'Save'; export let submitText = 'Save';
@ -29,29 +28,16 @@
}; };
</script> </script>
<FullScreenModal onClose={handleCancel}> <FullScreenModal id="api-key-modal" {title} icon={mdiKeyVariant} onClose={handleCancel}>
<div <form on:submit|preventDefault={handleSubmit} autocomplete="off">
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <div class="mb-4 flex flex-col gap-2">
> <label class="immich-form-label" for="name">Name</label>
<div <input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiKeyVariant} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h1>
</div> </div>
<form on:submit|preventDefault={handleSubmit} autocomplete="off"> <div class="mt-8 flex w-full gap-4">
<div class="m-4 flex flex-col gap-2"> <Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
<label class="immich-form-label" for="name">Name</label> <Button type="submit" fullwidth>{submitText}</Button>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} /> </div>
</div> </form>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
<Button type="submit" fullwidth>{submitText}</Button>
</div>
</form>
</div>
</FullScreenModal> </FullScreenModal>

View File

@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import { mdiKeyVariant } from '@mdi/js'; import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
@ -20,31 +19,22 @@
}); });
</script> </script>
<FullScreenModal> <FullScreenModal id="api-key-secret-modal" title="API key" icon={mdiKeyVariant} onClose={() => handleDone()}>
<div <div class="text-immich-primary dark:text-immich-dark-primary">
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <p class="text-sm dark:text-immich-dark-fg">
> This value will only be shown once. Please be sure to copy it before closing the window.
<div </p>
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" </div>
>
<Icon path={mdiKeyVariant} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">API Key</h1>
<p class="text-sm dark:text-immich-dark-fg"> <div class="my-4 flex flex-col gap-2">
This value will only be shown once. Please be sure to copy it before closing the window. <!-- <label class="immich-form-label" for="secret">API Key</label> -->
</p> <textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="mt-8 flex w-full gap-4">
<!-- <label class="immich-form-label" for="secret">API Key</label> --> {#if canCopyImagesToClipboard}
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} /> <Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
</div> {/if}
<Button on:click={() => handleDone()} fullwidth>Done</Button>
<div class="mt-8 flex w-full gap-4 px-4">
{#if canCopyImagesToClipboard}
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
{/if}
<Button on:click={() => handleDone()} fullwidth>Done</Button>
</div>
</div> </div>
</FullScreenModal> </FullScreenModal>

View File

@ -5,7 +5,6 @@
import { createUser } from '@immich/sdk'; import { createUser } from '@immich/sdk';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte';
import PasswordField from '../shared-components/password-field.svelte'; import PasswordField from '../shared-components/password-field.svelte';
import Slider from '../elements/slider.svelte'; import Slider from '../elements/slider.svelte';
@ -69,62 +68,53 @@
} }
</script> </script>
<div <form on:submit|preventDefault={registerUser} autocomplete="off">
class="max-h-screen w-[500px] max-w-[95vw] overflow-y-auto immich-scrollbar rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <div class="my-4 flex flex-col gap-2">
> <label class="immich-form-label" for="email">Email</label>
<div class="flex flex-col place-content-center place-items-center gap-4 px-4"> <input class="immich-form-input" id="email" bind:value={email} type="email" required />
<ImmichLogo noText class="text-center" height="75" width="75" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Create new user</h1>
</div> </div>
<form on:submit|preventDefault={registerUser} autocomplete="off"> <div class="my-4 flex flex-col gap-2">
<div class="m-4 flex flex-col gap-2"> <label class="immich-form-label" for="password">Password</label>
<label class="immich-form-label" for="email">Email</label> <PasswordField id="password" bind:password autocomplete="new-password" />
<input class="immich-form-input" id="email" bind:value={email} type="email" required /> </div>
</div>
<div class="m-4 flex flex-col gap-2"> <div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label> <label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<PasswordField id="password" bind:password autocomplete="new-password" /> <PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
</div> </div>
<div class="m-4 flex flex-col gap-2"> <div class="my-4 flex place-items-center justify-between gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label> <label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" /> Require user to change password on first login
</div> </label>
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
</div>
<div class="m-4 flex place-items-center justify-between gap-2"> <div class="my-4 flex flex-col gap-2">
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change"> <label class="immich-form-label" for="name">Name</label>
Require user to change password on first login <input class="immich-form-input" id="name" bind:value={name} type="text" required />
</label> </div>
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
</div>
<div class="m-4 flex flex-col gap-2"> <div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label> <label class="flex items-center gap-2 immich-form-label" for="quotaSize">
<input class="immich-form-input" id="name" bind:value={name} type="text" required /> Quota Size (GiB)
</div> {#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}
</label>
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
</div>
<div class="m-4 flex flex-col gap-2"> {#if error}
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"> <p class="text-sm text-red-400">{error}</p>
Quota Size (GiB) {/if}
{#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}
</label>
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
</div>
{#if error} {#if success}
<p class="ml-4 text-sm text-red-400">{error}</p> <p class="text-sm text-immich-primary">{success}</p>
{/if} {/if}
<div class="flex w-full gap-4 pt-4">
{#if success} <Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<p class="ml-4 text-sm text-immich-primary">{success}</p> <Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
{/if} </div>
<div class="flex w-full gap-4 p-4"> </form>
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
</div>
</form>
</div>

View File

@ -34,39 +34,29 @@
}; };
</script> </script>
<div <form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off">
class="max-h-screen w-[700px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <div class="flex items-center">
> <div class="hidden sm:flex">
<div <AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
class="flex flex-col place-content-center place-items-center gap-4 px-4 mb-4 text-immich-primary dark:text-immich-dark-primary" </div>
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit Album</h1> <div class="flex-grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">Description</label>
<textarea class="immich-form-input" id="description" bind:value={description} />
</div>
</div>
</div> </div>
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off"> <div class="flex justify-center">
<div class="flex items-center"> <div class="mt-8 flex w-full sm:w-2/3 gap-4">
<div class="hidden sm:flex"> <Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" /> <Button type="submit" fullwidth disabled={isSubmitting}>OK</Button>
</div>
<div class="flex-grow">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" type="text" bind:value={albumName} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="description">Description</label>
<textarea class="immich-form-input" id="description" bind:value={description} />
</div>
</div>
</div> </div>
</div>
<div class="flex justify-center"> </form>
<div class="mt-8 flex w-full sm:w-2/3 gap-4 px-4">
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
<Button type="submit" fullwidth disabled={isSubmitting}>OK</Button>
</div>
</div>
</form>
</div>

View File

@ -1,16 +1,12 @@
<script lang="ts"> <script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { serverInfo } from '$lib/stores/server-info.store'; import { serverInfo } from '$lib/stores/server-info.store';
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter'; import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { updateUser, type UserResponseDto } from '@immich/sdk'; import { updateUser, type UserResponseDto } from '@immich/sdk';
import { mdiAccountEditOutline, mdiClose } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from '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 FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
export let user: UserResponseDto; export let user: UserResponseDto;
export let canResetPassword = true; export let canResetPassword = true;
@ -91,82 +87,64 @@
} }
</script> </script>
<FocusTrap> <form on:submit|preventDefault={editUser} autocomplete="off">
<div <div class="my-4 flex flex-col gap-2">
class="relative max-h-screen w-[500px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <label class="immich-form-label" for="email">Email</label>
> <input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
<div class="absolute top-0 right-0 px-2 py-2 h-fit">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
</div>
<div
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiAccountEditOutline} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit user</h1>
</div>
<form on:submit|preventDefault={editUser} autocomplete="off">
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
>Quota Size (GiB) {#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}</label
>
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
<p>Note: Enter 0 for unlimited quota</p>
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="storage-label">Storage Label</label>
<input
class="immich-form-input"
id="storage-label"
name="storage-label"
type="text"
bind:value={user.storageLabel}
/>
<p>
Note: To apply the Storage Label to previously uploaded assets, run the
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
Storage Migration Job</a
>
</p>
</div>
{#if error}
<p class="ml-4 text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
<div class="mt-8 flex w-full gap-4 px-4">
{#if canResetPassword}
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
>Reset password</Button
>
{/if}
<Button type="submit" fullwidth>Confirm</Button>
</div>
</form>
</div> </div>
</FocusTrap>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="name">Name</label>
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
>Quota Size (GiB) {#if quotaSizeWarning}
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
{/if}</label
>
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
<p>Note: Enter 0 for unlimited quota</p>
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="storage-label">Storage Label</label>
<input
class="immich-form-input"
id="storage-label"
name="storage-label"
type="text"
bind:value={user.storageLabel}
/>
<p>
Note: To apply the Storage Label to previously uploaded assets, run the
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"> Storage Migration Job</a>
</p>
</div>
{#if error}
<p class="ml-4 text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
<div class="mt-8 flex w-full gap-4">
{#if canResetPassword}
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
>Reset password</Button
>
{/if}
<Button type="submit" fullwidth>Confirm</Button>
</div>
</form>
{#if isShowResetPasswordConfirmation} {#if isShowResetPasswordConfirmation}
<ConfirmDialogue <ConfirmDialogue
title="Reset Password" id="reset-password-modal"
title="Reset password"
confirmText="Reset" confirmText="Reset"
onConfirm={resetPassword} onConfirm={resetPassword}
onClose={() => (isShowResetPasswordConfirmation = false)} onClose={() => (isShowResetPasswordConfirmation = false)}

View File

@ -2,7 +2,6 @@
import { createEventDispatcher } from 'svelte'; 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 Icon from '$lib/components/elements/icon.svelte';
import { mdiFolderRemove } from '@mdi/js'; import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -29,48 +28,42 @@
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern }); const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
</script> </script>
<FullScreenModal onClose={handleCancel}> <FullScreenModal
<div id="add-exclusion-pattern-modal"
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" title="Add exclusion pattern"
> icon={mdiFolderRemove}
<div onClose={handleCancel}
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" >
> <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<Icon path={mdiFolderRemove} size="4em" /> <p class="py-5 text-sm">
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Add Exclusion pattern</h1> Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
folders that contain files you don't want to import, such as RAW files.
<br /><br />
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore".
</p>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div> </div>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off"> <Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
<p class="p-5 text-sm"> </div>
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have <div class="mt-8 flex w-full gap-4">
folders that contain files you don't want to import, such as RAW files. {#if isDuplicate}
<br /><br /> <p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw", {/if}
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore". </div>
</p> </form>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
<input
class="immich-form-input"
id="exclusionPattern"
name="exclusionPattern"
type="text"
bind:value={exclusionPattern}
/>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
{#if isEditing}
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
{/if}
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
{/if}
</div>
</form>
</div>
</FullScreenModal> </FullScreenModal>

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/elements/icon.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';
@ -31,45 +30,30 @@
const handleSubmit = () => dispatch('submit', { importPath }); const handleSubmit = () => dispatch('submit', { importPath });
</script> </script>
<FullScreenModal onClose={handleCancel}> <FullScreenModal id="library-import-path-modal" {title} icon={mdiFolderSync} onClose={handleCancel}>
<div <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <p class="py-5 text-sm">
> Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.
<div </p>
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
> <div class="my-4 flex flex-col gap-2">
<Icon path={mdiFolderSync} size="4em" /> <label class="immich-form-label" for="path">Path</label>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary"> <input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
{title}
</h1>
</div> </div>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off"> <div class="mt-8 flex w-full gap-4">
<p class="p-5 text-sm"> <Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos. Note that {#if isEditing}
you are only allowed to import paths inside of your account's external path, configured in the administrative <Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
settings. {/if}
</p>
<div class="m-4 flex flex-col gap-2"> <Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
<label class="immich-form-label" for="path">Path</label> </div>
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<div class="mt-8 flex w-full gap-4 px-4"> <div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button> {#if isDuplicate}
{#if isEditing} <p class="text-red-500 text-sm">This import path already exists.</p>
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button> {/if}
{/if} </div>
</form>
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="mt-8 flex w-full gap-4 px-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This import path already exists.</p>
{/if}
</div>
</form>
</div>
</FullScreenModal> </FullScreenModal>

View File

@ -152,7 +152,7 @@
{#if addImportPath} {#if addImportPath}
<LibraryImportPathForm <LibraryImportPathForm
title="Add Import Path" title="Add import path"
submitText="Add" submitText="Add"
bind:importPath={importPathToAdd} bind:importPath={importPathToAdd}
{importPaths} {importPaths}
@ -166,7 +166,7 @@
{#if editImportPath != undefined} {#if editImportPath != undefined}
<LibraryImportPathForm <LibraryImportPathForm
title="Edit Import Path" title="Edit import path"
submitText="Save" submitText="Save"
isEditing={true} isEditing={true}
bind:importPath={editedImportPath} bind:importPath={editedImportPath}

View File

@ -169,7 +169,7 @@
size="sm" size="sm"
on:click={() => { on:click={() => {
addExclusionPattern = true; addExclusionPattern = true;
}}>Add Exclusion Pattern</Button }}>Add exclusion pattern</Button
></td ></td
></tr ></tr
> >

View File

@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/elements/icon.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';
@ -28,27 +27,21 @@
const handleSubmit = () => dispatch('submit', { ownerId }); const handleSubmit = () => dispatch('submit', { ownerId });
</script> </script>
<FullScreenModal onClose={handleCancel}> <FullScreenModal
<div id="select-library-owner-modal"
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" title="Select library owner"
> icon={mdiFolderSync}
<div onClose={handleCancel}
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" >
> <form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<Icon path={mdiFolderSync} size="4em" /> <p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Select library owner</h1>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Create</Button>
</div> </div>
</form>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
<div class="mt-8 flex w-full gap-4 px-4">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Create</Button>
</div>
</form>
</div>
</FullScreenModal> </FullScreenModal>

View File

@ -21,98 +21,92 @@
const handleClose = () => dispatch('close'); const handleClose = () => dispatch('close');
</script> </script>
<FullScreenModal onClose={handleClose}> <FullScreenModal id="map-settings-modal" title="Map settings" onClose={handleClose}>
<div <form
class="flex w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray" on:submit|preventDefault={() => dispatch('save', settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
> >
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Map Settings</h1> <SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
<form <SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
on:submit|preventDefault={() => dispatch('save', settings)} <SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary" {#if customDateRange}
> <div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} /> <div class="flex items-center justify-between gap-8">
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} /> <label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} /> <DateInput
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} /> class="immich-form-input w-40"
{#if customDateRange} type="date"
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4"> id="date-after"
<div class="flex items-center justify-between gap-8"> max={settings.dateBefore}
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label> bind:value={settings.dateAfter}
<DateInput
class="immich-form-input w-40"
type="date"
id="date-after"
max={settings.dateBefore}
bind:value={settings.dateAfter}
/>
</div>
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
<DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
</div>
<div class="flex justify-center text-xs">
<LinkButton
on:click={() => {
customDateRange = false;
settings.dateAfter = '';
settings.dateBefore = '';
}}
>
Remove custom date range
</LinkButton>
</div>
</div>
{:else}
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
<SettingSelect
label="Date range"
name="date-range"
bind:value={settings.relativeDate}
options={[
{
value: '',
text: 'All',
},
{
value: Duration.fromObject({ hours: 24 }).toISO() || '',
text: 'Past 24 hours',
},
{
value: Duration.fromObject({ days: 7 }).toISO() || '',
text: 'Past 7 days',
},
{
value: Duration.fromObject({ days: 30 }).toISO() || '',
text: 'Past 30 days',
},
{
value: Duration.fromObject({ years: 1 }).toISO() || '',
text: 'Past year',
},
{
value: Duration.fromObject({ years: 3 }).toISO() || '',
text: 'Past 3 years',
},
]}
/> />
<div class="text-xs">
<LinkButton
on:click={() => {
customDateRange = true;
settings.relativeDate = '';
}}
>
Use custom date range instead
</LinkButton>
</div>
</div> </div>
{/if} <div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
<div class="mt-4 flex w-full gap-4"> <DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button> </div>
<Button type="submit" size="sm" fullwidth>Save</Button> <div class="flex justify-center text-xs">
<LinkButton
on:click={() => {
customDateRange = false;
settings.dateAfter = '';
settings.dateBefore = '';
}}
>
Remove custom date range
</LinkButton>
</div>
</div> </div>
</form> {:else}
</div> <div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
<SettingSelect
label="Date range"
name="date-range"
bind:value={settings.relativeDate}
options={[
{
value: '',
text: 'All',
},
{
value: Duration.fromObject({ hours: 24 }).toISO() || '',
text: 'Past 24 hours',
},
{
value: Duration.fromObject({ days: 7 }).toISO() || '',
text: 'Past 7 days',
},
{
value: Duration.fromObject({ days: 30 }).toISO() || '',
text: 'Past 30 days',
},
{
value: Duration.fromObject({ years: 1 }).toISO() || '',
text: 'Past year',
},
{
value: Duration.fromObject({ years: 3 }).toISO() || '',
text: 'Past 3 years',
},
]}
/>
<div class="text-xs">
<LinkButton
on:click={() => {
customDateRange = true;
settings.relativeDate = '';
}}
>
Use custom date range instead
</LinkButton>
</div>
</div>
{/if}
<div class="mt-4 flex w-full gap-4">
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
<Button type="submit" size="sm" fullwidth>Save</Button>
</div>
</form>
</FullScreenModal> </FullScreenModal>

View File

@ -57,6 +57,7 @@
{#if isShowConfirmation} {#if isShowConfirmation}
<ConfirmDialogue <ConfirmDialogue
id="remove-from-album-modal"
title="Remove from {album.albumName}" title="Remove from {album.albumName}"
confirmText="Remove" confirmText="Remove"
onConfirm={removeFromAlbum} onConfirm={removeFromAlbum}

View File

@ -50,7 +50,8 @@
{#if removing} {#if removing}
<ConfirmDialogue <ConfirmDialogue
title="Remove Assets?" id="remove-assets-modal"
title="Remove assets?"
prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?" prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
confirmText="Remove" confirmText="Remove"
onConfirm={() => handleRemove()} onConfirm={() => handleRemove()}

View File

@ -25,7 +25,8 @@
</script> </script>
<ConfirmDialogue <ConfirmDialogue
title="Permanently Delete Asset{size > 1 ? 's' : ''}" id="permanently-delete-asset-modal"
title="Permanently delete asset{size > 1 ? 's' : ''}"
confirmText="Delete" confirmText="Delete"
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={() => dispatch('cancel')} onClose={() => dispatch('cancel')}

View File

@ -3,12 +3,9 @@
import { quintOut } from 'svelte/easing'; import { quintOut } from 'svelte/easing';
import { createEventDispatcher, onMount, onDestroy } from 'svelte'; import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { clickOutside } from '$lib/utils/click-outside'; import { clickOutside } from '$lib/utils/click-outside';
import { mdiClose } from '@mdi/js';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte'; import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
import Icon from '$lib/components/elements/icon.svelte';
const dispatch = createEventDispatcher<{ const dispatch = createEventDispatcher<{
close: void; close: void;
@ -28,6 +25,8 @@
*/ */
export let icon: string | undefined = undefined; export let icon: string | undefined = undefined;
$: titleId = `${id}-title`;
onMount(() => { onMount(() => {
if (browser) { if (browser) {
const scrollTop = document.documentElement.scrollTop; const scrollTop = document.documentElement.scrollTop;
@ -51,7 +50,7 @@
<FocusTrap> <FocusTrap>
<div <div
aria-modal="true" aria-modal="true"
aria-labelledby={`${id}-title`} aria-labelledby={titleId}
style:z-index={zIndex} style:z-index={zIndex}
transition:fade={{ duration: 100, easing: quintOut }} transition:fade={{ duration: 100, easing: quintOut }}
class="fixed left-0 top-0 flex h-full w-full place-content-center place-items-center overflow-hidden bg-black/50" class="fixed left-0 top-0 flex h-full w-full place-content-center place-items-center overflow-hidden bg-black/50"
@ -61,23 +60,11 @@
onOutclick: () => dispatch('close'), onOutclick: () => dispatch('close'),
onEscape: () => dispatch('close'), onEscape: () => dispatch('close'),
}} }}
class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar" class="min-h-[200px] w-[450px] overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar scroll-pb-20"
style="max-height: min(95vh, 800px);"
tabindex="-1" tabindex="-1"
> >
<div class="flex place-items-center justify-between px-5 py-3"> <ModalHeader id={titleId} {title} {showLogo} {icon} on:close />
<div class="flex gap-2 place-items-center">
{#if showLogo}
<ImmichLogo noText={true} width={32} />
{:else if icon}
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
{/if}
<h1 id={`${id}-title`}>
{title}
</h1>
</div>
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
</div>
<div> <div>
<slot /> <slot />

View File

@ -63,16 +63,16 @@
<div role="presentation" on:keydown={handleKeydown}> <div role="presentation" on:keydown={handleKeydown}>
<ConfirmDialogue <ConfirmDialogue
id="edit-date-time-modal"
confirmColor="primary" confirmColor="primary"
cancelColor="secondary" cancelColor="secondary"
title="Edit date & time" title="Edit date and time"
prompt="Please select a new date:" prompt="Please select a new date:"
disabled={!date.isValid} disabled={!date.isValid}
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={handleCancel} onClose={handleCancel}
> >
<div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt"> <div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt">
<div class="mt-2" />
<div class="flex flex-col"> <div class="flex flex-col">
<label for="datetime">Date and Time</label> <label for="datetime">Date and Time</label>
<DateInput <DateInput

View File

@ -12,7 +12,6 @@
import SearchBar from '../elements/search-bar.svelte'; import SearchBar from '../elements/search-bar.svelte';
import { listNavigation } from '$lib/utils/list-navigation'; import { listNavigation } from '$lib/utils/list-navigation';
export const title = 'Change Location';
export let asset: AssetResponseDto | undefined = undefined; export let asset: AssetResponseDto | undefined = undefined;
interface Point { interface Point {
@ -95,10 +94,11 @@
</script> </script>
<ConfirmDialogue <ConfirmDialogue
id="change-location-modal"
confirmColor="primary" confirmColor="primary"
cancelColor="secondary" cancelColor="secondary"
title="Change Location" title="Change location"
width={800} width="wide"
onConfirm={handleConfirm} onConfirm={handleConfirm}
onClose={handleCancel} onClose={handleCancel}
> >

View File

@ -3,6 +3,7 @@
import Button from '../elements/buttons/button.svelte'; import Button from '../elements/buttons/button.svelte';
import type { Color } from '$lib/components/elements/buttons/button.svelte'; import type { Color } from '$lib/components/elements/buttons/button.svelte';
export let id: string;
export let title = 'Confirm'; export let title = 'Confirm';
export let prompt = 'Are you sure you want to do this?'; export let prompt = 'Are you sure you want to do this?';
export let confirmText = 'Confirm'; export let confirmText = 'Confirm';
@ -11,7 +12,7 @@
export let cancelColor: Color = 'primary'; export let cancelColor: Color = 'primary';
export let hideCancelButton = false; export let hideCancelButton = false;
export let disabled = false; export let disabled = false;
export let width = 500; export let width: 'wide' | 'narrow' = 'narrow';
export let onClose: () => void; export let onClose: () => void;
export let onConfirm: () => void; export let onConfirm: () => void;
@ -23,35 +24,21 @@
}; };
</script> </script>
<FullScreenModal {onClose}> <FullScreenModal {title} {id} {onClose} {width}>
<div <div class="text-md py-5 text-center">
class="max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <slot name="prompt">
style="width: {width}px" <p>{prompt}</p>
> </slot>
<div </div>
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="pb-2 text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
{title}
</h1>
</div>
<div>
<div class="text-md px-4 py-5 text-center">
<slot name="prompt">
<p>{prompt}</p>
</slot>
</div>
<div class="mt-4 flex w-full gap-4 px-4"> <div class="mt-4 flex flex-col sm:flex-row w-full gap-4">
{#if !hideCancelButton} {#if !hideCancelButton}
<Button color={cancelColor} fullwidth on:click={onClose}> <Button color={cancelColor} fullwidth on:click={onClose}>
{cancelText} {cancelText}
</Button> </Button>
{/if} {/if}
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}> <Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
{confirmText} {confirmText}
</Button> </Button>
</div>
</div>
</div> </div>
</FullScreenModal> </FullScreenModal>

View File

@ -2,8 +2,44 @@
import { clickOutside } from '../../utils/click-outside'; import { clickOutside } from '../../utils/click-outside';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte'; import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
export let onClose: (() => void) | undefined = undefined; export let onClose: () => void;
/**
* Unique identifier for the modal.
*/
export let id: string;
export let title: string;
/**
* If true, the logo will be displayed next to the modal title.
*/
export let showLogo = false;
/**
* Optional icon to display next to the modal title, if `showLogo` is false.
*/
export let icon: string | undefined = undefined;
/**
* Sets the width of the modal.
*
* - `wide`: 750px
* - `narrow`: 450px
* - `auto`: fits the width of the modal content, up to a maximum of 550px
*/
export let width: 'wide' | 'narrow' | 'auto' = 'narrow';
$: titleId = `${id}-title`;
let modalWidth: string;
$: {
if (width === 'wide') {
modalWidth = 'w-[750px]';
} else if (width === 'narrow') {
modalWidth = 'w-[450px]';
} else {
modalWidth = 'sm:max-w-[550px]';
}
}
</script> </script>
<FocusTrap> <FocusTrap>
@ -12,8 +48,17 @@
out:fade={{ duration: 100 }} out:fade={{ duration: 100 }}
class="fixed left-0 top-0 z-[9990] flex h-screen w-screen place-content-center place-items-center bg-black/40" class="fixed left-0 top-0 z-[9990] flex h-screen w-screen place-content-center place-items-center bg-black/40"
> >
<div class="z-[9999]" use:clickOutside={{ onOutclick: onClose, onEscape: onClose }} tabindex="-1"> <div
<slot /> class="z-[9999] max-w-[95vw] max-h-[95vh] {modalWidth} overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
tabindex="-1"
aria-modal="true"
aria-labelledby={titleId}
>
<ModalHeader id={titleId} {title} {showLogo} {icon} on:close={() => onClose?.()} />
<div class="p-5 pt-0">
<slot />
</div>
</div> </div>
</section> </section>
</FocusTrap> </FocusTrap>

View File

@ -0,0 +1,40 @@
<script lang="ts">
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { createEventDispatcher } from 'svelte';
import { mdiClose } from '@mdi/js';
const dispatch = createEventDispatcher<{
close: void;
}>();
/**
* Unique identifier for the header text.
*/
export let id: string;
export let title: string;
/**
* If true, the logo will be displayed next to the modal title.
*/
export let showLogo = false;
/**
* Optional icon to display next to the modal title, if `showLogo` is false.
*/
export let icon: string | undefined = undefined;
</script>
<div class="flex place-items-center justify-between px-5 py-3">
<div class="flex gap-2 place-items-center">
{#if showLogo}
<ImmichLogo noText={true} width={32} />
{:else if icon}
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
{/if}
<h1 {id}>
{title}
</h1>
</div>
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
</div>

View File

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk'; import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import FullScreenModal from '../full-screen-modal.svelte'; import FullScreenModal from '../full-screen-modal.svelte';
import UserAvatar from '../user-avatar.svelte'; import UserAvatar from '../user-avatar.svelte';
@ -15,28 +13,14 @@
const colors: UserAvatarColor[] = Object.values(UserAvatarColor); const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
</script> </script>
<FullScreenModal onClose={() => dispatch('close')}> <FullScreenModal id="avatar-selector-modal" title="Select avatar color" width="auto" onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden"> <div class="flex items-center justify-center mt-4">
<div <div class="grid grid-cols-2 md:grid-cols-5 gap-4">
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg p-4" {#each colors as color}
> <button on:click={() => dispatch('choose', color)}>
<div class="flex items-center"> <UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm"> </button>
SELECT AVATAR COLOR {/each}
</h1>
<div>
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
</div>
</div>
<div class="flex items-center justify-center p-4 mt-4">
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
{#each colors as color}
<button on:click={() => dispatch('choose', color)}>
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
</button>
{/each}
</div>
</div>
</div> </div>
</div> </div>
</FullScreenModal> </FullScreenModal>

View File

@ -1,8 +1,7 @@
<script lang="ts"> <script lang="ts">
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher } from 'svelte';
import FullScreenModal from './full-screen-modal.svelte'; import FullScreenModal from './full-screen-modal.svelte';
import { mdiClose, mdiInformationOutline } from '@mdi/js'; import { mdiInformationOutline } from '@mdi/js';
import Icon from '../elements/icon.svelte'; import Icon from '../elements/icon.svelte';
interface Shortcuts { interface Shortcuts {
@ -38,63 +37,51 @@
}>(); }>();
</script> </script>
<FullScreenModal onClose={() => dispatch('close')}> <FullScreenModal
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden"> id="keyboard-shortcuts-modal"
<div title="Keyboard shortcuts"
class="rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" width="auto"
> onClose={() => dispatch('close')}
<div class="relative px-4 pt-4"> >
<h1 class="px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">Keyboard Shortcuts</h1> <div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
<div class="absolute inset-y-0 right-0 px-4 py-4"> <div class="p-4">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} /> <h2>General</h2>
</div> <div class="text-sm">
{#each shortcuts.general as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
{key}
</p>
{/each}
</div>
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
</div>
{/each}
</div> </div>
</div>
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2"> <div class="p-4">
<div class="px-4 py-4"> <h2>Actions</h2>
<h2>General</h2> <div class="text-sm">
<div class="text-sm"> {#each shortcuts.actions as shortcut}
{#each shortcuts.general as shortcut} <div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm"> <div class="flex justify-self-end">
<div class="flex justify-self-end"> {#each shortcut.key as key}
{#each shortcut.key as key} <p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
<p {key}
class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2" </p>
> {/each}
{key} </div>
</p> <div class="flex items-center gap-2">
{/each} <p class="mb-1 mt-1 flex">{shortcut.action}</p>
</div> {#if shortcut.info}
<p class="mb-1 mt-1 flex">{shortcut.action}</p> <Icon path={mdiInformationOutline} title={shortcut.info} />
</div> {/if}
{/each} </div>
</div> </div>
</div> {/each}
<div class="px-4 py-4">
<h2>Actions</h2>
<div class="text-sm">
{#each shortcuts.actions as shortcut}
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
<div class="flex justify-self-end">
{#each shortcut.key as key}
<p
class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"
>
{key}
</p>
{/each}
</div>
<div class="flex items-center gap-2">
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
{#if shortcut.info}
<Icon path={mdiInformationOutline} title={shortcut.info} />
{/if}
</div>
</div>
{/each}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -33,34 +33,28 @@
</script> </script>
{#if showModal} {#if showModal}
<FullScreenModal onClose={() => (showModal = false)}> <FullScreenModal id="new-version-modal" title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}>
<div <div>
class="max-w-lg rounded-3xl border bg-immich-bg px-8 py-10 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" Hi friend, there is a new version of the application please take your time to visit the
> <span class="font-medium underline"
<p class="mb-4 text-2xl">🎉 NEW VERSION AVAILABLE 🎉</p> ><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
>release notes</a
></span
>
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
especially if you use WatchTower or any mechanism that handles updating your application automatically.
</div>
<div> <div class="mt-4 font-medium">Your friend, Alex</div>
Hi friend, there is a new version of the application please take your time to visit the
<span class="font-medium underline"
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
>release notes</a
></span
>
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
especially if you use WatchTower or any mechanism that handles updating your application automatically.
</div>
<div class="mt-4 font-medium">Your friend, Alex</div> <div class="font-sm mt-8">
<code>Server Version: {serverVersion}</code>
<br />
<code>Latest Version: {releaseVersion}</code>
</div>
<div class="font-sm mt-8"> <div class="mt-8 text-right">
<code>Server Version: {serverVersion}</code> <Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
<br />
<code>Latest Version: {releaseVersion}</code>
</div>
<div class="mt-8 text-right">
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
</div>
</div> </div>
</FullScreenModal> </FullScreenModal>
{/if} {/if}

View File

@ -30,31 +30,22 @@
}; };
</script> </script>
<FullScreenModal {onClose}> <FullScreenModal id="slideshow-settings-modal" title="Slideshow settings" {onClose}>
<div <div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
class="flex w-full md:w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray" <SettingDropdown
> title="Direction"
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary"> options={Object.values(options)}
Slideshow Settings selectedOption={options[$slideshowNavigation]}
</h1> onToggle={(option) => handleToggle(option)}
/>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"> <SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
<SettingDropdown <SettingInputField
title="Direction" inputType={SettingInputFieldType.NUMBER}
options={Object.values(options)} label="Duration"
selectedOption={options[$slideshowNavigation]} desc="Number of seconds to display each image"
onToggle={(option) => handleToggle(option)} min={1}
/> bind:value={$slideshowDelay}
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} /> />
<SettingInputField <Button class="w-full" color="gray" on:click={onClose}>Done</Button>
inputType={SettingInputFieldType.NUMBER}
label="Duration"
desc="Number of seconds to display each image"
min={1}
bind:value={$slideshowDelay}
/>
<Button class="w-full" color="gray" on:click={onClose}>Done</Button>
</div>
</div> </div>
</FullScreenModal> </FullScreenModal>

View File

@ -49,6 +49,7 @@
{#if deleteDevice} {#if deleteDevice}
<ConfirmDialogue <ConfirmDialogue
id="log-out-device-modal"
prompt="Are you sure you want to log out this device?" prompt="Are you sure you want to log out this device?"
onConfirm={() => handleDelete()} onConfirm={() => handleDelete()}
onClose={() => (deleteDevice = null)} onClose={() => (deleteDevice = null)}
@ -57,6 +58,7 @@
{#if deleteAll} {#if deleteAll}
<ConfirmDialogue <ConfirmDialogue
id="log-out-all-modal"
prompt="Are you sure you want to log out all devices?" prompt="Are you sure you want to log out all devices?"
onConfirm={() => handleDeleteAll()} onConfirm={() => handleDeleteAll()}
onClose={() => (deleteAll = false)} onClose={() => (deleteAll = false)}

View File

@ -189,6 +189,7 @@
{#if removePartnerDto} {#if removePartnerDto}
<ConfirmDialogue <ConfirmDialogue
id="stop-sharing-photos-modal"
title="Stop sharing your photos?" title="Stop sharing your photos?"
prompt="{removePartnerDto.name} will no longer be able to access your photos." prompt="{removePartnerDto.name} will no longer be able to access your photos."
onClose={() => (removePartnerDto = null)} onClose={() => (removePartnerDto = null)}

View File

@ -81,7 +81,7 @@
{#if newKey} {#if newKey}
<APIKeyForm <APIKeyForm
title="New API Key" title="New API key"
submitText="Create" submitText="Create"
apiKey={newKey} apiKey={newKey}
on:submit={({ detail }) => handleCreate(detail)} on:submit={({ detail }) => handleCreate(detail)}
@ -95,6 +95,7 @@
{#if editKey} {#if editKey}
<APIKeyForm <APIKeyForm
title="API key"
submitText="Save" submitText="Save"
apiKey={editKey} apiKey={editKey}
on:submit={({ detail }) => handleUpdate(detail)} on:submit={({ detail }) => handleUpdate(detail)}
@ -104,7 +105,8 @@
{#if deleteKey} {#if deleteKey}
<ConfirmDialogue <ConfirmDialogue
prompt="Are you sure you want to delete this API Key?" id="confirm-api-key-delete-modal"
prompt="Are you sure you want to delete this API key?"
onConfirm={() => handleDelete()} onConfirm={() => handleDelete()}
onClose={() => (deleteKey = null)} onClose={() => (deleteKey = null)}
/> />

View File

@ -683,6 +683,7 @@
{#if viewMode === ViewMode.CONFIRM_DELETE} {#if viewMode === ViewMode.CONFIRM_DELETE}
<ConfirmDialogue <ConfirmDialogue
id="delete-album-modal"
title="Delete album" title="Delete album"
confirmText="Delete" confirmText="Delete"
onConfirm={handleRemoveAlbum} onConfirm={handleRemoveAlbum}

View File

@ -463,35 +463,25 @@
{/if} {/if}
{#if showChangeNameModal} {#if showChangeNameModal}
<FullScreenModal onClose={() => (showChangeNameModal = false)}> <FullScreenModal id="change-name-modal" title="Change name" onClose={() => (showChangeNameModal = false)}>
<div <form on:submit|preventDefault={submitNameChange} autocomplete="off">
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg" <div class="flex flex-col gap-2">
> <label class="immich-form-label" for="name">Name</label>
<div <!-- svelte-ignore a11y-autofocus -->
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" <input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus />
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Change name</h1>
</div> </div>
<form on:submit|preventDefault={submitNameChange} autocomplete="off"> <div class="mt-8 flex w-full gap-4">
<div class="m-4 flex flex-col gap-2"> <Button
<label class="immich-form-label" for="name">Name</label> color="gray"
<!-- svelte-ignore a11y-autofocus --> fullwidth
<input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus /> on:click={() => {
</div> showChangeNameModal = false;
}}>Cancel</Button
<div class="mt-8 flex w-full gap-4 px-4"> >
<Button <Button type="submit" fullwidth>Ok</Button>
color="gray" </div>
fullwidth </form>
on:click={() => {
showChangeNameModal = false;
}}>Cancel</Button
>
<Button type="submit" fullwidth>Ok</Button>
</div>
</form>
</div>
</FullScreenModal> </FullScreenModal>
{/if} {/if}

View File

@ -88,7 +88,8 @@
{#if deleteLinkId} {#if deleteLinkId}
<ConfirmDialogue <ConfirmDialogue
title="Delete Shared Link" id="delete-shared-link-modal"
title="Delete shared link"
prompt="Are you sure you want to delete this shared link?" prompt="Are you sure you want to delete this shared link?"
confirmText="Delete" confirmText="Delete"
onConfirm={() => handleDeleteLink()} onConfirm={() => handleDeleteLink()}

View File

@ -98,7 +98,8 @@
{#if isShowEmptyConfirmation} {#if isShowEmptyConfirmation}
<ConfirmDialogue <ConfirmDialogue
title="Empty Trash" id="empty-trash-modal"
title="Empty trash"
confirmText="Empty" confirmText="Empty"
onConfirm={handleEmptyTrash} onConfirm={handleEmptyTrash}
onClose={() => (isShowEmptyConfirmation = false)} onClose={() => (isShowEmptyConfirmation = false)}

View File

@ -302,6 +302,7 @@
{#if confirmDeleteLibrary} {#if confirmDeleteLibrary}
<ConfirmDialogue <ConfirmDialogue
id="warning-modal"
title="Warning!" title="Warning!"
prompt="Are you sure you want to delete this library? This will delete all {deleteAssetCount} contained assets from Immich and cannot be undone. Files will remain on disk." prompt="Are you sure you want to delete this library? This will delete all {deleteAssetCount} contained assets from Immich and cannot be undone. Files will remain on disk."
onConfirm={handleDelete} onConfirm={handleDelete}

View File

@ -1,9 +1,9 @@
<script lang="ts"> <script lang="ts">
import { page } from '$app/stores'; import { page } from '$app/stores';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte'; import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte'; import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
import Button from '$lib/components/elements/buttons/button.svelte'; import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte'; import Icon from '$lib/components/elements/icon.svelte';
import CreateUserForm from '$lib/components/forms/create-user-form.svelte'; import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
@ -21,7 +21,14 @@
import { asByteUnitString } from '$lib/utils/byte-units'; import { asByteUnitString } from '$lib/utils/byte-units';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk'; import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'; import {
mdiAccountEditOutline,
mdiClose,
mdiContentCopy,
mdiDeleteRestore,
mdiPencilOutline,
mdiTrashCanOutline,
} from '@mdi/js';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@ -116,13 +123,23 @@
<section id="setting-content" class="flex place-content-center sm:mx-4"> <section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 lg:w-[850px]"> <section class="w-full pb-28 lg:w-[850px]">
{#if shouldShowCreateUserForm} {#if shouldShowCreateUserForm}
<FullScreenModal onClose={() => (shouldShowCreateUserForm = false)}> <FullScreenModal
id="create-new-user-modal"
title="Create new user"
showLogo
onClose={() => (shouldShowCreateUserForm = false)}
>
<CreateUserForm on:submit={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} /> <CreateUserForm on:submit={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} />
</FullScreenModal> </FullScreenModal>
{/if} {/if}
{#if shouldShowEditUserForm} {#if shouldShowEditUserForm}
<FullScreenModal onClose={() => (shouldShowEditUserForm = false)}> <FullScreenModal
id="edit-user-modal"
title="Edit user"
icon={mdiAccountEditOutline}
onClose={() => (shouldShowEditUserForm = false)}
>
<EditUserForm <EditUserForm
user={selectedUser} user={selectedUser}
bind:newPassword bind:newPassword
@ -153,40 +170,39 @@
{/if} {/if}
{#if shouldShowPasswordResetSuccess} {#if shouldShowPasswordResetSuccess}
<FullScreenModal onClose={() => (shouldShowPasswordResetSuccess = false)}> <ConfirmDialogue
<ConfirmDialogue id="password-reset-success-modal"
title="Password Reset Success" title="Password reset success"
confirmText="Done" confirmText="Done"
onConfirm={() => (shouldShowPasswordResetSuccess = false)} onConfirm={() => (shouldShowPasswordResetSuccess = false)}
onClose={() => (shouldShowPasswordResetSuccess = false)} onClose={() => (shouldShowPasswordResetSuccess = false)}
hideCancelButton={true} hideCancelButton={true}
confirmColor="green" confirmColor="green"
> >
<svelte:fragment slot="prompt"> <svelte:fragment slot="prompt">
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<p>The user's password has been reset:</p> <p>The user's password has been reset:</p>
<div class="flex justify-center gap-2"> <div class="flex justify-center gap-2">
<code <code
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700" class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
> >
{newPassword} {newPassword}
</code> </code>
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password"> <LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
<div class="flex place-items-center gap-2 text-sm"> <div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiContentCopy} size="18" /> <Icon path={mdiContentCopy} size="18" />
</div> </div>
</LinkButton> </LinkButton>
</div>
<p>
Please provide the temporary password to the user and inform them they will need to change the
password at their next login.
</p>
</div> </div>
</svelte:fragment>
</ConfirmDialogue> <p>
</FullScreenModal> Please provide the temporary password to the user and inform them they will need to change the password
at their next login.
</p>
</div>
</svelte:fragment>
</ConfirmDialogue>
{/if} {/if}
<table class="my-5 w-full text-left"> <table class="my-5 w-full text-left">