mirror of
https://github.com/immich-app/immich.git
synced 2025-12-23 07:29:28 -08:00
refactor: shared links modals (#23803)
This commit is contained in:
76
web/src/lib/components/SharedLinkExpiration.svelte
Normal file
76
web/src/lib/components/SharedLinkExpiration.svelte
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||||
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
|
import { minBy } from 'lodash-es';
|
||||||
|
import { DateTime, Duration } from 'luxon';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
createdAt?: string;
|
||||||
|
expiresAt: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
let { createdAt = DateTime.now().toISO(), expiresAt = $bindable() }: Props = $props();
|
||||||
|
|
||||||
|
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
|
||||||
|
[30, 'minutes'],
|
||||||
|
[1, 'hour'],
|
||||||
|
[6, 'hours'],
|
||||||
|
[1, 'day'],
|
||||||
|
[7, 'days'],
|
||||||
|
[30, 'days'],
|
||||||
|
[3, 'months'],
|
||||||
|
[1, 'year'],
|
||||||
|
];
|
||||||
|
|
||||||
|
const relativeTime = $derived(new Intl.RelativeTimeFormat($locale));
|
||||||
|
const expiredDateOptions = $derived([
|
||||||
|
{ text: $t('never'), value: 0 },
|
||||||
|
...expirationOptions
|
||||||
|
.map(([value, unit]) => ({
|
||||||
|
text: relativeTime.format(value, unit),
|
||||||
|
value: Duration.fromObject({ [unit]: value }).toMillis(),
|
||||||
|
}))
|
||||||
|
.filter(({ value: millis }) => DateTime.fromISO(createdAt).plus(millis) > DateTime.now()),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const getExpirationOption = (createdAt: string, expiresAt: string | null) => {
|
||||||
|
if (!expiresAt) {
|
||||||
|
return expiredDateOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const delta = DateTime.fromISO(expiresAt).diff(DateTime.fromISO(createdAt)).toMillis();
|
||||||
|
const closestOption = minBy(expiredDateOptions, ({ value }) => Math.abs(delta - value));
|
||||||
|
|
||||||
|
if (!closestOption) {
|
||||||
|
return expiredDateOptions[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow a generous epsilon to compensate for potential API delays
|
||||||
|
if (Math.abs(closestOption.value - delta) > 10_000) {
|
||||||
|
const interval = DateTime.fromMillis(closestOption.value) as DateTime<true>;
|
||||||
|
return { text: interval.toRelative({ locale: $locale }), value: closestOption.value };
|
||||||
|
}
|
||||||
|
|
||||||
|
return closestOption;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = (option: number | string) => {
|
||||||
|
const expirationOption = Number(option);
|
||||||
|
|
||||||
|
expiresAt = expirationOption === 0 ? null : DateTime.fromISO(createdAt).plus(expirationOption).toISO();
|
||||||
|
};
|
||||||
|
|
||||||
|
let expirationOption = $derived(getExpirationOption(createdAt, expiresAt).value);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<SettingSelect
|
||||||
|
bind:value={expirationOption}
|
||||||
|
{onSelect}
|
||||||
|
options={[...new Set([...expiredDateOptions, getExpirationOption(createdAt, expiresAt)])]}
|
||||||
|
label={$t('expire_after')}
|
||||||
|
disabled={expiresAt !== null && DateTime.fromISO(expiresAt) < DateTime.now()}
|
||||||
|
number={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
@@ -1,23 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
import SharedLinkExpiration from '$lib/components/SharedLinkExpiration.svelte';
|
||||||
import { handleCreateSharedLink, handleUpdateSharedLink } from '$lib/services/shared-link.service';
|
import { handleCreateSharedLink } from '$lib/services/shared-link.service';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { SharedLinkType } from '@immich/sdk';
|
||||||
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, PasswordInput, Switch, Text } from '@immich/ui';
|
||||||
import { Button, Field, Input, Modal, ModalBody, ModalFooter, PasswordInput, Switch, Text } from '@immich/ui';
|
|
||||||
import { mdiLink } from '@mdi/js';
|
import { mdiLink } from '@mdi/js';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClose: (success?: boolean) => void;
|
onClose: (success?: boolean) => void;
|
||||||
albumId?: string | undefined;
|
albumId?: string;
|
||||||
assetIds?: string[];
|
assetIds?: string[];
|
||||||
editingLink?: SharedLinkResponseDto | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let { onClose, albumId = $bindable(undefined), assetIds = $bindable([]), editingLink = undefined }: Props = $props();
|
let { onClose, albumId = $bindable(), assetIds = $bindable([]) }: Props = $props();
|
||||||
|
|
||||||
let sharedLink: string | null = $state(null);
|
|
||||||
let description = $state('');
|
let description = $state('');
|
||||||
let allowDownload = $state(true);
|
let allowDownload = $state(true);
|
||||||
let allowUpload = $state(false);
|
let allowUpload = $state(false);
|
||||||
@@ -25,27 +22,7 @@
|
|||||||
let expirationOption: number = $state(0);
|
let expirationOption: number = $state(0);
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
let slug = $state('');
|
let slug = $state('');
|
||||||
let shouldChangeExpirationTime = $state(false);
|
let expiresAt = $state<string | null>(null);
|
||||||
|
|
||||||
const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [
|
|
||||||
[30, 'minutes'],
|
|
||||||
[1, 'hour'],
|
|
||||||
[6, 'hours'],
|
|
||||||
[1, 'day'],
|
|
||||||
[7, 'days'],
|
|
||||||
[30, 'days'],
|
|
||||||
[3, 'months'],
|
|
||||||
[1, 'year'],
|
|
||||||
];
|
|
||||||
|
|
||||||
let relativeTime = $derived(new Intl.RelativeTimeFormat($locale));
|
|
||||||
let expiredDateOptions = $derived([
|
|
||||||
{ text: $t('never'), value: 0 },
|
|
||||||
...expirationOptions.map(([value, unit]) => ({
|
|
||||||
text: relativeTime.format(value, unit),
|
|
||||||
value: Duration.fromObject({ [unit]: value }).toMillis(),
|
|
||||||
})),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let shareType = $derived(albumId ? SharedLinkType.Album : SharedLinkType.Individual);
|
let shareType = $derived(albumId ? SharedLinkType.Album : SharedLinkType.Individual);
|
||||||
|
|
||||||
@@ -55,21 +32,6 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (editingLink) {
|
|
||||||
if (editingLink.description) {
|
|
||||||
description = editingLink.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
password = editingLink.password ?? '';
|
|
||||||
slug = editingLink.slug ?? '';
|
|
||||||
allowUpload = editingLink.allowUpload;
|
|
||||||
allowDownload = editingLink.allowDownload;
|
|
||||||
showMetadata = editingLink.showMetadata;
|
|
||||||
|
|
||||||
albumId = editingLink.album?.id;
|
|
||||||
assetIds = editingLink.assets.map(({ id }) => id);
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCreate = async () => {
|
const onCreate = async () => {
|
||||||
const success = await handleCreateSharedLink({
|
const success = await handleCreateSharedLink({
|
||||||
type: shareType,
|
type: shareType,
|
||||||
@@ -88,64 +50,16 @@
|
|||||||
onClose(true);
|
onClose(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onUpdate = async () => {
|
|
||||||
if (!editingLink) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const success = await handleUpdateSharedLink(editingLink, {
|
|
||||||
description,
|
|
||||||
password: password ?? null,
|
|
||||||
expiresAt: shouldChangeExpirationTime
|
|
||||||
? expirationOption > 0
|
|
||||||
? DateTime.now().plus(expirationOption).toISO()
|
|
||||||
: null
|
|
||||||
: undefined,
|
|
||||||
allowUpload,
|
|
||||||
allowDownload,
|
|
||||||
showMetadata,
|
|
||||||
slug: slug.trim() ?? null,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
onClose(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getTitle = () => {
|
|
||||||
if (sharedLink) {
|
|
||||||
return $t('view_link');
|
|
||||||
}
|
|
||||||
if (editingLink) {
|
|
||||||
return $t('edit_link');
|
|
||||||
}
|
|
||||||
return $t('create_link_to_share');
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal title={getTitle()} icon={mdiLink} size="small" {onClose}>
|
<Modal title={$t('create_link_to_share')} icon={mdiLink} size="small" {onClose}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{#if shareType === SharedLinkType.Album}
|
{#if shareType === SharedLinkType.Album}
|
||||||
{#if !editingLink}
|
|
||||||
<div>{$t('album_with_link_access')}</div>
|
<div>{$t('album_with_link_access')}</div>
|
||||||
{:else}
|
|
||||||
<div class="text-sm">
|
|
||||||
{$t('public_album')} |
|
|
||||||
<span class="text-primary">{editingLink.album?.albumName}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if shareType === SharedLinkType.Individual}
|
{#if shareType === SharedLinkType.Individual}
|
||||||
{#if !editingLink}
|
|
||||||
<div>{$t('create_link_to_share_description')}</div>
|
<div>{$t('create_link_to_share_description')}</div>
|
||||||
{:else}
|
|
||||||
<div class="text-sm">
|
|
||||||
{$t('individual_share')} |
|
|
||||||
<span class="text-primary">{editingLink.description || ''}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex flex-col gap-4 mt-4">
|
<div class="flex flex-col gap-4 mt-4">
|
||||||
@@ -166,15 +80,7 @@
|
|||||||
<Input bind:value={description} autocomplete="off" />
|
<Input bind:value={description} autocomplete="off" />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<div class="mt-2">
|
<SharedLinkExpiration bind:expiresAt />
|
||||||
<SettingSelect
|
|
||||||
bind:value={expirationOption}
|
|
||||||
options={expiredDateOptions}
|
|
||||||
label={$t('expire_after')}
|
|
||||||
disabled={editingLink && !shouldChangeExpirationTime}
|
|
||||||
number={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field label={$t('show_metadata')}>
|
<Field label={$t('show_metadata')}>
|
||||||
<Switch bind:checked={showMetadata} />
|
<Switch bind:checked={showMetadata} />
|
||||||
@@ -187,20 +93,13 @@
|
|||||||
<Field label={$t('allow_public_user_to_upload')}>
|
<Field label={$t('allow_public_user_to_upload')}>
|
||||||
<Switch bind:checked={allowUpload} />
|
<Switch bind:checked={allowUpload} />
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
{#if editingLink}
|
|
||||||
<Field label={$t('change_expiration_time')}>
|
|
||||||
<Switch bind:checked={shouldChangeExpirationTime} />
|
|
||||||
</Field>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{#if editingLink}
|
<HStack fullWidth>
|
||||||
<Button fullWidth onclick={onUpdate}>{$t('confirm')}</Button>
|
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
||||||
{:else}
|
<Button fullWidth shape="round" onclick={onCreate}>{$t('create_link')}</Button>
|
||||||
<Button fullWidth onclick={onCreate}>{$t('create_link')}</Button>
|
</HStack>
|
||||||
{/if}
|
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
98
web/src/lib/modals/SharedLinkUpdateModal.svelte
Normal file
98
web/src/lib/modals/SharedLinkUpdateModal.svelte
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import SharedLinkExpiration from '$lib/components/SharedLinkExpiration.svelte';
|
||||||
|
import { handleUpdateSharedLink } from '$lib/services/shared-link.service';
|
||||||
|
import { SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { Button, Field, HStack, Input, Modal, ModalBody, ModalFooter, PasswordInput, Switch, Text } from '@immich/ui';
|
||||||
|
import { mdiLink } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: (success?: boolean) => void;
|
||||||
|
sharedLink: SharedLinkResponseDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onClose, sharedLink }: Props = $props();
|
||||||
|
|
||||||
|
let description = $state(sharedLink.description ?? '');
|
||||||
|
let allowDownload = $state(sharedLink.allowDownload);
|
||||||
|
let allowUpload = $state(sharedLink.allowUpload);
|
||||||
|
let showMetadata = $state(sharedLink.showMetadata);
|
||||||
|
let password = $state(sharedLink.password ?? '');
|
||||||
|
let slug = $state(sharedLink.slug ?? '');
|
||||||
|
let shareType = sharedLink.album ? SharedLinkType.Album : SharedLinkType.Individual;
|
||||||
|
let expiresAt = $state(sharedLink.expiresAt);
|
||||||
|
|
||||||
|
const onUpdate = async () => {
|
||||||
|
const success = await handleUpdateSharedLink(sharedLink, {
|
||||||
|
description,
|
||||||
|
password: password ?? null,
|
||||||
|
expiresAt,
|
||||||
|
allowUpload,
|
||||||
|
allowDownload,
|
||||||
|
showMetadata,
|
||||||
|
slug: slug.trim() ?? null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
onClose(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal title={$t('edit_link')} icon={mdiLink} size="small" {onClose}>
|
||||||
|
<ModalBody>
|
||||||
|
{#if shareType === SharedLinkType.Album}
|
||||||
|
<div class="text-sm">
|
||||||
|
{$t('public_album')} |
|
||||||
|
<span class="text-primary">{sharedLink.album?.albumName}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if shareType === SharedLinkType.Individual}
|
||||||
|
<div class="text-sm">
|
||||||
|
{$t('individual_share')} |
|
||||||
|
<span class="text-primary">{sharedLink.description || ''}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 mt-4">
|
||||||
|
<div>
|
||||||
|
<Field label={$t('custom_url')} description={$t('shared_link_custom_url_description')}>
|
||||||
|
<Input bind:value={slug} autocomplete="off" />
|
||||||
|
</Field>
|
||||||
|
{#if slug}
|
||||||
|
<Text size="tiny" color="muted" class="pt-2">/s/{encodeURIComponent(slug)}</Text>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field label={$t('password')} description={$t('shared_link_password_description')}>
|
||||||
|
<PasswordInput bind:value={password} autocomplete="new-password" />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label={$t('description')}>
|
||||||
|
<Input bind:value={description} autocomplete="off" />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<SharedLinkExpiration createdAt={sharedLink.createdAt} bind:expiresAt />
|
||||||
|
|
||||||
|
<Field label={$t('show_metadata')}>
|
||||||
|
<Switch bind:checked={showMetadata} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label={$t('allow_public_user_to_download')} disabled={!showMetadata}>
|
||||||
|
<Switch bind:checked={allowDownload} />
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label={$t('allow_public_user_to_upload')}>
|
||||||
|
<Switch bind:checked={allowUpload} />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
|
||||||
|
<ModalFooter>
|
||||||
|
<HStack fullWidth>
|
||||||
|
<Button color="secondary" shape="round" fullWidth onclick={() => onClose()}>{$t('cancel')}</Button>
|
||||||
|
<Button fullWidth shape="round" onclick={onUpdate}>{$t('confirm')}</Button>
|
||||||
|
</HStack>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import GroupTab from '$lib/elements/GroupTab.svelte';
|
import GroupTab from '$lib/elements/GroupTab.svelte';
|
||||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
import SharedLinkUpdateModal from '$lib/modals/SharedLinkUpdateModal.svelte';
|
||||||
import { getAllSharedLinks, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
import { getAllSharedLinks, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if sharedLink}
|
{#if sharedLink}
|
||||||
<SharedLinkCreateModal editingLink={sharedLink} onClose={() => goto(AppRoute.SHARED_LINKS)} />
|
<SharedLinkUpdateModal {sharedLink} onClose={() => goto(AppRoute.SHARED_LINKS)} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</UserPageLayout>
|
</UserPageLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user