Merge branch 'main' of github.com:immich-app/immich into web/automation-ui
This commit is contained in:
@@ -50,6 +50,7 @@ module.exports = {
|
||||
'unicorn/no-nested-ternary': 'off',
|
||||
'unicorn/consistent-function-scoping': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
// TODO: set recommended-type-checked and remove these rules
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
FROM node:iron-alpine3.18@sha256:fa5d3cf51725bd42d32e67917623038539dbe720dab082f590785c001eb4dfef
|
||||
FROM node:iron-alpine3.18@sha256:3fb85a68652064ab109ed9730f45a3ede11f064afdd3ad9f96ef7e8a3c55f47e
|
||||
|
||||
RUN apk add --no-cache tini
|
||||
USER node
|
||||
|
||||
Generated
+338
-321
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -25,7 +25,7 @@
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@socket.io/component-emitter": "^3.1.0",
|
||||
"@sveltejs/adapter-static": "^3.0.1",
|
||||
"@sveltejs/enhanced-img": "^0.1.8",
|
||||
"@sveltejs/enhanced-img": "^0.2.0",
|
||||
"@sveltejs/kit": "^2.5.2",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.2",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
@@ -41,7 +41,7 @@
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.35.1",
|
||||
"eslint-plugin-unicorn": "^51.0.1",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"factory.ts": "^1.4.1",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.2.5",
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue
|
||||
title="Delete User"
|
||||
id="delete-user-confirmation-modal"
|
||||
title="Delete user"
|
||||
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
|
||||
onConfirm={handleDeleteUser}
|
||||
onClose={() => dispatch('cancel')}
|
||||
@@ -147,6 +147,7 @@
|
||||
|
||||
{#if confirmJob}
|
||||
<ConfirmDialogue
|
||||
id="reprocess-faces-modal"
|
||||
prompt="Are you sure you want to reprocess all faces? This will also clear named people."
|
||||
{onConfirm}
|
||||
onClose={() => (confirmJob = null)}
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue
|
||||
title="Restore User"
|
||||
id="restore-user-modal"
|
||||
title="Restore user"
|
||||
confirmText="Continue"
|
||||
confirmColor="green"
|
||||
onConfirm={handleRestoreUser}
|
||||
@@ -5,7 +5,7 @@
|
||||
export let onConfirm: () => void;
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue title="Disable Login" onClose={onCancel} {onConfirm}>
|
||||
<ConfirmDialogue id="disable-login-modal" title="Disable login" onClose={onCancel} {onConfirm}>
|
||||
<svelte:fragment slot="prompt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, 31 for VP9, and 35 for AV1. Lower is better, but produces larger files."
|
||||
bind:value={config.ffmpeg.crf}
|
||||
required={true}
|
||||
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
|
||||
@@ -115,12 +115,13 @@
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
{disabled}
|
||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files."
|
||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files. AV1 is the most efficient codec but lacks support on older devices."
|
||||
bind:value={config.ffmpeg.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||
{ value: VideoCodec.Av1, text: 'av1' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
|
||||
@@ -137,6 +138,7 @@
|
||||
{ value: VideoCodec.H264, text: 'H.264' },
|
||||
{ value: VideoCodec.Hevc, text: 'HEVC' },
|
||||
{ value: VideoCodec.Vp9, text: 'VP9' },
|
||||
{ value: VideoCodec.Av1, text: 'AV1' },
|
||||
]}
|
||||
isEdited={!isEqual(sortBy(config.ffmpeg.acceptedVideoCodecs), sortBy(savedConfig.ffmpeg.acceptedVideoCodecs))}
|
||||
/>
|
||||
@@ -179,7 +181,7 @@
|
||||
<SettingSelect
|
||||
label="TRANSCODE POLICY"
|
||||
{disabled}
|
||||
desc="Policy for when a video should be transcoded."
|
||||
desc="Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled)."
|
||||
bind:value={config.ffmpeg.transcode}
|
||||
name="transcode"
|
||||
options={[
|
||||
@@ -355,7 +357,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['ffmpeg'] })}
|
||||
on:save={() => dispatch('save', { ffmpeg: config.ffmpeg })}
|
||||
showResetToDefault={!isEqual(savedConfig.ffmpeg, defaultConfig)}
|
||||
showResetToDefault={!isEqual(savedConfig.ffmpeg, defaultConfig.ffmpeg)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Colorspace, type SystemConfigDto } from '@immich/sdk';
|
||||
import { Colorspace, ImageFormat, type SystemConfigDto } from '@immich/sdk';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -24,6 +24,19 @@
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="THUMBNAIL FORMAT"
|
||||
desc="WebP produces smaller files than JPEG, but is slower to encode."
|
||||
bind:value={config.image.thumbnailFormat}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={config.image.thumbnailFormat !== savedConfig.image.thumbnailFormat}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="THUMBNAIL RESOLUTION"
|
||||
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
@@ -41,6 +54,19 @@
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PREVIEW FORMAT"
|
||||
desc="WebP produces smaller files than JPEG, but is slower to encode."
|
||||
bind:value={config.image.previewFormat}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={config.image.previewFormat !== savedConfig.image.previewFormat}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PREVIEW RESOLUTION"
|
||||
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
@@ -81,7 +107,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['image'] })}
|
||||
on:save={() => dispatch('save', { image: config.image })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
showResetToDefault={!isEqual(savedConfig.image, defaultConfig.image)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['newVersionCheck'] })}
|
||||
on:save={() => dispatch('save', { newVersionCheck: config.newVersionCheck })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
showResetToDefault={!isEqual(savedConfig.newVersionCheck, defaultConfig.newVersionCheck)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['server'] })}
|
||||
on:save={() => dispatch('save', { server: config.server })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
showResetToDefault={!isEqual(savedConfig.server, defaultConfig.server)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -236,7 +236,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['storageTemplate'] })}
|
||||
on:save={() => dispatch('save', { storageTemplate: config.storageTemplate })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig) && !minified}
|
||||
showResetToDefault={!isEqual(savedConfig.storageTemplate, defaultConfig.storageTemplate) && !minified}
|
||||
{disabled}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['trash'] })}
|
||||
on:save={() => dispatch('save', { trash: config.trash })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
showResetToDefault={!isEqual(savedConfig.trash, defaultConfig.trash)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['user'] })}
|
||||
on:save={() => dispatch('save', { user: config.user })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
showResetToDefault={!isEqual(savedConfig.user, defaultConfig.user)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiClose, mdiPlus } from '@mdi/js';
|
||||
import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
@@ -52,67 +50,52 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={() => dispatch('close')}>
|
||||
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden p-2 md:p-0">
|
||||
<div
|
||||
class="w-[550px] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="px-2 pt-2">
|
||||
<div class="flex items-center">
|
||||
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary">Options</h1>
|
||||
<FullScreenModal id="album-options-modal" title="Options" onClose={() => dispatch('close')}>
|
||||
<div class="items-center justify-center">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
|
||||
<div class="grid p-2 gap-y-2">
|
||||
{#if order}
|
||||
<SettingDropdown
|
||||
title="Display order"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[order]}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
{/if}
|
||||
<SettingSwitch
|
||||
id="comments-likes"
|
||||
title="Comments & likes"
|
||||
subtitle="Let others respond"
|
||||
checked={album.isActivityEnabled}
|
||||
on:toggle={() => dispatch('toggleEnableActivity')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
||||
<div class="p-2">
|
||||
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
<div>Invite People</div>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 py-2 mt-2">
|
||||
<div>
|
||||
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
<div>Owner</div>
|
||||
</div>
|
||||
|
||||
<div class=" items-center justify-center p-4">
|
||||
<div class="py-2">
|
||||
<h2 class="text-gray text-sm mb-2">SETTINGS</h2>
|
||||
<div class="grid p-2 gap-y-2">
|
||||
{#if order}
|
||||
<SettingDropdown
|
||||
title="Display order"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[order]}
|
||||
onToggle={handleToggle}
|
||||
/>
|
||||
{/if}
|
||||
<SettingSwitch
|
||||
id="comments-likes"
|
||||
title="Comments & likes"
|
||||
subtitle="Let others respond"
|
||||
checked={album.isActivityEnabled}
|
||||
on:toggle={() => dispatch('toggleEnableActivity')}
|
||||
/>
|
||||
{#each album.sharedUsers as user (user.id)}
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
</div>
|
||||
<div class="py-2">
|
||||
<div class="text-gray text-sm mb-3">PEOPLE</div>
|
||||
<div class="p-2">
|
||||
<button class="flex items-center gap-2" on:click={() => dispatch('showSelectSharedUser')}>
|
||||
<div class="rounded-full w-10 h-10 border border-gray-500 flex items-center justify-center">
|
||||
<div><Icon path={mdiPlus} size="25" /></div>
|
||||
</div>
|
||||
<div>Invite People</div>
|
||||
</button>
|
||||
<div class="flex items-center gap-2 py-2 mt-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
<div>Owner</div>
|
||||
</div>
|
||||
{#each album.sharedUsers as user (user.id)}
|
||||
<div class="flex items-center gap-2 py-2">
|
||||
<div>
|
||||
<UserAvatar {user} size="md" />
|
||||
</div>
|
||||
<div class="w-full">{user.name}</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 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 -->
|
||||
@@ -442,7 +444,7 @@
|
||||
{#if showShareByURLModal}
|
||||
<CreateSharedLinkModal
|
||||
albumId={albumToShare.id}
|
||||
on:close={() => closeShareModal()}
|
||||
onClose={() => closeShareModal()}
|
||||
on:created={() => albumToShare && handleSharedLinkCreated(albumToShare)}
|
||||
/>
|
||||
{:else}
|
||||
@@ -450,7 +452,7 @@
|
||||
album={albumToShare}
|
||||
on:select={({ detail: users }) => handleAddUsers(users)}
|
||||
on:share={() => (showShareByURLModal = true)}
|
||||
on:close={() => closeShareModal()}
|
||||
onClose={() => closeShareModal()}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -458,7 +460,8 @@
|
||||
<!-- Delete Modal -->
|
||||
{#if albumToDelete}
|
||||
<ConfirmDialogue
|
||||
title="Delete Album"
|
||||
id="delete-album-dialogue-modal"
|
||||
title="Delete album"
|
||||
confirmText="Delete"
|
||||
onConfirm={deleteSelectedAlbum}
|
||||
onClose={() => (albumToDelete = null)}
|
||||
|
||||
@@ -5,18 +5,18 @@
|
||||
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;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
remove: string;
|
||||
close: void;
|
||||
}>();
|
||||
|
||||
let currentUser: UserResponseDto;
|
||||
@@ -66,7 +66,7 @@
|
||||
</script>
|
||||
|
||||
{#if !selectedRemoveUser}
|
||||
<BaseModal id="share-info-modal" title="Options" on:close>
|
||||
<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,12 +116,13 @@
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</BaseModal>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
{#if selectedRemoveUser && selectedRemoveUser?.id === currentUser?.id}
|
||||
<ConfirmDialogue
|
||||
title="Leave Album?"
|
||||
id="leave-album-modal"
|
||||
title="Leave album?"
|
||||
prompt="Are you sure you want to leave {album.albumName}?"
|
||||
confirmText="Leave"
|
||||
onConfirm={handleRemoveUser}
|
||||
@@ -131,8 +132,9 @@
|
||||
|
||||
{#if selectedRemoveUser && selectedRemoveUser?.id !== currentUser?.id}
|
||||
<ConfirmDialogue
|
||||
title="Remove User?"
|
||||
prompt="Are you sure you want to remove {selectedRemoveUser.name}"
|
||||
id="remove-user-modal"
|
||||
title="Remove user?"
|
||||
prompt="Are you sure you want to remove {selectedRemoveUser.name}?"
|
||||
confirmText="Remove"
|
||||
onConfirm={handleRemoveUser}
|
||||
onClose={() => (selectedRemoveUser = null)}
|
||||
|
||||
@@ -12,17 +12,17 @@
|
||||
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;
|
||||
let users: UserResponseDto[] = [];
|
||||
let selectedUsers: UserResponseDto[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
select: UserResponseDto[];
|
||||
share: void;
|
||||
close: void;
|
||||
}>();
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
onMount(async () => {
|
||||
@@ -54,7 +54,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal id="user-selection-modal" title="Invite to album" showLogo on:close>
|
||||
<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}
|
||||
@@ -756,7 +756,7 @@
|
||||
shared={addToSharedAlbum}
|
||||
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
|
||||
on:album={({ detail }) => handleAddToAlbum(detail)}
|
||||
on:close={() => (isShowAlbumPicker = false)}
|
||||
onClose={() => (isShowAlbumPicker = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -770,11 +770,11 @@
|
||||
{/if}
|
||||
|
||||
{#if isShowProfileImageCrop}
|
||||
<ProfileImageCropper {asset} on:close={() => (isShowProfileImageCrop = false)} />
|
||||
<ProfileImageCropper {asset} onClose={() => (isShowProfileImageCrop = false)} />
|
||||
{/if}
|
||||
|
||||
{#if isShowShareModal}
|
||||
<CreateSharedLinkModal assetIds={[asset.id]} on:close={() => (isShowShareModal = false)} />
|
||||
<CreateSharedLinkModal assetIds={[asset.id]} onClose={() => (isShowShareModal = false)} />
|
||||
{/if}
|
||||
</section>
|
||||
</FocusTrap>
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
viewer = new Viewer({
|
||||
container,
|
||||
panorama,
|
||||
touchmoveTwoFingers: true,
|
||||
mousewheelCtrlKey: false,
|
||||
navbar: false,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
class:rounded-xl={curve}
|
||||
muted
|
||||
autoplay
|
||||
loop
|
||||
src={url}
|
||||
on:play={() => {
|
||||
loading = false;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -161,6 +161,7 @@
|
||||
|
||||
{#if isShowConfirmation}
|
||||
<ConfirmDialogue
|
||||
id="merge-people-modal"
|
||||
title="Merge people"
|
||||
confirmText="Merge"
|
||||
onConfirm={handleMerge}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||
import { type PersonResponseDto } from '@immich/sdk';
|
||||
import { mdiArrowLeft, mdiClose, mdiMerge } from '@mdi/js';
|
||||
import { mdiArrowLeft, mdiMerge } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
@@ -30,95 +30,80 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={() => dispatch('close')}>
|
||||
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
|
||||
<div
|
||||
class="w-[250px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[375px]"
|
||||
>
|
||||
<div class="relative flex items-center justify-between">
|
||||
<h1 class="truncate px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Merge People - {title}
|
||||
</h1>
|
||||
<div class="p-2">
|
||||
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
|
||||
<FullScreenModal id="merge-people-modal" title="Merge people - {title}" onClose={() => dispatch('close')}>
|
||||
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
|
||||
{#if !choosePersonToMerge}
|
||||
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(personMerge1.id)}
|
||||
altText={personMerge1.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="mx-0.5 flex md:mx-2">
|
||||
<CircleIconButton
|
||||
title="Swap merge direction"
|
||||
icon={mdiMerge}
|
||||
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={potentialMergePeople.length === 0}
|
||||
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
|
||||
on:click={() => {
|
||||
if (potentialMergePeople.length > 0) {
|
||||
choosePersonToMerge = !choosePersonToMerge;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ImageThumbnail
|
||||
border={potentialMergePeople.length > 0}
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(personMerge2.id)}
|
||||
altText={personMerge2.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="grid w-full grid-cols-1 gap-2">
|
||||
<div class="px-2">
|
||||
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
|
||||
{#each potentialMergePeople as person (person.id)}
|
||||
<div class="h-24 w-24 md:h-28 md:w-28">
|
||||
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
|
||||
<ImageThumbnail
|
||||
border={true}
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
on:click={() => changePersonToMerge(person)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-center px-2 py-4 md:h-36 md:px-4 md:py-4">
|
||||
{#if !choosePersonToMerge}
|
||||
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
|
||||
<ImageThumbnail
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(personMerge1.id)}
|
||||
altText={personMerge1.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="mx-0.5 flex md:mx-2">
|
||||
<CircleIconButton
|
||||
title="Swap merge direction"
|
||||
icon={mdiMerge}
|
||||
on:click={() => ([personMerge1, personMerge2] = [personMerge2, personMerge1])}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
disabled={potentialMergePeople.length === 0}
|
||||
class="flex h-28 w-28 items-center rounded-full border-2 border-immich-primary px-1 dark:border-immich-dark-primary md:h-32 md:w-32 md:px-2"
|
||||
on:click={() => {
|
||||
if (potentialMergePeople.length > 0) {
|
||||
choosePersonToMerge = !choosePersonToMerge;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ImageThumbnail
|
||||
border={potentialMergePeople.length > 0}
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(personMerge2.id)}
|
||||
altText={personMerge2.name}
|
||||
widthStyle="100%"
|
||||
/>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="grid w-full grid-cols-1 gap-2">
|
||||
<div class="px-2">
|
||||
<button on:click={() => (choosePersonToMerge = false)}> <Icon path={mdiArrowLeft} /></button>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<div class="flex flex-wrap justify-center md:grid md:grid-cols-{potentialMergePeople.length}">
|
||||
{#each potentialMergePeople as person (person.id)}
|
||||
<div class="h-24 w-24 md:h-28 md:w-28">
|
||||
<button class="p-2 w-full" on:click={() => changePersonToMerge(person)}>
|
||||
<ImageThumbnail
|
||||
border={true}
|
||||
circle
|
||||
shadow
|
||||
url={getPeopleThumbnailUrl(person.id)}
|
||||
altText={person.name}
|
||||
widthStyle="100%"
|
||||
on:click={() => changePersonToMerge(person)}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex px-4 md:px-8 md:pt-4">
|
||||
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
|
||||
</div>
|
||||
<div class="flex px-4 pt-2 md:px-8">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
|
||||
</div>
|
||||
<div class="mt-8 flex w-full gap-4 px-4 pb-4">
|
||||
<Button fullwidth color="gray" on:click={() => dispatch('reject')}>No</Button>
|
||||
<Button fullwidth on:click={() => dispatch('confirm', [personMerge1, personMerge2])}>Yes</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex px-4 md:pt-4">
|
||||
<h1 class="text-xl text-gray-500 dark:text-gray-300">Are these the same person?</h1>
|
||||
</div>
|
||||
<div class="flex px-4 pt-2">
|
||||
<p class="text-sm text-gray-500 dark:text-gray-300">They will be merged together</p>
|
||||
</div>
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import { mdiCake } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import DateInput from '../elements/date-input.svelte';
|
||||
|
||||
export let birthDate: string;
|
||||
@@ -21,36 +20,27 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={handleCancel}>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiCake} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Set date of birth</h1>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Date of birth is used to calculate the age of this person at the time of a photo.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<DateInput
|
||||
class="immich-form-input"
|
||||
id="birthDate"
|
||||
name="birthDate"
|
||||
type="date"
|
||||
bind:value={birthDate}
|
||||
max={todayFormatted}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
||||
<Button type="submit" fullwidth>Set</Button>
|
||||
</div>
|
||||
</form>
|
||||
<FullScreenModal id="set-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" id="set-birth-date-form">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<DateInput
|
||||
class="immich-form-input"
|
||||
id="birthDate"
|
||||
name="birthDate"
|
||||
type="date"
|
||||
bind:value={birthDate}
|
||||
max={todayFormatted}
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { ApiKeyResponseDto } from '@immich/sdk';
|
||||
import { mdiKeyVariant } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
@@ -8,7 +7,7 @@
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
|
||||
export let apiKey: Partial<ApiKeyResponseDto>;
|
||||
export let title = 'API Key';
|
||||
export let title: string;
|
||||
export let cancelText = 'Cancel';
|
||||
export let submitText = 'Save';
|
||||
|
||||
@@ -29,29 +28,15 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={handleCancel}>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiKeyVariant} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{title}
|
||||
</h1>
|
||||
<FullScreenModal id="api-key-modal" {title} icon={mdiKeyVariant} onClose={handleCancel}>
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off" 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>
|
||||
|
||||
<form on:submit|preventDefault={handleSubmit} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">Name</label>
|
||||
<input class="immich-form-input" id="name" name="name" type="text" bind:value={apiKey.name} />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
<Button color="gray" fullwidth on:click={handleCancel}>{cancelText}</Button>
|
||||
<Button type="submit" fullwidth>{submitText}</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { mdiKeyVariant } from '@mdi/js';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
@@ -20,31 +19,22 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<FullScreenModal>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiKeyVariant} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">API Key</h1>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
This value will only be shown once. Please be sure to copy it before closing the window.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
|
||||
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
{#if canCopyImagesToClipboard}
|
||||
<Button on:click={() => copyToClipboard(secret)} fullwidth>Copy to Clipboard</Button>
|
||||
{/if}
|
||||
<Button on:click={() => handleDone()} fullwidth>Done</Button>
|
||||
</div>
|
||||
<FullScreenModal id="api-key-secret-modal" title="API key" icon={mdiKeyVariant} onClose={() => handleDone()}>
|
||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
This value will only be shown once. Please be sure to copy it before closing the window.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<!-- <label class="immich-form-label" for="secret">API Key</label> -->
|
||||
<textarea class="immich-form-input" id="secret" name="secret" readonly={true} value={secret} />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
import { createUser } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
import PasswordField from '../shared-components/password-field.svelte';
|
||||
import Slider from '../elements/slider.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
|
||||
export let onClose: () => void;
|
||||
|
||||
let error: string;
|
||||
let success: string;
|
||||
@@ -69,43 +71,36 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="max-h-screen w-[500px] max-w-[95vw] overflow-y-auto immich-scrollbar rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="flex flex-col place-content-center place-items-center gap-4 px-4">
|
||||
<ImmichLogo noText class="text-center" height="75" width="75" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Create new user</h1>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={registerUser} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<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="m-4 flex flex-col gap-2">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="password">Password</label>
|
||||
<PasswordField id="password" bind:password autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="confirmPassword">Confirm Password</label>
|
||||
<PasswordField id="confirmPassword" bind:password={confirmPassword} autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex place-items-center justify-between gap-2">
|
||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
|
||||
Require user to change password on first login
|
||||
</label>
|
||||
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">Name</label>
|
||||
<input class="immich-form-input" id="name" bind:value={name} type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<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}
|
||||
@@ -116,15 +111,15 @@
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="ml-4 text-sm text-red-400">{error}</p>
|
||||
<p class="text-sm text-red-400">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<p class="ml-4 text-sm text-immich-primary">{success}</p>
|
||||
<p class="text-sm text-immich-primary">{success}</p>
|
||||
{/if}
|
||||
<div class="flex w-full gap-4 p-4">
|
||||
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
|
||||
<Button type="submit" disabled={isCreatingUser} fullwidth>Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button color="gray" fullwidth on:click={() => dispatch('cancel')}>Cancel</Button>
|
||||
<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,16 +36,8 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="max-h-screen w-[700px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 mb-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit Album</h1>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={handleUpdateAlbumInfo} autocomplete="off">
|
||||
<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" />
|
||||
@@ -61,12 +55,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="mt-8 flex w-full sm:w-2/3 gap-4 px-4">
|
||||
<Button color="gray" fullwidth on:click={() => onCancel?.()}>Cancel</Button>
|
||||
<Button type="submit" fullwidth disabled={isSubmitting}>OK</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { convertFromBytes, convertToBytes } from '$lib/utils/byte-converter';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { updateUser, type UserResponseDto } from '@immich/sdk';
|
||||
import { mdiAccountEditOutline, mdiClose } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
|
||||
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;
|
||||
@@ -91,82 +90,68 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusTrap>
|
||||
<div
|
||||
class="relative max-h-screen w-[500px] max-w-[95vw] overflow-y-auto rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div class="absolute top-0 right-0 px-2 py-2 h-fit">
|
||||
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
|
||||
<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="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiAccountEditOutline} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Edit user</h1>
|
||||
<div 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>
|
||||
|
||||
<form on:submit|preventDefault={editUser} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="email">Email</label>
|
||||
<input class="immich-form-input" id="email" name="email" type="email" bind:value={user.email} />
|
||||
</div>
|
||||
<div class="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="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">Name</label>
|
||||
<input class="immich-form-input" id="name" name="name" type="text" required bind:value={user.name} />
|
||||
</div>
|
||||
<div class="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="m-4 flex flex-col gap-2">
|
||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
|
||||
>Quota Size (GiB) {#if quotaSizeWarning}
|
||||
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
|
||||
{/if}</label
|
||||
<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
|
||||
>
|
||||
<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>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="storage-label">Storage Label</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="storage-label"
|
||||
name="storage-label"
|
||||
type="text"
|
||||
bind:value={user.storageLabel}
|
||||
/>
|
||||
{#if error}
|
||||
<p class="ml-4 text-sm text-red-400">{error}</p>
|
||||
{/if}
|
||||
|
||||
<p>
|
||||
Note: To apply the Storage Label to previously uploaded assets, run the
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
|
||||
Storage Migration Job</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="ml-4 text-sm text-red-400">{error}</p>
|
||||
{/if}
|
||||
|
||||
{#if success}
|
||||
<p class="ml-4 text-sm text-immich-primary">{success}</p>
|
||||
{/if}
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
{#if canResetPassword}
|
||||
<Button color="light-red" fullwidth on:click={() => (isShowResetPasswordConfirmation = true)}
|
||||
>Reset password</Button
|
||||
>
|
||||
{/if}
|
||||
<Button type="submit" fullwidth>Confirm</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</FocusTrap>
|
||||
{#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 form="edit-user-form">Confirm</Button>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
||||
{#if isShowResetPasswordConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Reset Password"
|
||||
id="reset-password-modal"
|
||||
title="Reset password"
|
||||
confirmText="Reset"
|
||||
onConfirm={resetPassword}
|
||||
onClose={() => (isShowResetPasswordConfirmation = false)}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiFolderRemove } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -18,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;
|
||||
@@ -29,48 +28,41 @@
|
||||
const handleSubmit = () => dispatch('submit', { excludePattern: exclusionPattern });
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={handleCancel}>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiFolderRemove} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Add Exclusion pattern</h1>
|
||||
<FullScreenModal
|
||||
id="add-exclusion-pattern-modal"
|
||||
title="Add exclusion pattern"
|
||||
icon={mdiFolderRemove}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" 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.
|
||||
<br /><br />
|
||||
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
|
||||
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore".
|
||||
</p>
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="exclusionPattern"
|
||||
name="exclusionPattern"
|
||||
type="text"
|
||||
bind:value={exclusionPattern}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||
<p class="p-5 text-sm">
|
||||
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
|
||||
folders that contain files you don't want to import, such as RAW files.
|
||||
<br /><br />
|
||||
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
|
||||
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore".
|
||||
</p>
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="exclusionPattern">Pattern</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="exclusionPattern"
|
||||
name="exclusionPattern"
|
||||
type="text"
|
||||
bind:value={exclusionPattern}
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
||||
{#if isEditing}
|
||||
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
|
||||
{/if}
|
||||
|
||||
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
|
||||
</div>
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
{#if isDuplicate}
|
||||
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import { mdiFolderSync } from '@mdi/js';
|
||||
@@ -31,45 +30,28 @@
|
||||
const handleSubmit = () => dispatch('submit', { importPath });
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={handleCancel}>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiFolderSync} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{title}
|
||||
</h1>
|
||||
<FullScreenModal id="library-import-path-modal" {title} icon={mdiFolderSync} onClose={handleCancel}>
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" 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>
|
||||
|
||||
<div class="my-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="path">Path</label>
|
||||
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||
<p class="p-5 text-sm">
|
||||
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos. Note that
|
||||
you are only allowed to import paths inside of your account's external path, configured in the administrative
|
||||
settings.
|
||||
</p>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="path">Path</label>
|
||||
<input class="immich-form-input" id="path" name="path" type="text" bind:value={importPath} />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>{cancelText}</Button>
|
||||
{#if isEditing}
|
||||
<Button color="red" fullwidth on:click={() => dispatch('delete')}>Delete</Button>
|
||||
{/if}
|
||||
|
||||
<Button type="submit" disabled={!canSubmit} fullwidth>{submitText}</Button>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
{#if isDuplicate}
|
||||
<p class="text-red-500 text-sm">This import path already exists.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="mt-8 flex w-full gap-4">
|
||||
{#if isDuplicate}
|
||||
<p class="text-red-500 text-sm">This import path already exists.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<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>
|
||||
|
||||
@@ -152,7 +152,7 @@
|
||||
|
||||
{#if addImportPath}
|
||||
<LibraryImportPathForm
|
||||
title="Add Import Path"
|
||||
title="Add import path"
|
||||
submitText="Add"
|
||||
bind:importPath={importPathToAdd}
|
||||
{importPaths}
|
||||
@@ -166,7 +166,7 @@
|
||||
|
||||
{#if editImportPath != undefined}
|
||||
<LibraryImportPathForm
|
||||
title="Edit Import Path"
|
||||
title="Edit import path"
|
||||
submitText="Save"
|
||||
isEditing={true}
|
||||
bind:importPath={editedImportPath}
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
addExclusionPattern = true;
|
||||
}}>Add Exclusion Pattern</Button
|
||||
}}>Add exclusion pattern</Button
|
||||
></td
|
||||
></tr
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
|
||||
import { mdiFolderSync } from '@mdi/js';
|
||||
@@ -28,27 +27,19 @@
|
||||
const handleSubmit = () => dispatch('submit', { ownerId });
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={handleCancel}>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<Icon path={mdiFolderSync} size="4em" />
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Select library owner</h1>
|
||||
</div>
|
||||
<FullScreenModal
|
||||
id="select-library-owner-modal"
|
||||
title="Select library owner"
|
||||
icon={mdiFolderSync}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
<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>
|
||||
|
||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off">
|
||||
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
|
||||
|
||||
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
||||
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
<Button color="gray" fullwidth on:click={() => handleCancel()}>Cancel</Button>
|
||||
|
||||
<Button type="submit" fullwidth>Create</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
||||
</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>
|
||||
|
||||
@@ -21,98 +21,92 @@
|
||||
const handleClose = () => dispatch('close');
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={handleClose}>
|
||||
<div
|
||||
class="flex w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
<FullScreenModal id="map-settings-modal" title="Map settings" onClose={handleClose}>
|
||||
<form
|
||||
on:submit|preventDefault={() => dispatch('save', settings)}
|
||||
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
id="map-settings-form"
|
||||
>
|
||||
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Map Settings</h1>
|
||||
|
||||
<form
|
||||
on:submit|preventDefault={() => dispatch('save', settings)}
|
||||
class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
|
||||
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
|
||||
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
|
||||
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
|
||||
{#if customDateRange}
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-8">
|
||||
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
|
||||
<DateInput
|
||||
class="immich-form-input w-40"
|
||||
type="date"
|
||||
id="date-after"
|
||||
max={settings.dateBefore}
|
||||
bind:value={settings.dateAfter}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-8">
|
||||
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
|
||||
<DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
|
||||
</div>
|
||||
<div class="flex justify-center text-xs">
|
||||
<LinkButton
|
||||
on:click={() => {
|
||||
customDateRange = false;
|
||||
settings.dateAfter = '';
|
||||
settings.dateBefore = '';
|
||||
}}
|
||||
>
|
||||
Remove custom date range
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
|
||||
<SettingSelect
|
||||
label="Date range"
|
||||
name="date-range"
|
||||
bind:value={settings.relativeDate}
|
||||
options={[
|
||||
{
|
||||
value: '',
|
||||
text: 'All',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ hours: 24 }).toISO() || '',
|
||||
text: 'Past 24 hours',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ days: 7 }).toISO() || '',
|
||||
text: 'Past 7 days',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ days: 30 }).toISO() || '',
|
||||
text: 'Past 30 days',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ years: 1 }).toISO() || '',
|
||||
text: 'Past year',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ years: 3 }).toISO() || '',
|
||||
text: 'Past 3 years',
|
||||
},
|
||||
]}
|
||||
<SettingSwitch id="allow-dark-mode" title="Allow dark mode" bind:checked={settings.allowDarkMode} />
|
||||
<SettingSwitch id="only-favorites" title="Only favorites" bind:checked={settings.onlyFavorites} />
|
||||
<SettingSwitch id="include-archived" title="Include archived" bind:checked={settings.includeArchived} />
|
||||
<SettingSwitch id="include-shared-with-me" title="Include shared with me" bind:checked={settings.withPartners} />
|
||||
{#if customDateRange}
|
||||
<div in:fly={{ y: 10, duration: 200 }} class="flex flex-col gap-4">
|
||||
<div class="flex items-center justify-between gap-8">
|
||||
<label class="immich-form-label shrink-0 text-sm" for="date-after">Date after</label>
|
||||
<DateInput
|
||||
class="immich-form-input w-40"
|
||||
type="date"
|
||||
id="date-after"
|
||||
max={settings.dateBefore}
|
||||
bind:value={settings.dateAfter}
|
||||
/>
|
||||
<div class="text-xs">
|
||||
<LinkButton
|
||||
on:click={() => {
|
||||
customDateRange = true;
|
||||
settings.relativeDate = '';
|
||||
}}
|
||||
>
|
||||
Use custom date range instead
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-4 flex w-full gap-4">
|
||||
<Button color="gray" size="sm" fullwidth on:click={handleClose}>Cancel</Button>
|
||||
<Button type="submit" size="sm" fullwidth>Save</Button>
|
||||
<div class="flex items-center justify-between gap-8">
|
||||
<label class="immich-form-label shrink-0 text-sm" for="date-before">Date before</label>
|
||||
<DateInput class="immich-form-input w-40" type="date" id="date-before" bind:value={settings.dateBefore} />
|
||||
</div>
|
||||
<div class="flex justify-center text-xs">
|
||||
<LinkButton
|
||||
on:click={() => {
|
||||
customDateRange = false;
|
||||
settings.dateAfter = '';
|
||||
settings.dateBefore = '';
|
||||
}}
|
||||
>
|
||||
Remove custom date range
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{:else}
|
||||
<div in:fly={{ y: -10, duration: 200 }} class="flex flex-col gap-1">
|
||||
<SettingSelect
|
||||
label="Date range"
|
||||
name="date-range"
|
||||
bind:value={settings.relativeDate}
|
||||
options={[
|
||||
{
|
||||
value: '',
|
||||
text: 'All',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ hours: 24 }).toISO() || '',
|
||||
text: 'Past 24 hours',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ days: 7 }).toISO() || '',
|
||||
text: 'Past 7 days',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ days: 30 }).toISO() || '',
|
||||
text: 'Past 30 days',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ years: 1 }).toISO() || '',
|
||||
text: 'Past year',
|
||||
},
|
||||
{
|
||||
value: Duration.fromObject({ years: 3 }).toISO() || '',
|
||||
text: 'Past 3 years',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div class="text-xs">
|
||||
<LinkButton
|
||||
on:click={() => {
|
||||
customDateRange = true;
|
||||
settings.relativeDate = '';
|
||||
}}
|
||||
>
|
||||
Use custom date range instead
|
||||
</LinkButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</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>
|
||||
|
||||
@@ -46,6 +46,6 @@
|
||||
{shared}
|
||||
on:newAlbum={({ detail }) => handleAddToNewAlbum(detail)}
|
||||
on:album={({ detail }) => handleAddToAlbum(detail)}
|
||||
on:close={handleHideAlbumPicker}
|
||||
onClose={handleHideAlbumPicker}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
<CircleIconButton title="Share" icon={mdiShareVariantOutline} on:click={() => (showModal = true)} />
|
||||
|
||||
{#if showModal}
|
||||
<CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} on:close={() => (showModal = false)} />
|
||||
<CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />
|
||||
{/if}
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
|
||||
{#if isShowConfirmation}
|
||||
<ConfirmDialogue
|
||||
id="remove-from-album-modal"
|
||||
title="Remove from {album.albumName}"
|
||||
confirmText="Remove"
|
||||
onConfirm={removeFromAlbum}
|
||||
|
||||
@@ -50,7 +50,8 @@
|
||||
|
||||
{#if removing}
|
||||
<ConfirmDialogue
|
||||
title="Remove Assets?"
|
||||
id="remove-assets-modal"
|
||||
title="Remove assets?"
|
||||
prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
|
||||
confirmText="Remove"
|
||||
onConfirm={() => handleRemove()}
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue
|
||||
title="Permanently Delete Asset{size > 1 ? 's' : ''}"
|
||||
id="permanently-delete-asset-modal"
|
||||
title="Permanently delete asset{size > 1 ? 's' : ''}"
|
||||
confirmText="Delete"
|
||||
onConfirm={handleConfirm}
|
||||
onClose={() => dispatch('cancel')}
|
||||
|
||||
@@ -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[] = [];
|
||||
@@ -16,10 +16,10 @@
|
||||
const dispatch = createEventDispatcher<{
|
||||
newAlbum: string;
|
||||
album: AlbumResponseDto;
|
||||
close: void;
|
||||
}>();
|
||||
|
||||
export let shared: boolean;
|
||||
export let onClose: () => void;
|
||||
|
||||
onMount(async () => {
|
||||
albums = await getAllAlbums({ shared: shared || undefined });
|
||||
@@ -52,7 +52,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal id="album-selection-modal" title={getTitle()} on:close>
|
||||
<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,93 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { createEventDispatcher, onMount, onDestroy } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { clickOutside } from '$lib/utils/click-outside';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
}>();
|
||||
/**
|
||||
* Unique identifier for the modal.
|
||||
*/
|
||||
export let id: string;
|
||||
export let title: string;
|
||||
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;
|
||||
|
||||
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={`${id}-title`}
|
||||
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: () => dispatch('close'),
|
||||
onEscape: () => dispatch('close'),
|
||||
}}
|
||||
class="max-h-[800px] min-h-[200px] w-[450px] overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="flex place-items-center justify-between px-5 py-3">
|
||||
<div class="flex gap-2 place-items-center">
|
||||
{#if showLogo}
|
||||
<ImmichLogo noText={true} width={32} />
|
||||
{:else if icon}
|
||||
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
|
||||
{/if}
|
||||
<h1 id={`${id}-title`}>
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<CircleIconButton on:click={() => dispatch('close')} icon={mdiClose} size={'20'} title="Close" />
|
||||
</div>
|
||||
|
||||
<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>
|
||||
@@ -63,16 +63,15 @@
|
||||
|
||||
<div role="presentation" on:keydown={handleKeydown}>
|
||||
<ConfirmDialogue
|
||||
id="edit-date-time-modal"
|
||||
confirmColor="primary"
|
||||
cancelColor="secondary"
|
||||
title="Edit date & time"
|
||||
title="Edit date and time"
|
||||
prompt="Please select a new date:"
|
||||
disabled={!date.isValid}
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
<div class="flex flex-col text-md px-4 text-center gap-2" slot="prompt">
|
||||
<div class="mt-2" />
|
||||
<div class="flex flex-col">
|
||||
<label for="datetime">Date and Time</label>
|
||||
<DateInput
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
import SearchBar from '../elements/search-bar.svelte';
|
||||
import { listNavigation } from '$lib/utils/list-navigation';
|
||||
|
||||
export const title = 'Change Location';
|
||||
export let asset: AssetResponseDto | undefined = undefined;
|
||||
|
||||
interface Point {
|
||||
@@ -95,10 +94,10 @@
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue
|
||||
id="change-location-modal"
|
||||
confirmColor="primary"
|
||||
cancelColor="secondary"
|
||||
title="Change Location"
|
||||
width={800}
|
||||
title="Change location"
|
||||
width="wide"
|
||||
onConfirm={handleConfirm}
|
||||
onClose={handleCancel}
|
||||
>
|
||||
|
||||
@@ -3,15 +3,16 @@
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import type { Color } from '$lib/components/elements/buttons/button.svelte';
|
||||
|
||||
export let id: string;
|
||||
export let title = 'Confirm';
|
||||
export let prompt = 'Are you sure you want to do this?';
|
||||
export let confirmText = 'Confirm';
|
||||
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 = 500;
|
||||
export let width: 'wide' | 'narrow' = 'narrow';
|
||||
export let onClose: () => void;
|
||||
export let onConfirm: () => void;
|
||||
|
||||
@@ -23,35 +24,21 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal {onClose}>
|
||||
<div
|
||||
class="max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
style="width: {width}px"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<h1 class="pb-2 text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-md px-4 py-5 text-center">
|
||||
<slot name="prompt">
|
||||
<p>{prompt}</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex w-full gap-4 px-4">
|
||||
{#if !hideCancelButton}
|
||||
<Button color={cancelColor} fullwidth on:click={onClose}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<FullScreenModal {title} {id} {onClose} {width}>
|
||||
<div class="text-md py-5 text-center">
|
||||
<slot name="prompt">
|
||||
<p>{prompt}</p>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
{#if !hideCancelButton}
|
||||
<Button color={cancelColor} fullwidth on:click={onClose}>
|
||||
{cancelText}
|
||||
</Button>
|
||||
{/if}
|
||||
<Button color={confirmColor} fullwidth on:click={handleConfirm} disabled={disabled || isConfirmButtonDisabled}>
|
||||
{confirmText}
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
||||
+19
-21
@@ -8,13 +8,14 @@
|
||||
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;
|
||||
export let assetIds: string[] = [];
|
||||
export let editingLink: SharedLinkResponseDto | undefined = undefined;
|
||||
@@ -30,14 +31,12 @@
|
||||
let enablePassword = false;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
escape: void;
|
||||
created: void;
|
||||
}>();
|
||||
|
||||
const expiredDateOption: ImmichDropDownOption = {
|
||||
default: 'Never',
|
||||
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days'],
|
||||
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days', '3 months', '1 year'],
|
||||
};
|
||||
|
||||
$: shareType = albumId ? SharedLinkType.Album : SharedLinkType.Individual;
|
||||
@@ -105,6 +104,12 @@
|
||||
case '30 days': {
|
||||
return 30 * 24 * 60 * 60 * 1000;
|
||||
}
|
||||
case '3 months': {
|
||||
return 30 * 24 * 60 * 60 * 3 * 1000;
|
||||
}
|
||||
case '1 year': {
|
||||
return 30 * 24 * 60 * 60 * 12 * 1000;
|
||||
}
|
||||
default: {
|
||||
return 0;
|
||||
}
|
||||
@@ -140,7 +145,7 @@
|
||||
message: 'Edited',
|
||||
});
|
||||
|
||||
dispatch('close');
|
||||
onClose();
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to edit shared link');
|
||||
}
|
||||
@@ -154,8 +159,8 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal id="create-shared-link-modal" title={getTitle()} icon={mdiLink} on:close>
|
||||
<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>
|
||||
@@ -241,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>
|
||||
|
||||
@@ -1,28 +1,54 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import ImmichLogo from './immich-logo.svelte';
|
||||
export let dropHandler: (event: DragEvent) => void;
|
||||
import { page } from '$app/stores';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
|
||||
$: albumId = ($page.route?.id === '/(user)/albums/[albumId]' || undefined) && $page.params.albumId;
|
||||
$: isShare = $page.route?.id === '/(user)/share/[key]' || undefined;
|
||||
|
||||
let dragStartTarget: EventTarget | null = null;
|
||||
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
const onDragEnter = (e: DragEvent) => {
|
||||
if (e.dataTransfer && e.dataTransfer.types.includes('Files')) {
|
||||
dragStartTarget = e.target;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:body
|
||||
on:dragenter|stopPropagation|preventDefault={handleDragEnter}
|
||||
on:dragleave|stopPropagation|preventDefault={(e) => {
|
||||
const onDragLeave = (e: DragEvent) => {
|
||||
if (dragStartTarget === e.target) {
|
||||
dragStartTarget = null;
|
||||
}
|
||||
}}
|
||||
on:drop|stopPropagation|preventDefault={(e) => {
|
||||
};
|
||||
|
||||
const onDrop = async (e: DragEvent) => {
|
||||
dragStartTarget = null;
|
||||
dropHandler(e);
|
||||
}}
|
||||
await handleFiles(e.dataTransfer?.files);
|
||||
};
|
||||
|
||||
const onPaste = ({ clipboardData }: ClipboardEvent) => handleFiles(clipboardData?.files);
|
||||
|
||||
const handleFiles = async (files?: FileList) => {
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filesArray: File[] = Array.from<File>(files);
|
||||
if (isShare) {
|
||||
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
||||
} else {
|
||||
await fileUploadHandler(filesArray, albumId);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:window on:paste={onPaste} />
|
||||
|
||||
<svelte:body
|
||||
on:dragenter|stopPropagation|preventDefault={onDragEnter}
|
||||
on:dragleave|stopPropagation|preventDefault={onDragLeave}
|
||||
on:drop|stopPropagation|preventDefault={onDrop}
|
||||
/>
|
||||
|
||||
{#if dragStartTarget}
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,45 @@
|
||||
import { clickOutside } from '../../utils/click-outside';
|
||||
import { fade } from 'svelte/transition';
|
||||
import FocusTrap from '$lib/components/shared-components/focus-trap.svelte';
|
||||
import ModalHeader from '$lib/components/shared-components/modal-header.svelte';
|
||||
|
||||
export let onClose: (() => void) | undefined = undefined;
|
||||
export let onClose: () => void;
|
||||
|
||||
/**
|
||||
* Unique identifier for the modal.
|
||||
*/
|
||||
export let id: string;
|
||||
export let title: string;
|
||||
/**
|
||||
* If true, the logo will be displayed next to the modal title.
|
||||
*/
|
||||
export let showLogo = false;
|
||||
/**
|
||||
* Optional icon to display next to the modal title, if `showLogo` is false.
|
||||
*/
|
||||
export let icon: string | undefined = undefined;
|
||||
/**
|
||||
* Sets the width of the modal.
|
||||
*
|
||||
* - `wide`: 750px
|
||||
* - `narrow`: 450px
|
||||
* - `auto`: fits the width of the modal content, up to a maximum of 550px
|
||||
*/
|
||||
export let width: 'wide' | 'narrow' | 'auto' = 'narrow';
|
||||
|
||||
$: titleId = `${id}-title`;
|
||||
$: isStickyBottom = !!$$slots['sticky-bottom'];
|
||||
|
||||
let modalWidth: string;
|
||||
$: {
|
||||
if (width === 'wide') {
|
||||
modalWidth = 'w-[750px]';
|
||||
} else if (width === 'narrow') {
|
||||
modalWidth = 'w-[450px]';
|
||||
} else {
|
||||
modalWidth = 'sm:max-w-[550px]';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<FocusTrap>
|
||||
@@ -12,8 +49,27 @@
|
||||
out:fade={{ duration: 100 }}
|
||||
class="fixed left-0 top-0 z-[9990] flex h-screen w-screen place-content-center place-items-center bg-black/40"
|
||||
>
|
||||
<div class="z-[9999]" use:clickOutside={{ onOutclick: onClose, onEscape: onClose }} tabindex="-1">
|
||||
<slot />
|
||||
<div
|
||||
class="z-[9999] max-w-[95vw] max-h-[95vh] {modalWidth} overflow-y-auto rounded-3xl bg-immich-bg shadow-md dark:bg-immich-dark-gray dark:text-immich-dark-fg immich-scrollbar"
|
||||
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>
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
|
||||
/**
|
||||
* Unique identifier for the header text.
|
||||
*/
|
||||
export let id: string;
|
||||
export let title: string;
|
||||
export let onClose: () => void;
|
||||
/**
|
||||
* If true, the logo will be displayed next to the modal title.
|
||||
*/
|
||||
export let showLogo = false;
|
||||
/**
|
||||
* Optional icon to display next to the modal title, if `showLogo` is false.
|
||||
*/
|
||||
export let icon: string | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<div class="flex place-items-center justify-between px-5 py-3">
|
||||
<div class="flex gap-2 place-items-center">
|
||||
{#if showLogo}
|
||||
<ImmichLogo noText={true} width={32} />
|
||||
{:else if icon}
|
||||
<Icon path={icon} size={32} ariaHidden={true} class="text-immich-primary dark:text-immich-dark-primary" />
|
||||
{/if}
|
||||
<h1 {id}>
|
||||
{title}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<CircleIconButton on:click={onClose} icon={mdiClose} size={'20'} title="Close" />
|
||||
</div>
|
||||
@@ -1,7 +1,5 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { UserAvatarColor, type UserResponseDto } from '@immich/sdk';
|
||||
import { mdiClose } from '@mdi/js';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FullScreenModal from '../full-screen-modal.svelte';
|
||||
import UserAvatar from '../user-avatar.svelte';
|
||||
@@ -15,28 +13,14 @@
|
||||
const colors: UserAvatarColor[] = Object.values(UserAvatarColor);
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={() => dispatch('close')}>
|
||||
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
|
||||
<div
|
||||
class=" rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg p-4"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<h1 class="px-4 w-full self-center font-medium text-immich-primary dark:text-immich-dark-primary text-sm">
|
||||
SELECT AVATAR COLOR
|
||||
</h1>
|
||||
<div>
|
||||
<CircleIconButton icon={mdiClose} title="Close" on:click={() => dispatch('close')} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center p-4 mt-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{#each colors as color}
|
||||
<button on:click={() => dispatch('choose', color)}>
|
||||
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<FullScreenModal id="avatar-selector-modal" title="Select avatar color" width="auto" onClose={() => dispatch('close')}>
|
||||
<div class="flex items-center justify-center mt-4">
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
{#each colors as color}
|
||||
<button on:click={() => dispatch('choose', color)}>
|
||||
<UserAvatar label={color} {user} {color} size="xl" showProfileImage={false} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { createProfileImage, type AssetResponseDto } from '@immich/sdk';
|
||||
import domtoimage from 'dom-to-image';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
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;
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
close: void;
|
||||
}>();
|
||||
let imgElement: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
@@ -67,22 +65,19 @@
|
||||
} catch (error) {
|
||||
handleError(error, 'Error setting profile picture.');
|
||||
}
|
||||
dispatch('close');
|
||||
onClose();
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal id="profile-image-cropper" title="Set profile picture" on:close>
|
||||
<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
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
|
||||
import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { shortcut } from '$lib/utils/shortcut';
|
||||
import { shortcuts } from '$lib/utils/shortcut';
|
||||
import { focusOutside } from '$lib/utils/focus-outside';
|
||||
|
||||
export let value = '';
|
||||
@@ -87,12 +87,11 @@
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
use:shortcut={{
|
||||
shortcut: { key: 'Escape' },
|
||||
onShortcut: () => {
|
||||
onFocusOut();
|
||||
},
|
||||
}}
|
||||
use:shortcuts={[
|
||||
{ shortcut: { key: 'Escape' }, onShortcut: onFocusOut },
|
||||
{ shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input.focus() },
|
||||
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="w-full relative" use:clickOutside={{ onOutclick: onFocusOut }} use:focusOutside={{ onFocusOut }}>
|
||||
@@ -130,12 +129,10 @@
|
||||
on:click={onFocusIn}
|
||||
on:focus={onFocusIn}
|
||||
disabled={showFilter}
|
||||
use:shortcut={{
|
||||
shortcut: { key: 'Escape' },
|
||||
onShortcut: () => {
|
||||
onFocusOut();
|
||||
},
|
||||
}}
|
||||
use:shortcuts={[
|
||||
{ shortcut: { key: 'Escape' }, onShortcut: onFocusOut },
|
||||
{ shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="absolute inset-y-0 {showClearIcon ? 'right-14' : 'right-5'} flex items-center pl-6 transition-all">
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FullScreenModal from './full-screen-modal.svelte';
|
||||
import { mdiClose, mdiInformationOutline } from '@mdi/js';
|
||||
import { mdiInformationOutline } from '@mdi/js';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
|
||||
interface Shortcuts {
|
||||
@@ -20,7 +19,8 @@
|
||||
general: [
|
||||
{ key: ['←', '→'], action: 'Previous or next photo' },
|
||||
{ key: ['Esc'], action: 'Back, close, or deselect' },
|
||||
{ key: ['/'], action: 'Search your photos' },
|
||||
{ key: ['Ctrl', 'k'], action: 'Search your photos' },
|
||||
{ key: ['Ctrl', '⇧', 'k'], action: 'Open the search filters' },
|
||||
],
|
||||
actions: [
|
||||
{ key: ['f'], action: 'Favorite or unfavorite photo' },
|
||||
@@ -37,63 +37,51 @@
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<FullScreenModal onClose={() => dispatch('close')}>
|
||||
<div class="flex h-full w-full place-content-center place-items-center overflow-hidden">
|
||||
<div
|
||||
class="w-[400px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[650px]"
|
||||
>
|
||||
<div class="relative px-4 pt-4">
|
||||
<h1 class="px-4 py-4 font-medium text-immich-primary dark:text-immich-dark-primary">Keyboard Shortcuts</h1>
|
||||
<div class="absolute inset-y-0 right-0 px-4 py-4">
|
||||
<CircleIconButton title="Close" icon={mdiClose} on:click={() => dispatch('close')} />
|
||||
</div>
|
||||
<FullScreenModal
|
||||
id="keyboard-shortcuts-modal"
|
||||
title="Keyboard shortcuts"
|
||||
width="auto"
|
||||
onClose={() => dispatch('close')}
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
|
||||
<div class="p-4">
|
||||
<h2>General</h2>
|
||||
<div class="text-sm">
|
||||
{#each shortcuts.general as shortcut}
|
||||
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
|
||||
<div class="flex justify-self-end">
|
||||
{#each shortcut.key as key}
|
||||
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
|
||||
{key}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 px-4 pb-4 md:grid-cols-2">
|
||||
<div class="px-4 py-4">
|
||||
<h2>General</h2>
|
||||
<div class="text-sm">
|
||||
{#each shortcuts.general as shortcut}
|
||||
<div class="grid grid-cols-[20%_80%] items-center gap-4 pt-4 text-sm">
|
||||
<div class="flex justify-self-end">
|
||||
{#each shortcut.key as key}
|
||||
<p
|
||||
class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"
|
||||
>
|
||||
{key}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="p-4">
|
||||
<h2>Actions</h2>
|
||||
<div class="text-sm">
|
||||
{#each shortcuts.actions as shortcut}
|
||||
<div class="grid grid-cols-[30%_70%] items-center gap-4 pt-4 text-sm">
|
||||
<div class="flex justify-self-end">
|
||||
{#each shortcut.key as key}
|
||||
<p class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2">
|
||||
{key}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
|
||||
{#if shortcut.info}
|
||||
<Icon path={mdiInformationOutline} title={shortcut.info} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="px-4 py-4">
|
||||
<h2>Actions</h2>
|
||||
<div class="text-sm">
|
||||
{#each shortcuts.actions as shortcut}
|
||||
<div class="grid grid-cols-[20%_80%] items-center gap-4 pt-4 text-sm">
|
||||
<div class="flex justify-self-end">
|
||||
{#each shortcut.key as key}
|
||||
<p
|
||||
class="mr-1 flex items-center justify-center justify-self-end rounded-lg bg-immich-primary/25 p-2"
|
||||
>
|
||||
{key}
|
||||
</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="mb-1 mt-1 flex">{shortcut.action}</p>
|
||||
{#if shortcut.info}
|
||||
<Icon path={mdiInformationOutline} title={shortcut.info} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,34 +33,28 @@
|
||||
</script>
|
||||
|
||||
{#if showModal}
|
||||
<FullScreenModal onClose={() => (showModal = false)}>
|
||||
<div
|
||||
class="max-w-lg rounded-3xl border bg-immich-bg px-8 py-10 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<p class="mb-4 text-2xl">🎉 NEW VERSION AVAILABLE 🎉</p>
|
||||
|
||||
<div>
|
||||
Hi friend, there is a new version of the application please take your time to visit the
|
||||
<span class="font-medium underline"
|
||||
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
|
||||
>release notes</a
|
||||
></span
|
||||
>
|
||||
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
|
||||
especially if you use WatchTower or any mechanism that handles updating your application automatically.
|
||||
</div>
|
||||
|
||||
<div class="mt-4 font-medium">Your friend, Alex</div>
|
||||
|
||||
<div class="font-sm mt-8">
|
||||
<code>Server Version: {serverVersion}</code>
|
||||
<br />
|
||||
<code>Latest Version: {releaseVersion}</code>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 text-right">
|
||||
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
|
||||
</div>
|
||||
<FullScreenModal id="new-version-modal" title="🎉 NEW VERSION AVAILABLE" onClose={() => (showModal = false)}>
|
||||
<div>
|
||||
Hi friend, there is a new version of the application please take your time to visit the
|
||||
<span class="font-medium underline"
|
||||
><a href="https://github.com/immich-app/immich/releases/latest" target="_blank" rel="noopener noreferrer"
|
||||
>release notes</a
|
||||
></span
|
||||
>
|
||||
and ensure your <code>docker-compose</code>, and <code>.env</code> setup is up-to-date to prevent any misconfigurations,
|
||||
especially if you use WatchTower or any mechanism that handles updating your application automatically.
|
||||
</div>
|
||||
|
||||
<div class="mt-4 font-medium">Your friend, Alex</div>
|
||||
|
||||
<div class="font-sm mt-8">
|
||||
<code>Server Version: {serverVersion}</code>
|
||||
<br />
|
||||
<code>Latest Version: {releaseVersion}</code>
|
||||
</div>
|
||||
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button fullwidth on:click={onAcknowledge}>Acknowledge</Button>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
{/if}
|
||||
|
||||
@@ -30,31 +30,24 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<FullScreenModal {onClose}>
|
||||
<div
|
||||
class="flex w-full md:w-96 max-w-lg flex-col gap-8 rounded-3xl border bg-white p-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
<h1 class="self-center text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
Slideshow Settings
|
||||
</h1>
|
||||
|
||||
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<SettingDropdown
|
||||
title="Direction"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[$slideshowNavigation]}
|
||||
onToggle={(option) => handleToggle(option)}
|
||||
/>
|
||||
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Duration"
|
||||
desc="Number of seconds to display each image"
|
||||
min={1}
|
||||
bind:value={$slideshowDelay}
|
||||
/>
|
||||
|
||||
<Button class="w-full" color="gray" on:click={onClose}>Done</Button>
|
||||
</div>
|
||||
<FullScreenModal id="slideshow-settings-modal" title="Slideshow settings" {onClose}>
|
||||
<div class="flex flex-col gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<SettingDropdown
|
||||
title="Direction"
|
||||
options={Object.values(options)}
|
||||
selectedOption={options[$slideshowNavigation]}
|
||||
onToggle={(option) => handleToggle(option)}
|
||||
/>
|
||||
<SettingSwitch id="show-progress-bar" title="Show Progress Bar" bind:checked={$showProgressBar} />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Duration"
|
||||
desc="Number of seconds to display each image"
|
||||
min={1}
|
||||
bind:value={$slideshowDelay}
|
||||
/>
|
||||
</div>
|
||||
<svelte:fragment slot="sticky-bottom">
|
||||
<Button fullwidth color="primary" on:click={onClose}>Done</Button>
|
||||
</svelte:fragment>
|
||||
</FullScreenModal>
|
||||
|
||||
@@ -49,6 +49,7 @@
|
||||
|
||||
{#if deleteDevice}
|
||||
<ConfirmDialogue
|
||||
id="log-out-device-modal"
|
||||
prompt="Are you sure you want to log out this device?"
|
||||
onConfirm={() => handleDelete()}
|
||||
onClose={() => (deleteDevice = null)}
|
||||
@@ -57,6 +58,7 @@
|
||||
|
||||
{#if deleteAll}
|
||||
<ConfirmDialogue
|
||||
id="log-out-all-modal"
|
||||
prompt="Are you sure you want to log out all devices?"
|
||||
onConfirm={() => handleDeleteAll()}
|
||||
onClose={() => (deleteAll = false)}
|
||||
|
||||
@@ -2,15 +2,16 @@
|
||||
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;
|
||||
|
||||
let availableUsers: UserResponseDto[] = [];
|
||||
let selectedUsers: UserResponseDto[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher<{ close: void; 'add-users': UserResponseDto[] }>();
|
||||
const dispatch = createEventDispatcher<{ 'add-users': UserResponseDto[] }>();
|
||||
|
||||
onMount(async () => {
|
||||
// TODO: update endpoint to have a query param for deleted users
|
||||
@@ -32,13 +33,13 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal id="partner-selection-modal" title="Add partner" showLogo on:close>
|
||||
<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
|
||||
@@ -60,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>
|
||||
|
||||
@@ -182,13 +182,14 @@
|
||||
{#if createPartnerFlag}
|
||||
<PartnerSelectionModal
|
||||
{user}
|
||||
on:close={() => (createPartnerFlag = false)}
|
||||
onClose={() => (createPartnerFlag = false)}
|
||||
on:add-users={(event) => handleCreatePartners(event.detail)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if removePartnerDto}
|
||||
<ConfirmDialogue
|
||||
id="stop-sharing-photos-modal"
|
||||
title="Stop sharing your photos?"
|
||||
prompt="{removePartnerDto.name} will no longer be able to access your photos."
|
||||
onClose={() => (removePartnerDto = null)}
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
|
||||
{#if newKey}
|
||||
<APIKeyForm
|
||||
title="New API Key"
|
||||
title="New API key"
|
||||
submitText="Create"
|
||||
apiKey={newKey}
|
||||
on:submit={({ detail }) => handleCreate(detail)}
|
||||
@@ -95,6 +95,7 @@
|
||||
|
||||
{#if editKey}
|
||||
<APIKeyForm
|
||||
title="API key"
|
||||
submitText="Save"
|
||||
apiKey={editKey}
|
||||
on:submit={({ detail }) => handleUpdate(detail)}
|
||||
@@ -104,7 +105,8 @@
|
||||
|
||||
{#if deleteKey}
|
||||
<ConfirmDialogue
|
||||
prompt="Are you sure you want to delete this API Key?"
|
||||
id="confirm-api-key-delete-modal"
|
||||
prompt="Are you sure you want to delete this API key?"
|
||||
onConfirm={() => handleDelete()}
|
||||
onClose={() => (deleteKey = null)}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,29 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
import { fileUploadHandler } from '$lib/utils/file-uploader';
|
||||
|
||||
let albumId: string | undefined;
|
||||
|
||||
const dropHandler = async ({ dataTransfer }: DragEvent) => {
|
||||
const files = dataTransfer?.files;
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filesArray: File[] = Array.from<File>(files);
|
||||
albumId = ($page.route.id === '/(user)/albums/[albumId]' || undefined) && $page.params.albumId;
|
||||
|
||||
const isShare = $page.route.id === '/(user)/share/[key]' || undefined;
|
||||
if (isShare) {
|
||||
dragAndDropFilesStore.set({ isDragging: true, files: filesArray });
|
||||
} else {
|
||||
await fileUploadHandler(filesArray, albumId);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<slot {albumId} />
|
||||
|
||||
<UploadCover {dropHandler} />
|
||||
<slot />
|
||||
<UploadCover />
|
||||
|
||||
@@ -448,6 +448,7 @@
|
||||
{/if}
|
||||
|
||||
{#if album.assetCount > 0}
|
||||
<CircleIconButton title="Slideshow" on:click={handleStartSlideshow} icon={mdiPresentationPlay} />
|
||||
<CircleIconButton title="Download" on:click={handleDownloadAlbum} icon={mdiFolderDownloadOutline} />
|
||||
|
||||
{#if isOwned}
|
||||
@@ -455,7 +456,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"
|
||||
@@ -665,17 +665,17 @@
|
||||
{album}
|
||||
on:select={({ detail: users }) => handleAddUsers(users)}
|
||||
on:share={() => (viewMode = ViewMode.LINK_SHARING)}
|
||||
on:close={() => (viewMode = ViewMode.VIEW)}
|
||||
onClose={() => (viewMode = ViewMode.VIEW)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.LINK_SHARING}
|
||||
<CreateSharedLinkModal albumId={album.id} on:close={() => (viewMode = ViewMode.VIEW)} />
|
||||
<CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = ViewMode.VIEW)} />
|
||||
{/if}
|
||||
|
||||
{#if viewMode === ViewMode.VIEW_USERS}
|
||||
<ShareInfoModal
|
||||
on:close={() => (viewMode = ViewMode.VIEW)}
|
||||
onClose={() => (viewMode = ViewMode.VIEW)}
|
||||
{album}
|
||||
on:remove={({ detail: userId }) => handleRemoveUser(userId)}
|
||||
/>
|
||||
@@ -683,6 +683,7 @@
|
||||
|
||||
{#if viewMode === ViewMode.CONFIRM_DELETE}
|
||||
<ConfirmDialogue
|
||||
id="delete-album-modal"
|
||||
title="Delete album"
|
||||
confirmText="Delete"
|
||||
onConfirm={handleRemoveAlbum}
|
||||
|
||||
@@ -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>
|
||||
@@ -463,35 +464,23 @@
|
||||
{/if}
|
||||
|
||||
{#if showChangeNameModal}
|
||||
<FullScreenModal onClose={() => (showChangeNameModal = false)}>
|
||||
<div
|
||||
class="w-[500px] max-w-[95vw] rounded-3xl border bg-immich-bg p-4 py-8 shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col place-content-center place-items-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
<h1 class="text-2xl font-medium text-immich-primary dark:text-immich-dark-primary">Change name</h1>
|
||||
<FullScreenModal id="change-name-modal" title="Change name" onClose={() => (showChangeNameModal = false)}>
|
||||
<form on:submit|preventDefault={submitNameChange} autocomplete="off" id="change-name-form">
|
||||
<div class="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={personName} />
|
||||
</div>
|
||||
|
||||
<form on:submit|preventDefault={submitNameChange} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="name">Name</label>
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full gap-4 px-4">
|
||||
<Button
|
||||
color="gray"
|
||||
fullwidth
|
||||
on:click={() => {
|
||||
showChangeNameModal = false;
|
||||
}}>Cancel</Button
|
||||
>
|
||||
<Button type="submit" fullwidth>Ok</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</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}
|
||||
|
||||
|
||||
@@ -83,12 +83,13 @@
|
||||
</section>
|
||||
|
||||
{#if editSharedLink}
|
||||
<CreateSharedLinkModal editingLink={editSharedLink} on:close={handleEditDone} />
|
||||
<CreateSharedLinkModal editingLink={editSharedLink} onClose={handleEditDone} />
|
||||
{/if}
|
||||
|
||||
{#if deleteLinkId}
|
||||
<ConfirmDialogue
|
||||
title="Delete Shared Link"
|
||||
id="delete-shared-link-modal"
|
||||
title="Delete shared link"
|
||||
prompt="Are you sure you want to delete this shared link?"
|
||||
confirmText="Delete"
|
||||
onConfirm={() => handleDeleteLink()}
|
||||
|
||||
@@ -98,7 +98,8 @@
|
||||
|
||||
{#if isShowEmptyConfirmation}
|
||||
<ConfirmDialogue
|
||||
title="Empty Trash"
|
||||
id="empty-trash-modal"
|
||||
title="Empty trash"
|
||||
confirmText="Empty"
|
||||
onConfirm={handleEmptyTrash}
|
||||
onClose={() => (isShowEmptyConfirmation = false)}
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import '../app.css';
|
||||
|
||||
let showNavigationLoadingBar = false;
|
||||
let albumId: string | undefined;
|
||||
|
||||
const isSharedLinkRoute = (route: string | null) => route?.startsWith('/(user)/share/[key]');
|
||||
|
||||
@@ -111,7 +110,7 @@
|
||||
</FullscreenContainer>
|
||||
</noscript>
|
||||
|
||||
<slot {albumId} />
|
||||
<slot />
|
||||
|
||||
{#if showNavigationLoadingBar}
|
||||
<NavigationLoadingBar />
|
||||
|
||||
@@ -118,7 +118,9 @@
|
||||
|
||||
const handleCreate = async (ownerId: string) => {
|
||||
try {
|
||||
const createdLibrary = await createLibrary({ createLibraryDto: { ownerId, type: LibraryType.External } });
|
||||
const createdLibrary = await createLibrary({
|
||||
createLibraryDto: { ownerId, type: LibraryType.External },
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
message: `Created library: ${createdLibrary.name}`,
|
||||
@@ -300,6 +302,7 @@
|
||||
|
||||
{#if confirmDeleteLibrary}
|
||||
<ConfirmDialogue
|
||||
id="warning-modal"
|
||||
title="Warning!"
|
||||
prompt="Are you sure you want to delete this library? This will delete all {deleteAssetCount} contained assets from Immich and cannot be undone. Files will remain on disk."
|
||||
onConfirm={handleDelete}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialoge.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/admin-page/delete-confirm-dialogue.svelte';
|
||||
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
|
||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialoge.svelte';
|
||||
import RestoreDialogue from '$lib/components/admin-page/restore-dialogue.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import CreateUserForm from '$lib/components/forms/create-user-form.svelte';
|
||||
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,
|
||||
@@ -116,22 +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 onClose={() => (shouldShowCreateUserForm = false)}>
|
||||
<CreateUserForm on:submit={onUserCreated} on:cancel={() => (shouldShowCreateUserForm = false)} />
|
||||
</FullScreenModal>
|
||||
<CreateUserForm
|
||||
on:submit={onUserCreated}
|
||||
on:cancel={() => (shouldShowCreateUserForm = false)}
|
||||
onClose={() => (shouldShowCreateUserForm = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowEditUserForm}
|
||||
<FullScreenModal onClose={() => (shouldShowEditUserForm = false)}>
|
||||
<EditUserForm
|
||||
user={selectedUser}
|
||||
bind:newPassword
|
||||
canResetPassword={selectedUser?.id !== $user.id}
|
||||
on:editSuccess={onEditUserSuccess}
|
||||
on:resetPasswordSuccess={onEditPasswordSuccess}
|
||||
on:close={() => (shouldShowEditUserForm = false)}
|
||||
/>
|
||||
</FullScreenModal>
|
||||
<EditUserForm
|
||||
user={selectedUser}
|
||||
bind:newPassword
|
||||
canResetPassword={selectedUser?.id !== $user.id}
|
||||
on:editSuccess={onEditUserSuccess}
|
||||
on:resetPasswordSuccess={onEditPasswordSuccess}
|
||||
onClose={() => (shouldShowEditUserForm = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if shouldShowDeleteConfirmDialog}
|
||||
@@ -153,40 +152,39 @@
|
||||
{/if}
|
||||
|
||||
{#if shouldShowPasswordResetSuccess}
|
||||
<FullScreenModal onClose={() => (shouldShowPasswordResetSuccess = false)}>
|
||||
<ConfirmDialogue
|
||||
title="Password Reset Success"
|
||||
confirmText="Done"
|
||||
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
|
||||
onClose={() => (shouldShowPasswordResetSuccess = false)}
|
||||
hideCancelButton={true}
|
||||
confirmColor="green"
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>The user's password has been reset:</p>
|
||||
<ConfirmDialogue
|
||||
id="password-reset-success-modal"
|
||||
title="Password reset success"
|
||||
confirmText="Done"
|
||||
onConfirm={() => (shouldShowPasswordResetSuccess = false)}
|
||||
onClose={() => (shouldShowPasswordResetSuccess = false)}
|
||||
hideCancelButton={true}
|
||||
confirmColor="green"
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>The user's password has been reset:</p>
|
||||
|
||||
<div class="flex justify-center gap-2">
|
||||
<code
|
||||
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
|
||||
>
|
||||
{newPassword}
|
||||
</code>
|
||||
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiContentCopy} size="18" />
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Please provide the temporary password to the user and inform them they will need to change the
|
||||
password at their next login.
|
||||
</p>
|
||||
<div class="flex justify-center gap-2">
|
||||
<code
|
||||
class="rounded-md bg-gray-200 px-2 py-1 font-bold text-immich-primary dark:text-immich-dark-primary dark:bg-gray-700"
|
||||
>
|
||||
{newPassword}
|
||||
</code>
|
||||
<LinkButton on:click={() => copyToClipboard(newPassword)} title="Copy password">
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiContentCopy} size="18" />
|
||||
</div>
|
||||
</LinkButton>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
</FullScreenModal>
|
||||
|
||||
<p>
|
||||
Please provide the temporary password to the user and inform them they will need to change the password
|
||||
at their next login.
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
{/if}
|
||||
|
||||
<table class="my-5 w-full text-left">
|
||||
@@ -200,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