1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-28 06:31:58 +00:00

feat(web,a11y): consolidate BaseModal into FullScreenModal (#8787)

* feat(web,a11y): FullScreenModal sticky buttons

* chore(web): combine BaseModal into FullScreenModal

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Ben 2024-04-16 05:06:15 +00:00 committed by GitHub
parent 28f591d01b
commit bcdec25843
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 251 additions and 337 deletions

View file

@ -7,7 +7,6 @@
import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import {
NotificationType,
notificationController,
@ -432,9 +431,12 @@
{#if allowEdit}
<!-- Edit Modal -->
{#if albumToEdit}
<FullScreenModal id="edit-album-modal" title="Edit album" width="wide" onClose={() => (albumToEdit = null)}>
<EditAlbumForm album={albumToEdit} onEditSuccess={successEditAlbumInfo} onCancel={() => (albumToEdit = null)} />
</FullScreenModal>
<EditAlbumForm
album={albumToEdit}
onEditSuccess={successEditAlbumInfo}
onCancel={() => (albumToEdit = null)}
onClose={() => (albumToEdit = null)}
/>
{/if}
<!-- Share Modal -->

View file

@ -5,12 +5,12 @@
import { getContextMenuPosition } from '../../utils/context-menu';
import { handleError } from '../../utils/handle-error';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import BaseModal from '../shared-components/base-modal.svelte';
import ConfirmDialogue from '../shared-components/confirm-dialogue.svelte';
import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let album: AlbumResponseDto;
export let onClose: () => void;
@ -66,7 +66,7 @@
</script>
{#if !selectedRemoveUser}
<BaseModal id="share-info-modal" title="Options" {onClose}>
<FullScreenModal id="share-info-modal" title="Options" {onClose}>
<section class="immich-scrollbar max-h-[400px] overflow-y-auto pb-4">
<div class="flex w-full place-items-center justify-between gap-4 p-5">
<div class="flex place-items-center gap-4">
@ -80,7 +80,7 @@
</div>
{#each album.sharedUsers as user}
<div
class="flex w-full place-items-center justify-between gap-4 p-5 transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
class="flex w-full place-items-center justify-between gap-4 p-5 rounded-xl transition-colors hover:bg-gray-50 dark:hover:bg-gray-700"
>
<div class="flex place-items-center gap-4">
<UserAvatar {user} size="md" />
@ -116,7 +116,7 @@
</div>
{/each}
</section>
</BaseModal>
</FullScreenModal>
{/if}
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}

View file

@ -12,8 +12,8 @@
import { mdiCheck, mdiLink, mdiShareCircle } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import BaseModal from '../shared-components/base-modal.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let album: AlbumResponseDto;
export let onClose: () => void;
@ -54,7 +54,7 @@
};
</script>
<BaseModal id="user-selection-modal" title="Invite to album" showLogo {onClose}>
<FullScreenModal id="user-selection-modal" title="Invite to album" showLogo {onClose}>
{#if selectedUsers.length > 0}
<div class="mb-2 flex flex-wrap place-items-center gap-4 overflow-x-auto px-5 py-2 sticky">
<p class="font-medium">To</p>
@ -75,13 +75,13 @@
<div class="immich-scrollbar max-h-[500px] overflow-y-auto">
{#if users.length > 0}
<p class="px-5 text-xs font-medium">SUGGESTIONS</p>
<p class="text-xs font-medium">SUGGESTIONS</p>
<div class="my-4">
{#each users as user}
<button
on:click={() => handleSelect(user)}
class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
>
{#if selectedUsers.includes(user)}
<div
@ -112,7 +112,7 @@
</div>
{#if users.length > 0}
<div class="p-3">
<div class="py-3">
<Button
size="sm"
fullwidth
@ -125,7 +125,7 @@
<hr />
<div id="shared-buttons" class="my-4 flex place-content-center place-items-center justify-around">
<div id="shared-buttons" class="mt-4 flex place-content-center place-items-center justify-around">
<button
class="flex flex-col place-content-center place-items-center gap-2 hover:cursor-pointer"
on:click={() => dispatch('share')}
@ -144,4 +144,4 @@
</button>
{/if}
</div>
</BaseModal>
</FullScreenModal>

View file

@ -29,7 +29,7 @@
<button
on:click={() => dispatch('album')}
class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
class="flex w-full gap-4 px-6 py-2 text-left transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
>
<div class="h-12 w-12 shrink-0 rounded-xl bg-slate-300">
{#if album.albumThumbnailAssetId}

View file

@ -27,6 +27,7 @@
export let fullwidth = false;
export let border = false;
export let title: string | undefined = '';
export let form: string | undefined = undefined;
let className = '';
export { className as class };
@ -65,6 +66,7 @@
{type}
{disabled}
{title}
{form}
on:click
on:focus
on:blur

View file

@ -102,8 +102,8 @@
<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">
<svelte:fragment slot="sticky-bottom">
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
</div>
</svelte:fragment>
</FullScreenModal>

View file

@ -20,14 +20,14 @@
};
</script>
<FullScreenModal id="set-birthday-modal" title="Set date of birth" icon={mdiCake} onClose={handleCancel}>
<FullScreenModal id="set-birth-date-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">
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="set-birth-date-form">
<div class="my-4 flex flex-col gap-2">
<DateInput
class="immich-form-input"
@ -38,9 +38,9 @@
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>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth form="set-birth-date-form">Set</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -29,15 +29,14 @@
</script>
<FullScreenModal id="api-key-modal" {title} icon={mdiKeyVariant} onClose={handleCancel}>
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
<form on:submit|preventDefault={handleSubmit} autocomplete="off" id="api-key-form">
<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>
<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>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
<Button type="submit" fullwidth form="api-key-form">{submitText}</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -31,10 +31,10 @@
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
</div>
<div class="mt-8 flex w-full gap-4">
<svelte:fragment slot="sticky-bottom">
{#if canCopyImagesToClipboard}
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
{/if}
<Button on:click={() => handleDone()} fullwidth>Done</Button>
</div>
</svelte:fragment>
</FullScreenModal>

View file

@ -7,6 +7,9 @@
import Button from '../elements/buttons/button.svelte';
import PasswordField from '../shared-components/password-field.svelte';
import Slider from '../elements/slider.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let onClose: () => void;
let error: string;
let success: string;
@ -68,53 +71,55 @@
}
</script>
<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>
<FullScreenModal id="create-new-user-modal" title="Create new user" showLogo {onClose}>
<form on:submit|preventDefault={registerUser} autocomplete="off" id="create-new-user-form">
<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>
<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="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="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="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="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="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="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="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="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="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>
{#if error}
<p class="text-sm text-red-400">{error}</p>
{/if}
{#if error}
<p class="text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="text-sm text-immich-primary">{success}</p>
{/if}
<div class="flex w-full gap-4 pt-4">
{#if success}
<p class="text-sm text-immich-primary">{success}</p>
{/if}
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
</div>
</form>
<Button type="submit" disabled={isCreatingUser} fullwidth form="create-new-user-form">Create</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -3,10 +3,12 @@
import { handleError } from '$lib/utils/handle-error';
import Button from '$lib/components/elements/buttons/button.svelte';
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let album: AlbumResponseDto;
export let onEditSuccess: ((album: AlbumResponseDto) => unknown) | undefined = undefined;
export let onCancel: (() => unknown) | undefined = undefined;
export let onClose: () => void;
let albumName = album.albumName;
let description = album.description;
@ -34,29 +36,28 @@
};
</script>
<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} />
<FullScreenModal id="edit-album-modal" title="Edit album" width="wide" {onClose}>
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off" id="edit-album-form">
<div class="flex items-center">
<div class="hidden sm:flex">
<AlbumCover {album} css="h-[200px] w-[200px] m-4 shadow-lg" />
</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 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 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>
</form>
</form>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
<Button type="submit" fullwidth disabled={isSubmitting} form="edit-album-form">OK</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -7,10 +7,13 @@
import { updateUser, type UserResponseDto } from '@immich/sdk';
import { createEventDispatcher } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import { mdiAccountEditOutline } from '@mdi/js';
export let user: UserResponseDto;
export let canResetPassword = true;
export let newPassword: string;
export let onClose: () => void;
let error: string;
let success: string;
@ -87,59 +90,63 @@
}
</script>
<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>
<FullScreenModal id="edit-user-modal" title="Edit user" icon={mdiAccountEditOutline} {onClose}>
<form on:submit|preventDefault={editUser} autocomplete="off" id="edit-user-form">
<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>
<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="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="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}
/>
<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>
<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 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 success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
</form>
<svelte:fragment slot="sticky-bottom">
{#if canResetPassword}
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
>Reset password</Button
>
{/if}
<Button type="submit" fullwidth>Confirm</Button>
</div>
</form>
<Button type="submit" fullwidth form="edit-user-form">Confirm</Button>
</svelte:fragment>
</FullScreenModal>
{#if isShowResetPasswordConfirmation}
<ConfirmDialogue

View file

@ -34,7 +34,7 @@
icon={mdiFolderRemove}
onClose={handleCancel}
>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
<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.
@ -52,18 +52,17 @@
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}
<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>
<svelte:fragment slot="sticky-bottom">
<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 form="add-exclusion-pattern-form">{submitText}</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -31,7 +31,7 @@
</script>
<FullScreenModal id="library-import-path-modal" {title} icon={mdiFolderSync} onClose={handleCancel}>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form">
<p class="py-5 text-sm">
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.
</p>
@ -41,19 +41,17 @@
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
</div>
<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}
<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 import path already exists.</p>
{/if}
</div>
</form>
<svelte:fragment slot="sticky-bottom">
<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 form="library-import-path-form">{submitText}</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -33,15 +33,13 @@
icon={mdiFolderSync}
onClose={handleCancel}
>
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
<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>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
<Button type="submit" fullwidth form="select-library-owner-form">Create</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -25,6 +25,7 @@
<form
on:submit|preventDefault={() => dispatch('save', settings)}
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
id="map-settings-form"
>
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
@ -103,10 +104,9 @@
</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>
<svelte:fragment slot="sticky-bottom">
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
<Button type="submit" size="sm" fullwidth form="map-settings-form">Save</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -4,8 +4,8 @@
import { mdiPlus } from '@mdi/js';
import { createEventDispatcher, onMount } from 'svelte';
import AlbumListItem from '../asset-viewer/album-list-item.svelte';
import BaseModal from './base-modal.svelte';
import { normalizeSearchString } from '$lib/utils/string-utils';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
let albums: AlbumResponseDto[] = [];
let recentAlbums: AlbumResponseDto[] = [];
@ -52,7 +52,7 @@
};
</script>
<BaseModal id="album-selection-modal" title={getTitle()} {onClose}>
<FullScreenModal id="album-selection-modal" title={getTitle()} {onClose}>
<div class="mb-2 flex max-h-[400px] flex-col">
{#if loading}
{#each { length: 3 } as _}
@ -76,7 +76,7 @@
<div class="immich-scrollbar overflow-y-auto">
<button
on:click={handleNew}
class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700"
class="flex w-full items-center gap-4 px-6 py-2 transition-colors hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
>
<div class="flex h-12 w-12 items-center justify-center">
<Icon path={mdiPlus} size="30" />
@ -110,4 +110,4 @@
</div>
{/if}
</div>
</BaseModal>
</FullScreenModal>

View file

@ -1,78 +0,0 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { onMount, onDestroy } from 'svelte';
import { browser } from '$app/environment';
import { clickOutside } from '$lib/utils/click-outside';
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
/**
* Unique identifier for the modal.
*/
export let id: string;
export let title: string;
export let onClose: () => void;
export let zIndex = 9999;
/**
* 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;
$: titleId = `${id}-title`;
onMount(() => {
if (browser) {
const scrollTop = document.documentElement.scrollTop;
const scrollLeft = document.documentElement.scrollLeft;
/* eslint-disable unicorn/prefer-add-event-listener */
window.onscroll = function () {
window.scrollTo(scrollLeft, scrollTop);
};
}
});
onDestroy(() => {
if (browser) {
/* eslint-disable unicorn/prefer-add-event-listener */
window.onscroll = null;
}
});
</script>
<FocusTrap>
<div
aria-modal="true"
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"
>
<div
use:clickOutside={{
onOutclick: onClose,
onEscape: onClose,
}}
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"
>
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
<div>
<slot />
</div>
{#if $$slots['sticky-bottom']}
<div class="sticky bottom-0 bg-immich-bg px-5 pb-5 pt-3 dark:bg-immich-dark-gray">
<slot name="sticky-bottom" />
</div>
{/if}
</div>
</div>
</FocusTrap>

View file

@ -65,7 +65,6 @@
<ConfirmDialogue
id="edit-date-time-modal"
confirmColor="primary"
cancelColor="secondary"
title="Edit date and time"
prompt="Please select a new date:"
disabled={!date.isValid}

View file

@ -96,7 +96,6 @@
<ConfirmDialogue
id="change-location-modal"
confirmColor="primary"
cancelColor="secondary"
title="Change location"
width="wide"
onConfirm={handleConfirm}

View file

@ -9,7 +9,7 @@
export let confirmText = 'Confirm';
export let confirmColor: Color = 'red';
export let cancelText = 'Cancel';
export let cancelColor: Color = 'primary';
export let cancelColor: Color = 'secondary';
export let hideCancelButton = false;
export let disabled = false;
export let width: 'wide' | 'narrow' = 'narrow';
@ -31,7 +31,7 @@
</slot>
</div>
<div class="mt-4 flex flex-col sm:flex-row w-full gap-4">
<svelte:fragment slot="sticky-bottom">
{#if !hideCancelButton}
<Button color={cancelColor} fullwidth on:click={onClose}>
{cancelText}
@ -40,5 +40,5 @@
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
{confirmText}
</Button>
</div>
</svelte:fragment>
</FullScreenModal>

View file

@ -8,12 +8,12 @@
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiContentCopy, mdiLink } from '@mdi/js';
import { createEventDispatcher } from 'svelte';
import BaseModal from '../base-modal.svelte';
import type { ImmichDropDownOption } from '../dropdown-button.svelte';
import DropdownButton from '../dropdown-button.svelte';
import { NotificationType, notificationController } from '../notification/notification';
import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte';
import SettingSwitch from '../settings/setting-switch.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let onClose: () => void;
export let albumId: string | undefined = undefined;
@ -159,8 +159,8 @@
};
</script>
<BaseModal id="create-shared-link-modal" title={getTitle()} icon={mdiLink} {onClose}>
<section class="mx-6 mb-6">
<FullScreenModal id="create-shared-link-modal" title={getTitle()} icon={mdiLink} {onClose}>
<section>
{#if shareType === SharedLinkType.Album}
{#if !editingLink}
<div>Let anyone with the link see photos and people in this album.</div>
@ -246,29 +246,22 @@
</div>
</section>
<hr />
<section slot="sticky-bottom">
<svelte:fragment slot="sticky-bottom">
{#if !sharedLink}
{#if editingLink}
<div class="flex justify-end">
<Button size="sm" on:click={handleEditLink}>Confirm</Button>
</div>
<Button size="sm" fullwidth on:click={handleEditLink}>Confirm</Button>
{:else}
<div class="flex justify-end">
<Button size="sm" on:click={handleCreateSharedLink}>Create link</Button>
</div>
<Button size="sm" fullwidth on:click={handleCreateSharedLink}>Create link</Button>
{/if}
{:else}
<div class="flex w-full gap-4">
<div class="flex w-full gap-2">
<input class="immich-form-input w-full" bind:value={sharedLink} disabled />
<LinkButton on:click={() => (sharedLink ? copyToClipboard(sharedLink) : '')}>
<div class="flex place-items-center gap-2 text-sm">
<Icon path={mdiContentCopy} size="18" />
<Icon path={mdiContentCopy} ariaLabel="Copy link to clipboard" size="18" />
</div>
</LinkButton>
</div>
{/if}
</section>
</BaseModal>
</svelte:fragment>
</FullScreenModal>

View file

@ -29,6 +29,7 @@
export let width: 'wide' | 'narrow' | 'auto' = 'narrow';
$: titleId = `${id}-title`;
$: isStickyBottom = !!$$slots['sticky-bottom'];
let modalWidth: string;
$: {
@ -50,15 +51,25 @@
>
<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"
style="max-height: min(95vh, 900px);"
use:clickOutside={{ onOutclick: onClose, onEscape: onClose }}
tabindex="-1"
aria-modal="true"
aria-labelledby={titleId}
class:scroll-pb-40={isStickyBottom}
class:sm:scroll-p-24={isStickyBottom}
>
<ModalHeader id={titleId} {title} {showLogo} {icon} {onClose} />
<div class="p-5 pt-0">
<slot />
</div>
{#if isStickyBottom}
<div
class="flex flex-col sm:flex-row justify-end w-full gap-2 sm:gap-4 sticky bottom-0 py-4 px-5 bg-immich-bg dark:bg-immich-dark-gray border-t border-gray-200 dark:border-gray-500 shadow"
>
<slot name="sticky-bottom" />
</div>
{/if}
</div>
</section>
</FocusTrap>

View file

@ -6,8 +6,8 @@
import { onMount } from 'svelte';
import PhotoViewer from '../asset-viewer/photo-viewer.svelte';
import Button from '../elements/buttons/button.svelte';
import BaseModal from './base-modal.svelte';
import { NotificationType, notificationController } from './notification/notification';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let asset: AssetResponseDto;
export let onClose: () => void;
@ -69,18 +69,15 @@
};
</script>
<BaseModal id="profile-image-cropper" title="Set profile picture" {onClose}>
<FullScreenModal id="profile-image-cropper" title="Set profile picture" width="auto" {onClose}>
<div class="flex place-items-center items-center justify-center">
<div
class="relative flex aspect-square w-1/2 overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
class="relative flex aspect-square w-[250px] overflow-hidden rounded-full border-4 border-immich-primary bg-immich-dark-primary dark:border-immich-dark-primary dark:bg-immich-primary"
>
<PhotoViewer bind:element={imgElement} {asset} />
</div>
</div>
<span class="flex justify-end p-4">
<Button on:click={handleSetProfilePicture}>
<p>Set as profile picture</p>
</Button>
</span>
<div class="mb-2 flex max-h-[400px] flex-col" />
</BaseModal>
<svelte:fragment slot="sticky-bottom">
<Button fullwidth on:click={handleSetProfilePicture}>Set as profile picture</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -53,8 +53,8 @@
<code>Latest Version: {releaseVersion}</code>
</div>
<div class="mt-8 text-right">
<svelte:fragment slot="sticky-bottom">
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
</div>
</svelte:fragment>
</FullScreenModal>
{/if}

View file

@ -46,6 +46,8 @@
min={1}
bind:value={$slideshowDelay}
/>
<Button class="w-full" color="gray" on:click={onClose}>Done</Button>
</div>
<svelte:fragment slot="sticky-bottom">
<Button fullwidth color="primary" on:click={onClose}>Done</Button>
</svelte:fragment>
</FullScreenModal>

View file

@ -2,8 +2,8 @@
import { getAllUsers, getPartners, type UserResponseDto } from '@immich/sdk';
import { createEventDispatcher, onMount } from 'svelte';
import Button from '../elements/buttons/button.svelte';
import BaseModal from '../shared-components/base-modal.svelte';
import UserAvatar from '../shared-components/user-avatar.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
export let user: UserResponseDto;
export let onClose: () => void;
@ -33,13 +33,13 @@
};
</script>
<BaseModal id="partner-selection-modal" title="Add partner" showLogo {onClose}>
<FullScreenModal id="partner-selection-modal" title="Add partner" showLogo {onClose}>
<div class="immich-scrollbar max-h-[300px] overflow-y-auto">
{#if availableUsers.length > 0}
{#each availableUsers as user}
<button
on:click={() => selectUser(user)}
class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700"
class="flex w-full place-items-center gap-4 px-5 py-4 transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"
>
{#if selectedUsers.includes(user)}
<span
@ -61,15 +61,15 @@
</button>
{/each}
{:else}
<p class="p-5 text-sm">
<p class="py-5 text-sm">
Looks like you shared your photos with all users or you don't have any user to share with.
</p>
{/if}
{#if selectedUsers.length > 0}
<div class="flex place-content-end p-5">
<Button size="sm" rounded="lg" on:click={() => dispatch('add-users', selectedUsers)}>Add</Button>
<div class="pt-5">
<Button size="sm" fullwidth on:click={() => dispatch('add-users', selectedUsers)}>Add</Button>
</div>
{/if}
</div>
</BaseModal>
</FullScreenModal>

View file

@ -465,24 +465,22 @@
{#if showChangeNameModal}
<FullScreenModal id="change-name-modal" title="Change name" onClose={() => (showChangeNameModal = false)}>
<form on:submit|preventDefault={submitNameChange} autocomplete="off">
<form on:submit|preventDefault={submitNameChange} autocomplete="off" id="change-name-form">
<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>
<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>
<input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} />
</div>
</form>
<svelte:fragment slot="sticky-bottom">
<Button
color="gray"
fullwidth
on:click={() => {
showChangeNameModal = false;
}}>Cancel</Button
>
<Button type="submit" fullwidth form="change-name-form">Ok</Button>
</svelte:fragment>
</FullScreenModal>
{/if}

View file

@ -9,7 +9,6 @@
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
import EditUserForm from '$lib/components/forms/edit-user-form.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import {
NotificationType,
notificationController,
@ -21,14 +20,7 @@
import { asByteUnitString } from '$lib/utils/byte-units';
import { copyToClipboard } from '$lib/utils';
import { UserStatus, getAllUsers, type UserResponseDto } from '@immich/sdk';
import {
mdiAccountEditOutline,
mdiClose,
mdiContentCopy,
mdiDeleteRestore,
mdiPencilOutline,
mdiTrashCanOutline,
} from '@mdi/js';
import { mdiClose, mdiContentCopy, mdiDeleteRestore, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js';
import { DateTime } from 'luxon';
import { onMount } from 'svelte';
import type { PageData } from './$types';
@ -123,32 +115,22 @@
<section id="setting-content" class="flex place-content-center sm:mx-4">
<section class="w-full pb-28 lg:w-[850px]">
{#if shouldShowCreateUserForm}
<FullScreenModal
id="create-new-user-modal"
title="Create new user"
showLogo
<CreateUserForm
on:submit={onUserCreated}
on:cancel={() => (shouldShowCreateUserForm = false)}
onClose={() => (shouldShowCreateUserForm = false)}
>
<CreateUserForm on:submit={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} />
</FullScreenModal>
/>
{/if}
{#if shouldShowEditUserForm}
<FullScreenModal
id="edit-user-modal"
title="Edit user"
icon={mdiAccountEditOutline}
<EditUserForm
user={selectedUser}
bind:newPassword
canResetPassword={selectedUser?.id !== $user.id}
on:editSuccess={onEditUserSuccess}
on:resetPasswordSuccess={onEditPasswordSuccess}
onClose={() => (shouldShowEditUserForm = false)}
>
<EditUserForm
user={selectedUser}
bind:newPassword
canResetPassword={selectedUser?.id !== $user.id}
on:editSuccess={onEditUserSuccess}
on:resetPasswordSuccess={onEditPasswordSuccess}
on:close={() => (shouldShowEditUserForm = false)}
/>
</FullScreenModal>
/>
{/if}
{#if shouldShowDeleteConfirmDialog}