Merge branch 'main' into feature/readonly-sharing
# Conflicts: # mobile/openapi/.openapi-generator/FILES # mobile/openapi/README.md # mobile/openapi/lib/api.dart # mobile/openapi/lib/api_client.dart # server/src/services/album.service.spec.ts
This commit is contained in:
@@ -32,7 +32,7 @@
|
||||
|
||||
{#if isOwned}
|
||||
<textarea
|
||||
class="w-full mt-2 resize-none overflow-hidden text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
|
||||
class="w-full mt-2 resize-none text-black dark:text-white border-b-2 border-transparent border-gray-500 bg-transparent text-base outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:focus:border-immich-dark-primary hover:border-gray-400"
|
||||
bind:value={newDescription}
|
||||
on:input={(e) => autoGrowHeight(e.currentTarget)}
|
||||
on:focusout={handleUpdateDescription}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -11,12 +11,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;
|
||||
@@ -86,7 +86,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">
|
||||
@@ -108,7 +108,7 @@
|
||||
return a.user.name.localeCompare(b.user.name);
|
||||
}) as { user, role }}
|
||||
<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" />
|
||||
@@ -156,7 +156,7 @@
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</BaseModal>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
/>
|
||||
{/if}
|
||||
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || numberOfComments > 0)}
|
||||
<div class="z-[9999] absolute bottom-0 right-0 mb-6 mr-6 justify-self-end">
|
||||
<div class="z-[9999] absolute bottom-0 right-0 mb-4 mr-6">
|
||||
<ActivityStatus
|
||||
disabled={!album?.isActivityEnabled}
|
||||
{isLiked}
|
||||
|
||||
@@ -29,7 +29,11 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
<div
|
||||
transition:fade={{ duration: 150 }}
|
||||
class="flex select-none place-content-center place-items-center"
|
||||
style="height: calc(100% - 64px)"
|
||||
>
|
||||
<video
|
||||
bind:this={element}
|
||||
autoplay
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,9 +28,7 @@
|
||||
<Icon path={mdiMagnify} size="24" />
|
||||
</div>
|
||||
</button>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="w-full gap-2 bg-gray-200 dark:bg-immich-dark-gray dark:text-white"
|
||||
type="text"
|
||||
{placeholder}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
|
||||
@@ -9,11 +9,17 @@
|
||||
export let suggestedPeople = false;
|
||||
export let thumbnailData: string;
|
||||
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
change: string;
|
||||
cancel: void;
|
||||
input: void;
|
||||
}>();
|
||||
|
||||
onMount(() => {
|
||||
inputElement.focus();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -27,13 +33,12 @@
|
||||
autocomplete="off"
|
||||
on:submit|preventDefault={() => dispatch('change', name)}
|
||||
>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
class="w-full gap-2 bg-gray-100 dark:bg-gray-700 dark:text-white"
|
||||
type="text"
|
||||
placeholder="New name or nickname"
|
||||
bind:value={name}
|
||||
bind:this={inputElement}
|
||||
on:input={() => dispatch('input')}
|
||||
/>
|
||||
<Button size="sm" type="submit">Done</Button>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
});
|
||||
|
||||
$: isDuplicate = exclusionPattern !== null && exclusionPatterns.includes(exclusionPattern);
|
||||
$: canSubmit = exclusionPattern !== '' && exclusionPattern !== null && !exclusionPatterns.includes(exclusionPattern);
|
||||
$: canSubmit = exclusionPattern && !exclusionPatterns.includes(exclusionPattern);
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
cancel: void;
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -71,11 +71,13 @@
|
||||
};
|
||||
|
||||
const onDelete = () => {
|
||||
if (!isTrashEnabled && $showDeleteModal) {
|
||||
const hasTrashedAsset = Array.from($selectedAssets).some((asset) => asset.isTrashed);
|
||||
|
||||
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
||||
isShowDeleteConfirmation = true;
|
||||
return;
|
||||
}
|
||||
handlePromiseError(trashOrDelete(false));
|
||||
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
||||
};
|
||||
|
||||
const onForceDelete = () => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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}
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
<ConfirmDialogue
|
||||
id="change-location-modal"
|
||||
confirmColor="primary"
|
||||
cancelColor="secondary"
|
||||
title="Change location"
|
||||
width="wide"
|
||||
onConfirm={handleConfirm}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
|
||||
const getFocusableElements = () => {
|
||||
return Array.from(
|
||||
container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'),
|
||||
container.querySelectorAll(
|
||||
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
||||
),
|
||||
) as HTMLElement[];
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import type { DateTime } from 'luxon';
|
||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
|
||||
export let timelineY = 0;
|
||||
export let height = 0;
|
||||
@@ -12,8 +14,10 @@
|
||||
let isDragging = false;
|
||||
let isAnimating = false;
|
||||
let hoverLabel = '';
|
||||
let hoverY = 0;
|
||||
let clientY = 0;
|
||||
let windowHeight = 0;
|
||||
let scrollBar: HTMLElement | undefined;
|
||||
|
||||
const toScrollY = (timelineY: number) => (timelineY / $assetStore.timelineHeight) * height;
|
||||
const toTimelineY = (scrollY: number) => Math.round((scrollY * $assetStore.timelineHeight) / height);
|
||||
@@ -21,7 +25,16 @@
|
||||
const HOVER_DATE_HEIGHT = 30;
|
||||
const MIN_YEAR_LABEL_DISTANCE = 16;
|
||||
|
||||
$: hoverY = height - windowHeight + clientY;
|
||||
$: {
|
||||
hoverY = clamp(height - windowHeight + clientY, 0, height);
|
||||
if (scrollBar) {
|
||||
const rect = scrollBar.getBoundingClientRect();
|
||||
const x = rect.left + rect.width / 2;
|
||||
const y = rect.top + Math.min(hoverY, height - 1);
|
||||
updateLabel(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
$: scrollY = toScrollY(timelineY);
|
||||
|
||||
class Segment {
|
||||
@@ -58,6 +71,21 @@
|
||||
const dispatch = createEventDispatcher<{ scrollTimeline: number }>();
|
||||
const scrollTimeline = () => dispatch('scrollTimeline', toTimelineY(hoverY));
|
||||
|
||||
const updateLabel = (cursorX: number, cursorY: number) => {
|
||||
const segment = document.elementsFromPoint(cursorX, cursorY).find(({ id }) => id === 'time-segment');
|
||||
if (!segment) {
|
||||
return;
|
||||
}
|
||||
const attr = (segment as HTMLElement).dataset.date;
|
||||
if (!attr) {
|
||||
return;
|
||||
}
|
||||
hoverLabel = new Date(attr).toLocaleString($locale, {
|
||||
month: 'short',
|
||||
year: 'numeric',
|
||||
});
|
||||
};
|
||||
|
||||
const handleMouseEvent = (event: { clientY: number; isDragging?: boolean }) => {
|
||||
const wasDragging = isDragging;
|
||||
|
||||
@@ -81,7 +109,12 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight={windowHeight} />
|
||||
<svelte:window
|
||||
bind:innerHeight={windowHeight}
|
||||
on:mousemove={({ clientY }) => (isDragging || isHover) && handleMouseEvent({ clientY })}
|
||||
on:mousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })}
|
||||
on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
|
||||
/>
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
@@ -93,20 +126,15 @@
|
||||
style:height={height + 'px'}
|
||||
style:background-color={isDragging ? 'transparent' : 'transparent'}
|
||||
draggable="false"
|
||||
bind:this={scrollBar}
|
||||
on:mouseenter={() => (isHover = true)}
|
||||
on:mouseleave={() => {
|
||||
isHover = false;
|
||||
isDragging = false;
|
||||
}}
|
||||
on:mouseenter={({ clientY, buttons }) => handleMouseEvent({ clientY, isDragging: !!buttons })}
|
||||
on:mousemove={({ clientY }) => handleMouseEvent({ clientY })}
|
||||
on:mousedown={({ clientY }) => handleMouseEvent({ clientY, isDragging: true })}
|
||||
on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })}
|
||||
on:mouseleave={() => (isHover = false)}
|
||||
>
|
||||
{#if isHover}
|
||||
{#if isHover || isDragging}
|
||||
<div
|
||||
class="pointer-events-none absolute right-0 z-[100] w-[100px] rounded-tl-md border-b-2 border-immich-primary bg-immich-bg py-1 pl-1 pr-6 text-sm font-medium shadow-lg dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
style:top="{Math.max(hoverY - HOVER_DATE_HEIGHT, 0)}px"
|
||||
id="time-label"
|
||||
class="pointer-events-none absolute right-0 z-[100] w-[100px] rounded-tl-md border-b-2 border-immich-primary bg-immich-bg py-1 px-1 text-sm font-medium shadow-[0_0_8px_rgba(0,0,0,0.25)] dark:border-immich-dark-primary dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
style:top="{clamp(hoverY - HOVER_DATE_HEIGHT, 0, height - HOVER_DATE_HEIGHT - 2)}px"
|
||||
>
|
||||
{hoverLabel}
|
||||
</div>
|
||||
@@ -121,15 +149,12 @@
|
||||
{/if}
|
||||
<!-- Time Segment -->
|
||||
{#each segments as segment}
|
||||
{@const label = `${segment.date.toLocaleString({ month: 'short' })} ${segment.date.year}`}
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
id="time-segment"
|
||||
class="relative"
|
||||
data-date={segment.date}
|
||||
style:height={segment.height + 'px'}
|
||||
aria-label={segment.timeGroup + ' ' + segment.count}
|
||||
on:mousemove={() => (hoverLabel = label)}
|
||||
>
|
||||
{#if segment.hasLabel}
|
||||
<div
|
||||
|
||||
@@ -43,21 +43,20 @@
|
||||
|
||||
const filterPeople = (list: PersonResponseDto[], name: string) => {
|
||||
const nameLower = name.toLowerCase();
|
||||
return name ? list.filter((p) => p.name.toLowerCase().startsWith(nameLower)) : list;
|
||||
return name ? list.filter((p) => p.name.toLowerCase().includes(nameLower)) : list;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#await peoplePromise then people}
|
||||
{#if people && people.length > 0}
|
||||
{@const peopleList = showAllPeople ? filterPeople(people, name) : people.slice(0, numberOfPeople)}
|
||||
{@const peopleList = showAllPeople
|
||||
? filterPeople(people, name)
|
||||
: filterPeople(people, name).slice(0, numberOfPeople)}
|
||||
|
||||
<div id="people-selection" class="-mb-4">
|
||||
<div class="flex items-center w-full justify-between gap-6">
|
||||
<p class="immich-form-label py-3">PEOPLE</p>
|
||||
|
||||
{#if showAllPeople}
|
||||
<SearchBar bind:name placeholder="Filter people" isSearching={false} />
|
||||
{/if}
|
||||
<SearchBar bind:name placeholder="Filter people" isSearching={false} />
|
||||
</div>
|
||||
|
||||
<div class="flex -mx-1 max-h-64 gap-1 mt-2 flex-wrap overflow-y-auto immich-scrollbar">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export const isExternalUrl = (url: string): boolean => {
|
||||
return new URL(url, window.location.href).origin !== window.location.origin;
|
||||
};
|
||||
|
||||
export const clearQueryParam = async (queryParam: string, url: URL) => {
|
||||
if (url.searchParams.has(queryParam)) {
|
||||
url.searchParams.delete(queryParam);
|
||||
await goto(url);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -452,6 +452,7 @@
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<CircleIconButton title="Slideshow" on:click={handleStartSlideshow} icon={mdiPresentationPlay} />
|
||||
<CircleIconButton title="Download" on:click={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
|
||||
|
||||
{#if isOwned}
|
||||
@@ -459,7 +460,6 @@
|
||||
<CircleIconButton title="Album options" on:click={handleOpenAlbumOptions} icon={mdiDotsVertical}>
|
||||
{#if viewMode === ViewMode.ALBUM_OPTIONS}
|
||||
<ContextMenu {...contextMenuPosition}>
|
||||
<MenuOption icon={mdiPresentationPlay} text="Slideshow" on:click={handleStartSlideshow} />
|
||||
<MenuOption
|
||||
icon={mdiImageOutline}
|
||||
text="Select album cover"
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { clearQueryParam } from '$lib/utils/navigation';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
@@ -279,10 +280,7 @@
|
||||
|
||||
const handleSearchPeople = async (force: boolean) => {
|
||||
if (searchName === '') {
|
||||
if ($page.url.searchParams.has(QueryParameter.SEARCHED_PEOPLE)) {
|
||||
$page.url.searchParams.delete(QueryParameter.SEARCHED_PEOPLE);
|
||||
await goto($page.url);
|
||||
}
|
||||
await clearQueryParam(QueryParameter.SEARCHED_PEOPLE, $page.url);
|
||||
return;
|
||||
}
|
||||
if (!force && people.length < maximumLengthSearchPeople && searchName.startsWith(searchWord)) {
|
||||
@@ -393,6 +391,11 @@
|
||||
handleError(error, 'Unable to save name');
|
||||
}
|
||||
};
|
||||
|
||||
const onResetSearchBar = async () => {
|
||||
searchedPeople = [];
|
||||
await clearQueryParam(QueryParameter.SEARCHED_PEOPLE, $page.url);
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window bind:innerHeight use:shortcut={{ shortcut: { key: 'Escape' }, onShortcut: handleCloseClick }} />
|
||||
@@ -421,9 +424,7 @@
|
||||
bind:name={searchName}
|
||||
isSearching={isSearchingPeople}
|
||||
placeholder="Search people"
|
||||
on:reset={() => {
|
||||
searchedPeople = [];
|
||||
}}
|
||||
on:reset={onResetSearchBar}
|
||||
on:search={({ detail }) => handleSearch(detail.force ?? false)}
|
||||
/>
|
||||
</div>
|
||||
@@ -464,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}
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -216,7 +198,7 @@
|
||||
<th class="w-4/12 lg:w-3/12 xl:w-2/12 text-center text-sm font-medium">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="block max-h-[320px] w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
<tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
|
||||
{#if allUsers}
|
||||
{#each allUsers as immichUser, index}
|
||||
<tr
|
||||
|
||||
Reference in New Issue
Block a user