1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

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>
<ConfirmDialogue
title="Delete User"
id="delete-user-confirmation-modal"
title="Delete user"
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
onConfirm={handleDeleteUser}
onClose={() => dispatch('cancel')}

View file

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

View file

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

View file

@ -5,7 +5,7 @@
export let onConfirm: () => void;
</script>
<ConfirmDialogue title="Disable Login" onClose={onCancel} {onConfirm}>
<ConfirmDialogue id="disable-login-modal" title="Disable login" onClose={onCancel} {onConfirm}>
<svelte:fragment slot="prompt">
<div class="flex flex-col gap-4">
<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">
import Icon from '$lib/components/elements/icon.svelte';
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 CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@ -52,67 +50,52 @@
};
</script>
<FullScreenModal 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="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"
>
<div class="px-2 pt-2">
<div class="flex items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
<FullScreenModal id="album-options-modal" title="Options" onClose={() => dispatch('close')}>
<div class="items-center justify-center">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
<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="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>
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
<UserAvatar {user} size="md" />
</div>
<div class="w-full">{user.name}</div>
<div>Owner</div>
</div>
<div class=" items-center justify-center p-4">
<div class="py-2">
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
<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')}
/>
{#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>
<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>
<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>
{/each}
</div>
</div>
</div>

View file

@ -432,7 +432,7 @@
{#if allowEdit}
<!-- Edit Modal -->
{#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)} />
</FullScreenModal>
{/if}
@ -458,7 +458,8 @@
<!-- Delete Modal -->
{#if albumToDelete}
<ConfirmDialogue
title="Delete Album"
id="delete-album-dialogue-modal"
title="Delete album"
confirmText="Delete"
onConfirm={deleteSelectedAlbum}
onClose={() => (albumToDelete = null)}

View file

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

View file

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

View file

@ -3,7 +3,7 @@
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { getPeopleThumbnailUrl } from '$lib/utils';
import { type PersonResponseDto } from '@immich/sdk';
import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js';
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import Button from '../elements/buttons/button.svelte';
@ -30,95 +30,80 @@
};
</script>
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
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="relative flex items-center justify-between">
<h1 class="truncate px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">
Merge People - {title}
</h1>
<div class="p-2">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
<FullScreenModal id="merge-people-modal" title="Merge people - {title}" onClose={() => dispatch('close')}>
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personMerge1.id)}
altText={personMerge1.name}
widthStyle="100%"
/>
</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 items-center justify-center px-2 py-4 md:h-36 md:px-4 md:py-4">
{#if !choosePersonToMerge}
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
<ImageThumbnail
circle
shadow
url={getPeopleThumbnailUrl(personMerge1.id)}
altText={personMerge1.name}
widthStyle="100%"
/>
</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 class="flex px-4 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">
<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 pb-4">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
</div>
</FullScreenModal>

View file

@ -3,7 +3,6 @@
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiCake } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import DateInput from '../elements/date-input.svelte';
export let birthDate: string;
@ -21,36 +20,27 @@
};
</script>
<FullScreenModal onClose={handleCancel}>
<div
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 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>
<FullScreenModal id="set-birthday-modal" title="Set date of birth" icon={mdiCake} onClose={handleCancel}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<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="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>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import type { ApiKeyResponseDto } from '@immich/sdk';
import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
@ -8,7 +7,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification';
export let apiKey: Partial<ApiKeyResponseDto>;
export let title = 'API Key';
export let title: string;
export let cancelText = 'Cancel';
export let submitText = 'Save';
@ -29,29 +28,16 @@
};
</script>
<FullScreenModal onClose={handleCancel}>
<div
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 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>
<FullScreenModal id="api-key-modal" {title} icon={mdiKeyVariant} onClose={handleCancel}>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<div class="mb-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" bind:value={apiKey.name} />
</div>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<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" bind:value={apiKey.name} />
</div>
<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>
<div class="mt-8 flex w-full gap-4">
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
<Button type="submit" fullwidth>{submitText}</Button>
</div>
</form>
</FullScreenModal>

View file

@ -1,5 +1,4 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { copyToClipboard } from '$lib/utils';
import { mdiKeyVariant } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
@ -20,31 +19,22 @@
});
</script>
<FullScreenModal>
<div
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 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">API Key</h1>
<FullScreenModal id="api-key-secret-modal" title="API key" icon={mdiKeyVariant} onClose={() => handleDone()}>
<div class="text-immich-primary dark:text-immich-dark-primary">
<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.
</p>
</div>
<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.
</p>
</div>
<div class="my-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div>
<div class="m-4 flex flex-col gap-2">
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div>
<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 class="mt-8 flex w-full gap-4">
{#if canCopyImagesToClipboard}
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
{/if}
<Button on:click={() => handleDone()} fullwidth>Done</Button>
</div>
</FullScreenModal>

View file

@ -5,7 +5,6 @@
import { createUser } from '@immich/sdk';
import { createEventDispatcher } from '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 Slider from '../elements/slider.svelte';
@ -69,62 +68,53 @@
}
</script>
<div
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="flex flex-col place-content-center place-items-center gap-4 px-4">
<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>
<form on:submit|preventDefault={registerUser} autocomplete="off">
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="email">Email</label>
<input class="immich-form-input" id="email" bind:value={email} type="email" required />
</div>
<form on:submit|preventDefault={registerUser} 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" bind:value={email} type="email" required />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<PasswordField id="password" bind:password autocomplete="new-password" />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="password">Password</label>
<PasswordField id="password" bind:password autocomplete="new-password" />
</div>
<div class="my-4 flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
</div>
<div class="m-4 flex flex-col gap-2">
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
</div>
<div class="my-4 flex place-items-center justify-between gap-2">
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
Require user to change password on first login
</label>
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
</div>
<div class="m-4 flex place-items-center justify-between gap-2">
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
Require user to change password on first login
</label>
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
</div>
<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" bind:value={name} type="text" required />
</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" bind:value={name} type="text" required />
</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" type="number" min="0" bind:value={quotaSize} />
</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" type="number" min="0" bind:value={quotaSize} />
</div>
{#if error}
<p class="text-sm text-red-400">{error}</p>
{/if}
{#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="flex w-full gap-4 p-4">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
</div>
</form>
</div>
{#if success}
<p class="text-sm text-immich-primary">{success}</p>
{/if}
<div class="flex w-full gap-4 pt-4">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
</div>
</form>

View file

@ -34,39 +34,29 @@
};
</script>
<div
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 flex-col place-content-center place-items-center gap-4 px-4 mb-4 text-immich-primary dark:text-immich-dark-primary"
>
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit Album</h1>
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
</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>
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
</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 class="flex justify-center">
<div class="mt-8 flex w-full sm:w-2/3 gap-4">
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
<Button type="submit" fullwidth disabled={isSubmitting}>OK</Button>
</div>
<div class="flex justify-center">
<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>
</div>
</form>

View file

@ -1,16 +1,12 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import { AppRoute } from '$lib/constants';
import { serverInfo } from '$lib/stores/server-info.store';
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
import { handleError } from '$lib/utils/handle-error';
import { updateUser, type UserResponseDto } from '@immich/sdk';
import { mdiAccountEditOutline, mdiClose } from '@mdi/js';
import { createEventDispatcher } from '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 canResetPassword = true;
@ -91,82 +87,64 @@
}
</script>
<FocusTrap>
<div
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"
>
<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>
<form on:submit|preventDefault={editUser} autocomplete="off">
<div class="my-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>
</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}
<ConfirmDialogue
title="Reset Password"
id="reset-password-modal"
title="Reset password"
confirmText="Reset"
onConfirm={resetPassword}
onClose={() => (isShowResetPasswordConfirmation = false)}

View file

@ -2,7 +2,6 @@
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiFolderRemove } from '@mdi/js';
import { onMount } from 'svelte';
@ -29,48 +28,42 @@
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
</script>
<FullScreenModal onClose={handleCancel}>
<div
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 place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiFolderRemove} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Add Exclusion pattern</h1>
<FullScreenModal
id="add-exclusion-pattern-modal"
title="Add exclusion pattern"
icon={mdiFolderRemove}
onClose={handleCancel}
>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<p class="py-5 text-sm">
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 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">
<p class="p-5 text-sm">
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="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>
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
</div>
<div class="mt-8 flex w-full gap-4">
{#if isDuplicate}
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
{/if}
</div>
</form>
</FullScreenModal>

View file

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

View file

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

View file

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

View file

@ -1,6 +1,5 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Icon from '$lib/components/elements/icon.svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { mdiFolderSync } from '@mdi/js';
@ -28,27 +27,21 @@
const handleSubmit = () => dispatch('submit', { ownerId });
</script>
<FullScreenModal onClose={handleCancel}>
<div
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 place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
>
<Icon path={mdiFolderSync} size="4em" />
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Select library owner</h1>
<FullScreenModal
id="select-library-owner-modal"
title="Select library owner"
icon={mdiFolderSync}
onClose={handleCancel}
>
<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">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth>Create</Button>
</div>
<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>
</form>
</FullScreenModal>

View file

@ -21,98 +21,92 @@
const handleClose = () => dispatch('close');
</script>
<FullScreenModal onClose={handleClose}>
<div
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"
<FullScreenModal id="map-settings-modal" title="Map settings" onClose={handleClose}>
<form
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>
<form
on:submit|preventDefault={() => dispatch('save', settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
>
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
<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',
},
]}
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
{#if customDateRange}
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
<div class="flex items-center justify-between gap-8">
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
<DateInput
class="immich-form-input w-40"
type="date"
id="date-after"
max={settings.dateBefore}
bind:value={settings.dateAfter}
/>
<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 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>
</form>
</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>
{/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>

View file

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

View file

@ -50,7 +50,8 @@
{#if removing}
<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?"
confirmText="Remove"
onConfirm={() => handleRemove()}

View file

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

View file

@ -3,12 +3,9 @@
import { quintOut } from 'svelte/easing';
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { clickOutside } from '$lib/utils/click-outside';
import { mdiClose } from '@mdi/js';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
const dispatch = createEventDispatcher<{
close: void;
@ -28,6 +25,8 @@
*/
export let icon: string | undefined = undefined;
$: titleId = `${id}-title`;
onMount(() => {
if (browser) {
const scrollTop = document.documentElement.scrollTop;
@ -51,7 +50,7 @@
<FocusTrap>
<div
aria-modal="true"
aria-labelledby={`${id}-title`}
aria-labelledby={titleId}
style:z-index={zIndex}
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"
@ -61,23 +60,11 @@
onOutclick: () => 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"
>
<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={`${id}-title`}>
{title}
</h1>
</div>
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
</div>
<ModalHeader id={titleId} {title} {showLogo} {icon} on:close />
<div>
<slot />

View file

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

View file

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

View file

@ -3,6 +3,7 @@
import Button from '../elements/buttons/button.svelte';
import type { Color } from '$lib/components/elements/buttons/button.svelte';
export let id: string;
export let title = 'Confirm';
export let prompt = 'Are you sure you want to do this?';
export let confirmText = 'Confirm';
@ -11,7 +12,7 @@
export let cancelColor: Color = 'primary';
export let hideCancelButton = false;
export let disabled = false;
export let width = 500;
export let width: 'wide' | 'narrow' = 'narrow';
export let onClose: () => void;
export let onConfirm: () => void;
@ -23,35 +24,21 @@
};
</script>
<FullScreenModal {onClose}>
<div
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"
style="width: {width}px"
>
<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>
<FullScreenModal {title} {id} {onClose} {width}>
<div class="text-md py-5 text-center">
<slot name="prompt">
<p>{prompt}</p>
</slot>
</div>
<div class="mt-4 flex w-full gap-4 px-4">
{#if !hideCancelButton}
<Button color={cancelColor} fullwidth on:click={onClose}>
{cancelText}
</Button>
{/if}
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
{confirmText}
</Button>
</div>
</div>
<div class="mt-4 flex flex-col sm:flex-row w-full gap-4">
{#if !hideCancelButton}
<Button color={cancelColor} fullwidth on:click={onClose}>
{cancelText}
</Button>
{/if}
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
{confirmText}
</Button>
</div>
</FullScreenModal>

View file

@ -2,8 +2,44 @@
import { clickOutside } from '../../utils/click-outside';
import { fade } from 'svelte/transition';
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>
<FocusTrap>
@ -12,8 +48,17 @@
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"
>
<div class="z-[9999]" use:clickOutside={{ onOutclick: onClose, onEscape: onClose }} tabindex="-1">
<slot />
<div
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>
</section>
</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">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
import { mdiClose } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import FullScreenModal from '../full-screen-modal.svelte';
import UserAvatar from '../user-avatar.svelte';
@ -15,28 +13,14 @@
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
</script>
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
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"
>
<div class="flex items-center">
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm">
SELECT AVATAR COLOR
</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>
<FullScreenModal id="avatar-selector-modal" title="Select avatar color" width="auto" onClose={() => dispatch('close')}>
<div class="flex items-center justify-center 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>
</FullScreenModal>

View file

@ -1,8 +1,7 @@
<script lang="ts">
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { createEventDispatcher } from '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';
interface Shortcuts {
@ -38,63 +37,51 @@
}>();
</script>
<FullScreenModal onClose={() => dispatch('close')}>
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
<div
class="rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
>
<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="absolute inset-y-0 right-0 px-4 py-4">
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
</div>
<FullScreenModal
id="keyboard-shortcuts-modal"
title="Keyboard shortcuts"
width="auto"
onClose={() => dispatch('close')}
>
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
<div class="p-4">
<h2>General</h2>
<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 class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
<div class="px-4 py-4">
<h2>General</h2>
<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 class="p-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>
</div>
<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>
{/each}
</div>
</div>
</div>

View file

@ -33,34 +33,28 @@
</script>
{#if showModal}
<FullScreenModal onClose={() => (showModal = false)}>
<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"
>
<p class="mb-4 text-2xl">🎉 NEW VERSION AVAILABLE 🎉</p>
<FullScreenModal id="new-version-modal" title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}>
<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>
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="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">
<code>Server Version: {serverVersion}</code>
<br />
<code>Latest Version: {releaseVersion}</code>
</div>
<div class="mt-8 text-right">
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
</div>
<div class="mt-8 text-right">
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
</div>
</FullScreenModal>
{/if}

View file

@ -30,31 +30,22 @@
};
</script>
<FullScreenModal {onClose}>
<div
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"
>
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
Slideshow Settings
</h1>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title="Direction"
options={Object.values(options)}
selectedOption={options[$slideshowNavigation]}
onToggle={(option) => handleToggle(option)}
/>
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
<SettingInputField
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>
<FullScreenModal id="slideshow-settings-modal" title="Slideshow settings" {onClose}>
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
<SettingDropdown
title="Direction"
options={Object.values(options)}
selectedOption={options[$slideshowNavigation]}
onToggle={(option) => handleToggle(option)}
/>
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
<SettingInputField
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>
</FullScreenModal>

View file

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

View file

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

View file

@ -81,7 +81,7 @@
{#if newKey}
<APIKeyForm
title="New API Key"
title="New API key"
submitText="Create"
apiKey={newKey}
on:submit={({ detail }) => handleCreate(detail)}
@ -95,6 +95,7 @@
{#if editKey}
<APIKeyForm
title="API key"
submitText="Save"
apiKey={editKey}
on:submit={({ detail }) => handleUpdate(detail)}
@ -104,7 +105,8 @@
{#if deleteKey}
<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()}
onClose={() => (deleteKey = null)}
/>

View file

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

View file

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

View file

@ -88,7 +88,8 @@
{#if deleteLinkId}
<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?"
confirmText="Delete"
onConfirm={() => handleDeleteLink()}

View file

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

View file

@ -302,6 +302,7 @@
{#if confirmDeleteLibrary}
<ConfirmDialogue
id="warning-modal"
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."
onConfirm={handleDelete}

View file

@ -1,9 +1,9 @@
<script lang="ts">
import { page } from '$app/stores';
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 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 Icon from '$lib/components/elements/icon.svelte';
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
@ -21,7 +21,14 @@
import { asByteUnitString } from '$lib/utils/byte-units';
import { copyToClipboard } from '$lib/utils';
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 { onMount } from 'svelte';
import type { PageData } from './$types';
@ -116,13 +123,23 @@
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 lg:w-[850px]">
{#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)} />
</FullScreenModal>
{/if}
{#if shouldShowEditUserForm}
<FullScreenModal onClose={() => (shouldShowEditUserForm = false)}>
<FullScreenModal
id="edit-user-modal"
title="Edit user"
icon={mdiAccountEditOutline}
onClose={() => (shouldShowEditUserForm = false)}
>
<EditUserForm
user={selectedUser}
bind:newPassword
@ -153,40 +170,39 @@
{/if}
{#if shouldShowPasswordResetSuccess}
<FullScreenModal onClose={() => (shouldShowPasswordResetSuccess = false)}>
<ConfirmDialogue
title="Password Reset Success"
confirmText="Done"
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
onClose={() => (shouldShowPasswordResetSuccess = false)}
hideCancelButton={true}
confirmColor="green"
>
<svelte:fragment slot="prompt">
<div class="flex flex-col gap-4">
<p>The user's password has been reset:</p>
<ConfirmDialogue
id="password-reset-success-modal"
title="Password reset success"
confirmText="Done"
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
onClose={() => (shouldShowPasswordResetSuccess = false)}
hideCancelButton={true}
confirmColor="green"
>
<svelte:fragment slot="prompt">
<div class="flex flex-col gap-4">
<p>The user's password has been reset:</p>
<div class="flex justify-center gap-2">
<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"
>
{newPassword}
</code>
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiContentCopy} size="18" />
</div>
</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 class="flex justify-center gap-2">
<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"
>
{newPassword}
</code>
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiContentCopy} size="18" />
</div>
</LinkButton>
</div>
</svelte:fragment>
</ConfirmDialogue>
</FullScreenModal>
<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>
</svelte:fragment>
</ConfirmDialogue>
{/if}
<table class="my-5 w-full text-left">