diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 0696a937cc..befe2019a1 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -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} diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte index 6fd5aa456e..f32d3e7515 100644 --- a/web/src/lib/components/asset-viewer/actions/share-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte @@ -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')} /> diff --git a/web/src/lib/components/photos-page/actions/create-shared-link.svelte b/web/src/lib/components/photos-page/actions/create-shared-link.svelte index 1b99627ea9..05baf822c1 100644 --- a/web/src/lib/components/photos-page/actions/create-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/create-shared-link.svelte @@ -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} /> diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte deleted file mode 100644 index a87ca3da4a..0000000000 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ /dev/null @@ -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} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 285bccae30..f13d008a3c 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -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 diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/modals/AlbumShareModal.svelte similarity index 89% rename from web/src/lib/components/album-page/user-selection-modal.svelte rename to web/src/lib/modals/AlbumShareModal.svelte index 9ee7cc550d..56e9a92305 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/modals/AlbumShareModal.svelte @@ -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} diff --git a/web/src/lib/components/shared-components/qr-code-modal.svelte b/web/src/lib/modals/QrCodeModal.svelte similarity index 75% rename from web/src/lib/components/shared-components/qr-code-modal.svelte rename to web/src/lib/modals/QrCodeModal.svelte index 166b2837ee..c56fda801b 100644 --- a/web/src/lib/components/shared-components/qr-code-modal.svelte +++ b/web/src/lib/modals/QrCodeModal.svelte @@ -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> diff --git a/web/src/lib/modals/SharedLinkCreateModal.svelte b/web/src/lib/modals/SharedLinkCreateModal.svelte new file mode 100644 index 0000000000..b4b9eaf98f --- /dev/null +++ b/web/src/lib/modals/SharedLinkCreateModal.svelte @@ -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> diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7f996396d8..2331ae01b1 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -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} diff --git a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte index 6fd1f17ecc..1c1e1cfbd4 100644 --- a/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte +++ b/web/src/routes/(user)/shared-links/[[id=id]]/+page.svelte @@ -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>