mirror of
https://github.com/immich-app/immich
synced 2025-06-06 19:28:26 +00:00
refactor: share modals (#18183)
This commit is contained in:
parent
47b1938f17
commit
6a69dafd31
@ -1,43 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { addUsersToAlbum, deleteAlbum, type AlbumUserAddDto, type AlbumResponseDto, isHttpError } from '@immich/sdk';
|
||||
import { mdiDeleteOutline, mdiShareVariantOutline, mdiFolderDownloadOutline, mdiRenameOutline } from '@mdi/js';
|
||||
import { goto } from '$app/navigation';
|
||||
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
||||
import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
|
||||
import EditAlbumForm from '$lib/components/forms/edit-album-form.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import RightClickContextMenu from '$lib/components/shared-components/context-menu/right-click-context-menu.svelte';
|
||||
import AlbumsTable from '$lib/components/album-page/albums-table.svelte';
|
||||
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
|
||||
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { downloadAlbum } from '$lib/utils/asset-utils';
|
||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||
import {
|
||||
getSelectedAlbumGroupOption,
|
||||
type AlbumGroup,
|
||||
confirmAlbumDelete,
|
||||
sortAlbums,
|
||||
stringToSortOrder,
|
||||
} from '$lib/utils/album-utils';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
|
||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import {
|
||||
AlbumFilter,
|
||||
AlbumGroupBy,
|
||||
AlbumSortBy,
|
||||
AlbumFilter,
|
||||
AlbumViewMode,
|
||||
SortOrder,
|
||||
locale,
|
||||
type AlbumViewSettings,
|
||||
} from '$lib/stores/preferences.store';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { userInteraction } from '$lib/stores/user.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { makeSharedLinkUrl } from '$lib/utils';
|
||||
import {
|
||||
confirmAlbumDelete,
|
||||
getSelectedAlbumGroupOption,
|
||||
sortAlbums,
|
||||
stringToSortOrder,
|
||||
type AlbumGroup,
|
||||
} from '$lib/utils/album-utils';
|
||||
import { downloadAlbum } from '$lib/utils/asset-utils';
|
||||
import type { ContextMenuPosition } from '$lib/utils/context-menu';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { normalizeSearchString } from '$lib/utils/string-utils';
|
||||
import { addUsersToAlbum, deleteAlbum, isHttpError, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk';
|
||||
import { mdiDeleteOutline, mdiFolderDownloadOutline, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js';
|
||||
import { groupBy } from 'lodash-es';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { run } from 'svelte/legacy';
|
||||
|
||||
@ -140,8 +144,6 @@
|
||||
|
||||
let albumGroupOption: string = $state(AlbumGroupBy.None);
|
||||
|
||||
let showShareByURLModal = $state(false);
|
||||
|
||||
let albumToEdit: AlbumResponseDto | null = $state(null);
|
||||
let albumToShare: AlbumResponseDto | null = $state(null);
|
||||
let albumToDelete: AlbumResponseDto | null = null;
|
||||
@ -346,18 +348,32 @@
|
||||
updateAlbumInfo(album);
|
||||
};
|
||||
|
||||
const openShareModal = () => {
|
||||
const openShareModal = async () => {
|
||||
if (!contextMenuTargetAlbum) {
|
||||
return;
|
||||
}
|
||||
|
||||
albumToShare = contextMenuTargetAlbum;
|
||||
closeAlbumContextMenu();
|
||||
};
|
||||
const result = await modalManager.show(AlbumShareModal, { album: albumToShare });
|
||||
|
||||
const closeShareModal = () => {
|
||||
albumToShare = null;
|
||||
showShareByURLModal = false;
|
||||
switch (result?.action) {
|
||||
case 'sharedUsers': {
|
||||
await handleAddUsers(result.data);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'sharedLink': {
|
||||
const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: albumToShare.id });
|
||||
|
||||
if (sharedLink) {
|
||||
const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
|
||||
handleSharedLinkCreated(albumToShare);
|
||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -419,22 +435,4 @@
|
||||
onClose={() => (albumToEdit = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Share Modal -->
|
||||
{#if albumToShare}
|
||||
{#if showShareByURLModal}
|
||||
<CreateSharedLinkModal
|
||||
albumId={albumToShare.id}
|
||||
onClose={() => closeShareModal()}
|
||||
onCreated={() => albumToShare && handleSharedLinkCreated(albumToShare)}
|
||||
/>
|
||||
{:else}
|
||||
<UserSelectionModal
|
||||
album={albumToShare}
|
||||
onSelect={handleAddUsers}
|
||||
onShare={() => (showShareByURLModal = true)}
|
||||
onClose={() => closeShareModal()}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
|
@ -1,7 +1,10 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { makeSharedLinkUrl } from '$lib/utils';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { mdiShareVariantOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
@ -12,13 +15,14 @@
|
||||
|
||||
let { asset }: Props = $props();
|
||||
|
||||
let showModal = $state(false);
|
||||
const handleClick = async () => {
|
||||
const sharedLink = await modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] });
|
||||
|
||||
if (sharedLink) {
|
||||
const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
|
||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<CircleIconButton color="opaque" icon={mdiShareVariantOutline} onclick={() => (showModal = true)} title={$t('share')} />
|
||||
|
||||
{#if showModal}
|
||||
<Portal target="body">
|
||||
<CreateSharedLinkModal assetIds={[asset.id]} onClose={() => (showModal = false)} />
|
||||
</Portal>
|
||||
{/if}
|
||||
<CircleIconButton color="opaque" icon={mdiShareVariantOutline} onclick={handleClick} title={$t('share')} />
|
||||
|
@ -1,16 +1,26 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import { getAssetControlContext } from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { makeSharedLinkUrl } from '$lib/utils';
|
||||
import { mdiShareVariantOutline } from '@mdi/js';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let showModal = $state(false);
|
||||
const { getAssets } = getAssetControlContext();
|
||||
|
||||
const handleClick = async () => {
|
||||
const sharedLink = await modalManager.show(SharedLinkCreateModal, {
|
||||
assetIds: [...getAssets()].map(({ id }) => id),
|
||||
});
|
||||
|
||||
if (sharedLink) {
|
||||
const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
|
||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} onclick={() => (showModal = true)} />
|
||||
|
||||
{#if showModal}
|
||||
<CreateSharedLinkModal assetIds={[...getAssets()].map(({ id }) => id)} onClose={() => (showModal = false)} />
|
||||
{/if}
|
||||
<CircleIconButton title={$t('share')} icon={mdiShareVariantOutline} onclick={handleClick} />
|
||||
|
@ -1,251 +0,0 @@
|
||||
<script lang="ts">
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import QrCodeModal from '$lib/components/shared-components/qr-code-modal.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { makeSharedLinkUrl } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { mdiLink } from '@mdi/js';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { NotificationType, notificationController } from '../notification/notification';
|
||||
import SettingInputField from '../settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '../settings/setting-switch.svelte';
|
||||
|
||||
interface Props {
|
||||
onClose: () => void;
|
||||
albumId?: string | undefined;
|
||||
assetIds?: string[];
|
||||
editingLink?: SharedLinkResponseDto | undefined;
|
||||
onCreated?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
onClose,
|
||||
albumId = $bindable(undefined),
|
||||
assetIds = $bindable([]),
|
||||
editingLink = undefined,
|
||||
onCreated = () => {},
|
||||
}: Props = $props();
|
||||
|
||||
let sharedLink: string | null = $state(null);
|
||||
let description = $state('');
|
||||
let allowDownload = $state(true);
|
||||
let allowUpload = $state(false);
|
||||
let showMetadata = $state(true);
|
||||
let expirationOption: number = $state(0);
|
||||
let password = $state('');
|
||||
let shouldChangeExpirationTime = $state(false);
|
||||
let enablePassword = $state(false);
|
||||
|
||||
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);
|
||||
|
||||
$effect(() => {
|
||||
if (!showMetadata) {
|
||||
allowDownload = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (editingLink) {
|
||||
if (editingLink.description) {
|
||||
description = editingLink.description;
|
||||
}
|
||||
if (editingLink.password) {
|
||||
password = editingLink.password;
|
||||
}
|
||||
allowUpload = editingLink.allowUpload;
|
||||
allowDownload = editingLink.allowDownload;
|
||||
showMetadata = editingLink.showMetadata;
|
||||
|
||||
albumId = editingLink.album?.id;
|
||||
assetIds = editingLink.assets.map(({ id }) => id);
|
||||
|
||||
enablePassword = !!editingLink.password;
|
||||
}
|
||||
|
||||
const handleCreateSharedLink = async () => {
|
||||
const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined;
|
||||
|
||||
try {
|
||||
const data = await createSharedLink({
|
||||
sharedLinkCreateDto: {
|
||||
type: shareType,
|
||||
albumId,
|
||||
assetIds,
|
||||
expiresAt: expirationDate,
|
||||
allowUpload,
|
||||
description,
|
||||
password,
|
||||
allowDownload,
|
||||
showMetadata,
|
||||
},
|
||||
});
|
||||
sharedLink = makeSharedLinkUrl($serverConfig.externalDomain, data.key);
|
||||
onCreated();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_create_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditLink = async () => {
|
||||
if (!editingLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null;
|
||||
|
||||
await updateSharedLink({
|
||||
id: editingLink.id,
|
||||
sharedLinkEditDto: {
|
||||
description,
|
||||
password: enablePassword ? password : '',
|
||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||
allowUpload,
|
||||
allowDownload,
|
||||
showMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('edited'),
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_edit_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (sharedLink) {
|
||||
return $t('view_link');
|
||||
}
|
||||
if (editingLink) {
|
||||
return $t('edit_link');
|
||||
}
|
||||
return $t('create_link_to_share');
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if !sharedLink || editingLink}
|
||||
<FullScreenModal title={getTitle()} icon={mdiLink} {onClose}>
|
||||
<section>
|
||||
{#if shareType === SharedLinkType.Album}
|
||||
{#if !editingLink}
|
||||
<div>{$t('album_with_link_access')}</div>
|
||||
{:else}
|
||||
<div class="text-sm">
|
||||
{$t('public_album')} |
|
||||
<span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.album?.albumName}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if shareType === SharedLinkType.Individual}
|
||||
{#if !editingLink}
|
||||
<div>{$t('create_link_to_share_description')}</div>
|
||||
{:else}
|
||||
<div class="text-sm">
|
||||
{$t('individual_share')} |
|
||||
<span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.description || ''}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="mb-2 mt-4">
|
||||
<p class="text-xs">{$t('link_options').toUpperCase()}</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 overflow-y-auto">
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('description')}
|
||||
bind:value={description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('password')}
|
||||
bind:value={password}
|
||||
disabled={!enablePassword}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={enablePassword} title={$t('require_password')} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={showMetadata} title={$t('show_metadata')} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch
|
||||
bind:checked={allowDownload}
|
||||
title={$t('allow_public_user_to_download')}
|
||||
disabled={!showMetadata}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} />
|
||||
</div>
|
||||
|
||||
{#if editingLink}
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-3">
|
||||
<SettingSelect
|
||||
bind:value={expirationOption}
|
||||
options={expiredDateOptions}
|
||||
label={$t('expire_after')}
|
||||
disabled={editingLink && !shouldChangeExpirationTime}
|
||||
number={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#snippet stickyBottom()}
|
||||
{#if editingLink}
|
||||
<Button fullWidth onclick={handleEditLink}>{$t('confirm')}</Button>
|
||||
{:else}
|
||||
<Button fullWidth onclick={handleCreateSharedLink}>{$t('create_link')}</Button>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</FullScreenModal>
|
||||
{:else}
|
||||
<QrCodeModal title={$t('view_link')} {onClose} value={sharedLink} />
|
||||
{/if}
|
@ -367,8 +367,6 @@ export enum SettingInputFieldType {
|
||||
}
|
||||
|
||||
export const AlbumPageViewMode = {
|
||||
LINK_SHARING: 'link-sharing',
|
||||
SELECT_USERS: 'select-users',
|
||||
SELECT_THUMBNAIL: 'select-thumbnail',
|
||||
SELECT_ASSETS: 'select-assets',
|
||||
VIEW_USERS: 'view-users',
|
||||
@ -377,8 +375,6 @@ export const AlbumPageViewMode = {
|
||||
};
|
||||
|
||||
export type AlbumPageViewMode =
|
||||
| typeof AlbumPageViewMode.LINK_SHARING
|
||||
| typeof AlbumPageViewMode.SELECT_USERS
|
||||
| typeof AlbumPageViewMode.SELECT_THUMBNAIL
|
||||
| typeof AlbumPageViewMode.SELECT_ASSETS
|
||||
| typeof AlbumPageViewMode.VIEW_USERS
|
||||
|
@ -3,8 +3,8 @@
|
||||
import Dropdown from '$lib/components/elements/dropdown.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import QrCodeModal from '$lib/components/shared-components/qr-code-modal.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { makeSharedLinkUrl } from '$lib/utils';
|
||||
import {
|
||||
@ -20,16 +20,14 @@
|
||||
import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import UserAvatar from '../shared-components/user-avatar.svelte';
|
||||
import UserAvatar from '../components/shared-components/user-avatar.svelte';
|
||||
|
||||
interface Props {
|
||||
album: AlbumResponseDto;
|
||||
onClose: () => void;
|
||||
onSelect: (selectedUsers: AlbumUserAddDto[]) => void;
|
||||
onShare: () => void;
|
||||
onClose: (result?: { action: 'sharedLink' } | { action: 'sharedUsers'; data: AlbumUserAddDto[] }) => void;
|
||||
}
|
||||
|
||||
let { album, onClose, onSelect, onShare }: Props = $props();
|
||||
let { album, onClose }: Props = $props();
|
||||
|
||||
let users: UserResponseDto[] = $state([]);
|
||||
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
|
||||
@ -160,8 +158,10 @@
|
||||
shape="round"
|
||||
disabled={Object.keys(selectedUsers).length === 0}
|
||||
onclick={() =>
|
||||
onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))}
|
||||
>{$t('add')}</Button
|
||||
onClose({
|
||||
action: 'sharedUsers',
|
||||
data: Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })),
|
||||
})}>{$t('add')}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@ -182,7 +182,13 @@
|
||||
</Stack>
|
||||
{/if}
|
||||
|
||||
<Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={onShare}>{$t('create_link')}</Button>
|
||||
<Button
|
||||
leadingIcon={mdiLink}
|
||||
size="small"
|
||||
shape="round"
|
||||
fullWidth
|
||||
onclick={() => onClose({ action: 'sharedLink' })}>{$t('create_link')}</Button
|
||||
>
|
||||
</Stack>
|
||||
</FullScreenModal>
|
||||
{/if}
|
@ -1,24 +1,23 @@
|
||||
<script lang="ts">
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import QRCode from '$lib/components/shared-components/qrcode.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { HStack, IconButton, Input } from '@immich/ui';
|
||||
import { HStack, IconButton, Input, Modal, ModalBody } from '@immich/ui';
|
||||
import { mdiContentCopy, mdiLink } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
onClose: () => void;
|
||||
value: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
let { onClose, title, value }: Props = $props();
|
||||
let { title, value, onClose }: Props = $props();
|
||||
|
||||
let modalWidth = $state(0);
|
||||
</script>
|
||||
|
||||
<FullScreenModal {title} icon={mdiLink} {onClose}>
|
||||
<div class="w-full">
|
||||
<Modal {title} icon={mdiLink} {onClose} size="small">
|
||||
<ModalBody>
|
||||
<div class="w-full py-2 px-10">
|
||||
<div bind:clientWidth={modalWidth} class="w-full">
|
||||
<QRCode {value} width={modalWidth} />
|
||||
@ -37,5 +36,5 @@
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
</FullScreenModal>
|
||||
</ModalBody>
|
||||
</Modal>
|
235
web/src/lib/modals/SharedLinkCreateModal.svelte
Normal file
235
web/src/lib/modals/SharedLinkCreateModal.svelte
Normal file
@ -0,0 +1,235 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { Button, Modal, ModalBody, ModalFooter } from '@immich/ui';
|
||||
import { mdiLink } from '@mdi/js';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { NotificationType, notificationController } from '../components/shared-components/notification/notification';
|
||||
import SettingInputField from '../components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '../components/shared-components/settings/setting-switch.svelte';
|
||||
|
||||
interface Props {
|
||||
onClose: (sharedLink?: SharedLinkResponseDto) => void;
|
||||
albumId?: string | undefined;
|
||||
assetIds?: string[];
|
||||
editingLink?: SharedLinkResponseDto | undefined;
|
||||
}
|
||||
|
||||
let { onClose, albumId = $bindable(undefined), assetIds = $bindable([]), editingLink = undefined }: Props = $props();
|
||||
|
||||
let sharedLink: string | null = $state(null);
|
||||
let description = $state('');
|
||||
let allowDownload = $state(true);
|
||||
let allowUpload = $state(false);
|
||||
let showMetadata = $state(true);
|
||||
let expirationOption: number = $state(0);
|
||||
let password = $state('');
|
||||
let shouldChangeExpirationTime = $state(false);
|
||||
let enablePassword = $state(false);
|
||||
|
||||
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);
|
||||
|
||||
$effect(() => {
|
||||
if (!showMetadata) {
|
||||
allowDownload = false;
|
||||
}
|
||||
});
|
||||
|
||||
if (editingLink) {
|
||||
if (editingLink.description) {
|
||||
description = editingLink.description;
|
||||
}
|
||||
if (editingLink.password) {
|
||||
password = editingLink.password;
|
||||
}
|
||||
allowUpload = editingLink.allowUpload;
|
||||
allowDownload = editingLink.allowDownload;
|
||||
showMetadata = editingLink.showMetadata;
|
||||
|
||||
albumId = editingLink.album?.id;
|
||||
assetIds = editingLink.assets.map(({ id }) => id);
|
||||
|
||||
enablePassword = !!editingLink.password;
|
||||
}
|
||||
|
||||
const handleCreateSharedLink = async () => {
|
||||
const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined;
|
||||
|
||||
try {
|
||||
const data = await createSharedLink({
|
||||
sharedLinkCreateDto: {
|
||||
type: shareType,
|
||||
albumId,
|
||||
assetIds,
|
||||
expiresAt: expirationDate,
|
||||
allowUpload,
|
||||
description,
|
||||
password,
|
||||
allowDownload,
|
||||
showMetadata,
|
||||
},
|
||||
});
|
||||
onClose(data);
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_create_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditLink = async () => {
|
||||
if (!editingLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null;
|
||||
|
||||
await updateSharedLink({
|
||||
id: editingLink.id,
|
||||
sharedLinkEditDto: {
|
||||
description,
|
||||
password: enablePassword ? password : '',
|
||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||
allowUpload,
|
||||
allowDownload,
|
||||
showMetadata,
|
||||
},
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: $t('edited'),
|
||||
});
|
||||
|
||||
onClose();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.failed_to_edit_shared_link'));
|
||||
}
|
||||
};
|
||||
|
||||
const getTitle = () => {
|
||||
if (sharedLink) {
|
||||
return $t('view_link');
|
||||
}
|
||||
if (editingLink) {
|
||||
return $t('edit_link');
|
||||
}
|
||||
return $t('create_link_to_share');
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal title={getTitle()} icon={mdiLink} size="small" {onClose}>
|
||||
<ModalBody>
|
||||
{#if shareType === SharedLinkType.Album}
|
||||
{#if !editingLink}
|
||||
<div>{$t('album_with_link_access')}</div>
|
||||
{:else}
|
||||
<div class="text-sm">
|
||||
{$t('public_album')} |
|
||||
<span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.album?.albumName}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if shareType === SharedLinkType.Individual}
|
||||
{#if !editingLink}
|
||||
<div>{$t('create_link_to_share_description')}</div>
|
||||
{:else}
|
||||
<div class="text-sm">
|
||||
{$t('individual_share')} |
|
||||
<span class="text-immich-primary dark:text-immich-dark-primary">{editingLink.description || ''}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="mb-2 mt-4">
|
||||
<p class="text-xs">{$t('link_options').toUpperCase()}</p>
|
||||
</div>
|
||||
<div class="rounded-lg bg-gray-100 p-4 dark:bg-black/40 overflow-y-auto">
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('description')}
|
||||
bind:value={description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('password')}
|
||||
bind:value={password}
|
||||
disabled={!enablePassword}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={enablePassword} title={$t('require_password')} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={showMetadata} title={$t('show_metadata')} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch
|
||||
bind:checked={allowDownload}
|
||||
title={$t('allow_public_user_to_download')}
|
||||
disabled={!showMetadata}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={allowUpload} title={$t('allow_public_user_to_upload')} />
|
||||
</div>
|
||||
|
||||
{#if editingLink}
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={shouldChangeExpirationTime} title={$t('change_expiration_time')} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-3">
|
||||
<SettingSelect
|
||||
bind:value={expirationOption}
|
||||
options={expiredDateOptions}
|
||||
label={$t('expire_after')}
|
||||
disabled={editingLink && !shouldChangeExpirationTime}
|
||||
number={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
{#if editingLink}
|
||||
<Button fullWidth onclick={handleEditLink}>{$t('confirm')}</Button>
|
||||
{:else}
|
||||
<Button fullWidth onclick={handleCreateSharedLink}>{$t('create_link')}</Button>
|
||||
{/if}
|
||||
</ModalFooter>
|
||||
</Modal>
|
@ -7,7 +7,6 @@
|
||||
import AlbumSummary from '$lib/components/album-page/album-summary.svelte';
|
||||
import AlbumTitle from '$lib/components/album-page/album-title.svelte';
|
||||
import ShareInfoModal from '$lib/components/album-page/share-info-modal.svelte';
|
||||
import UserSelectionModal from '$lib/components/album-page/user-selection-modal.svelte';
|
||||
import ActivityStatus from '$lib/components/asset-viewer/activity-status.svelte';
|
||||
import ActivityViewer from '$lib/components/asset-viewer/activity-viewer.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
@ -29,7 +28,6 @@
|
||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController,
|
||||
@ -37,12 +35,17 @@
|
||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||
import { AlbumPageViewMode, AppRoute } from '$lib/constants';
|
||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
|
||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { handlePromiseError, makeSharedLinkUrl } from '$lib/utils';
|
||||
import { confirmAlbumDelete } from '$lib/utils/album-utils';
|
||||
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
@ -178,10 +181,6 @@
|
||||
|
||||
const handleEscape = async () => {
|
||||
assetStore.suspendTransitions = true;
|
||||
if (viewMode === AlbumPageViewMode.SELECT_USERS) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
if (viewMode === AlbumPageViewMode.SELECT_THUMBNAIL) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
@ -190,10 +189,6 @@
|
||||
await handleCloseSelectAssets();
|
||||
return;
|
||||
}
|
||||
if (viewMode === AlbumPageViewMode.LINK_SHARING) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
}
|
||||
if (viewMode === AlbumPageViewMode.OPTIONS) {
|
||||
viewMode = AlbumPageViewMode.VIEW;
|
||||
return;
|
||||
@ -423,6 +418,31 @@
|
||||
const currentAssetIntersection = $derived(
|
||||
viewMode === AlbumPageViewMode.SELECT_ASSETS ? timelineInteraction : assetInteraction,
|
||||
);
|
||||
|
||||
const handleShare = async () => {
|
||||
const result = await modalManager.show(AlbumShareModal, { album });
|
||||
|
||||
switch (result?.action) {
|
||||
case 'sharedLink': {
|
||||
await handleShareLink();
|
||||
return;
|
||||
}
|
||||
|
||||
case 'sharedUsers': {
|
||||
await handleAddUsers(result.data);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleShareLink = async () => {
|
||||
const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: album.id });
|
||||
|
||||
if (sharedLink) {
|
||||
const url = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key);
|
||||
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: url });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex overflow-hidden" use:scrollMemoryClearer={{ routeStartsWith: AppRoute.ALBUMS }}>
|
||||
@ -496,11 +516,7 @@
|
||||
{/if}
|
||||
|
||||
{#if isOwned}
|
||||
<CircleIconButton
|
||||
title={$t('share')}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
icon={mdiShareVariantOutline}
|
||||
/>
|
||||
<CircleIconButton title={$t('share')} onclick={handleShare} icon={mdiShareVariantOutline} />
|
||||
{/if}
|
||||
|
||||
<AlbumMap {album} />
|
||||
@ -530,12 +546,7 @@
|
||||
{/if}
|
||||
|
||||
{#if isCreatingSharedAlbum && album.albumUsers.length === 0}
|
||||
<Button
|
||||
size="sm"
|
||||
rounded="lg"
|
||||
disabled={album.assetCount === 0}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
>
|
||||
<Button size="sm" rounded="lg" disabled={album.assetCount === 0} onclick={handleShare}>
|
||||
{$t('share')}
|
||||
</Button>
|
||||
{/if}
|
||||
@ -619,7 +630,7 @@
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiLink}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
|
||||
onclick={handleShareLink}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@ -651,7 +662,7 @@
|
||||
color="gray"
|
||||
size="20"
|
||||
icon={mdiPlus}
|
||||
onclick={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
onclick={handleShare}
|
||||
title={$t('add_more_users')}
|
||||
/>
|
||||
{/if}
|
||||
@ -714,18 +725,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if viewMode === AlbumPageViewMode.SELECT_USERS}
|
||||
<UserSelectionModal
|
||||
{album}
|
||||
onSelect={handleAddUsers}
|
||||
onShare={() => (viewMode = AlbumPageViewMode.LINK_SHARING)}
|
||||
onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if viewMode === AlbumPageViewMode.LINK_SHARING}
|
||||
<CreateSharedLinkModal albumId={album.id} onClose={() => (viewMode = AlbumPageViewMode.VIEW)} />
|
||||
{/if}
|
||||
|
||||
{#if viewMode === AlbumPageViewMode.VIEW_USERS}
|
||||
<ShareInfoModal
|
||||
@ -749,7 +748,7 @@
|
||||
onRefreshAlbum={refreshAlbum}
|
||||
onClose={() => (viewMode = AlbumPageViewMode.VIEW)}
|
||||
onToggleEnabledActivity={handleToggleEnableActivity}
|
||||
onShowSelectSharedUser={() => (viewMode = AlbumPageViewMode.SELECT_USERS)}
|
||||
onShowSelectSharedUser={handleShare}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
@ -3,7 +3,6 @@
|
||||
import { page } from '$app/state';
|
||||
import GroupTab from '$lib/components/elements/group-tab.svelte';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import {
|
||||
notificationController,
|
||||
@ -11,6 +10,7 @@
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getAllSharedLinks, removeSharedLink, SharedLinkType, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { onMount } from 'svelte';
|
||||
@ -113,7 +113,7 @@
|
||||
{/if}
|
||||
|
||||
{#if sharedLink}
|
||||
<CreateSharedLinkModal editingLink={sharedLink} onClose={handleEditDone} />
|
||||
<SharedLinkCreateModal editingLink={sharedLink} onClose={handleEditDone} />
|
||||
{/if}
|
||||
</div>
|
||||
</UserPageLayout>
|
||||
|
Loading…
x
Reference in New Issue
Block a user