diff --git a/i18n/en.json b/i18n/en.json index 454b776aac..e9d4652d4e 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1371,6 +1371,7 @@ "view_next_asset": "View next asset", "view_previous_asset": "View previous asset", "view_stack": "View Stack", + "view_qr_code": "View QR code", "visibility_changed": "Visibility changed for {count, plural, one {# person} other {# people}}", "waiting": "Waiting", "warning": "Warning", diff --git a/web/src/lib/components/album-page/album-shared-link.svelte b/web/src/lib/components/album-page/album-shared-link.svelte index 2b02eb8e07..b56aa11b6d 100644 --- a/web/src/lib/components/album-page/album-shared-link.svelte +++ b/web/src/lib/components/album-page/album-shared-link.svelte @@ -1,17 +1,20 @@ <script lang="ts"> + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import SharedLinkCopy from '$lib/components/sharedlinks-page/actions/shared-link-copy.svelte'; import { locale } from '$lib/stores/preferences.store'; import type { AlbumResponseDto, SharedLinkResponseDto } from '@immich/sdk'; import { Text } from '@immich/ui'; + import { mdiQrcode } from '@mdi/js'; import { DateTime } from 'luxon'; import { t } from 'svelte-i18n'; type Props = { album: AlbumResponseDto; sharedLink: SharedLinkResponseDto; + onViewQrCode: () => void; }; - const { album, sharedLink }: Props = $props(); + const { album, sharedLink, onViewQrCode }: Props = $props(); const getShareProperties = () => [ @@ -37,5 +40,8 @@ <Text size="small">{sharedLink.description || album.albumName}</Text> <Text size="tiny" color="muted">{getShareProperties()}</Text> </div> - <SharedLinkCopy link={sharedLink} /> + <div class="flex"> + <CircleIconButton title={$t('view_qr_code')} icon={mdiQrcode} onclick={onViewQrCode} /> + <SharedLinkCopy link={sharedLink} /> + </div> </div> diff --git a/web/src/lib/components/album-page/user-selection-modal.svelte b/web/src/lib/components/album-page/user-selection-modal.svelte index cd454f515f..1496c1ce66 100644 --- a/web/src/lib/components/album-page/user-selection-modal.svelte +++ b/web/src/lib/components/album-page/user-selection-modal.svelte @@ -3,7 +3,10 @@ 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 { serverConfig } from '$lib/stores/server-config.store'; + import { makeSharedLinkUrl } from '$lib/utils'; import { AlbumUserRole, getAllSharedLinks, @@ -31,6 +34,11 @@ let users: UserResponseDto[] = $state([]); let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({}); + let sharedLinkUrl = $state(''); + const handleViewQrCode = (sharedLink: SharedLinkResponseDto) => { + sharedLinkUrl = makeSharedLinkUrl($serverConfig.externalDomain, sharedLink.key); + }; + const roleOptions: Array<{ title: string; value: AlbumUserRole | 'none'; icon?: string }> = [ { title: $t('role_editor'), value: AlbumUserRole.Editor, icon: mdiPencil }, { title: $t('role_viewer'), value: AlbumUserRole.Viewer, icon: mdiEye }, @@ -68,59 +76,24 @@ }; </script> -<FullScreenModal title={$t('share')} showLogo {onClose}> - {#if Object.keys(selectedUsers).length > 0} - <div class="mb-2 py-2 sticky"> - <p class="text-xs font-medium">{$t('selected')}</p> - <div class="my-2"> - {#each Object.values(selectedUsers) as { user } (user.id)} - {#key user.id} - <div class="flex place-items-center gap-4 p-4"> - <div - class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success" - > - <Icon path={mdiCheck} size={24} /> - </div> +{#if sharedLinkUrl} + <QrCodeModal title={$t('view_link')} onClose={() => (sharedLinkUrl = '')} value={sharedLinkUrl} /> +{:else} + <FullScreenModal title={$t('share')} showLogo {onClose}> + {#if Object.keys(selectedUsers).length > 0} + <div class="mb-2 py-2 sticky"> + <p class="text-xs font-medium">{$t('selected')}</p> + <div class="my-2"> + {#each Object.values(selectedUsers) as { user } (user.id)} + {#key user.id} + <div class="flex place-items-center gap-4 p-4"> + <div + class="flex h-10 w-10 items-center justify-center rounded-full border bg-immich-dark-success text-3xl text-white dark:border-immich-dark-gray dark:bg-immich-dark-success" + > + <Icon path={mdiCheck} size={24} /> + </div> - <!-- <UserAvatar {user} size="md" /> --> - <div class="text-left flex-grow"> - <p class="text-immich-fg dark:text-immich-dark-fg"> - {user.name} - </p> - <p class="text-xs"> - {user.email} - </p> - </div> - - <Dropdown - title={$t('role')} - options={roleOptions} - render={({ title, icon }) => ({ title, icon })} - onSelect={({ value }) => handleChangeRole(user, value)} - /> - </div> - {/key} - {/each} - </div> - </div> - {/if} - - {#if users.length + Object.keys(selectedUsers).length === 0} - <p class="p-5 text-sm"> - {$t('album_share_no_users')} - </p> - {/if} - - <div class="immich-scrollbar max-h-[500px] overflow-y-auto"> - {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} - <Text>{$t('users')}</Text> - - <div class="my-2"> - {#each users as user (user.id)} - {#if !Object.keys(selectedUsers).includes(user.id)} - <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"> - <button type="button" onclick={() => handleToggle(user)} class="flex w-full place-items-center gap-4 p-4"> - <UserAvatar {user} size="md" /> + <!-- <UserAvatar {user} size="md" /> --> <div class="text-left flex-grow"> <p class="text-immich-fg dark:text-immich-dark-fg"> {user.name} @@ -129,44 +102,87 @@ {user.email} </p> </div> - </button> - </div> - {/if} - {/each} + + <Dropdown + title={$t('role')} + options={roleOptions} + render={({ title, icon }) => ({ title, icon })} + onSelect={({ value }) => handleChangeRole(user, value)} + /> + </div> + {/key} + {/each} + </div> </div> {/if} - </div> - {#if users.length > 0} - <div class="py-3"> - <Button - size="small" - fullWidth - shape="round" - disabled={Object.keys(selectedUsers).length === 0} - onclick={() => - onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))} - >{$t('add')}</Button - > + {#if users.length + Object.keys(selectedUsers).length === 0} + <p class="p-5 text-sm"> + {$t('album_share_no_users')} + </p> + {/if} + + <div class="immich-scrollbar max-h-[500px] overflow-y-auto"> + {#if users.length > 0 && users.length !== Object.keys(selectedUsers).length} + <Text>{$t('users')}</Text> + + <div class="my-2"> + {#each users as user (user.id)} + {#if !Object.keys(selectedUsers).includes(user.id)} + <div class="flex place-items-center transition-all hover:bg-gray-200 dark:hover:bg-gray-700 rounded-xl"> + <button + type="button" + onclick={() => handleToggle(user)} + class="flex w-full place-items-center gap-4 p-4" + > + <UserAvatar {user} size="md" /> + <div class="text-left flex-grow"> + <p class="text-immich-fg dark:text-immich-dark-fg"> + {user.name} + </p> + <p class="text-xs"> + {user.email} + </p> + </div> + </button> + </div> + {/if} + {/each} + </div> + {/if} </div> - {/if} - <hr class="my-4" /> - - <Stack gap={6}> - {#if sharedLinks.length > 0} - <div class="flex justify-between items-center"> - <Text>{$t('shared_links')}</Text> - <Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link> + {#if users.length > 0} + <div class="py-3"> + <Button + size="small" + fullWidth + shape="round" + disabled={Object.keys(selectedUsers).length === 0} + onclick={() => + onSelect(Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })))} + >{$t('add')}</Button + > </div> - - <Stack gap={4}> - {#each sharedLinks as sharedLink (sharedLink.id)} - <AlbumSharedLink {album} {sharedLink} /> - {/each} - </Stack> {/if} - <Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={onShare}>{$t('create_link')}</Button> - </Stack> -</FullScreenModal> + <hr class="my-4" /> + + <Stack gap={6}> + {#if sharedLinks.length > 0} + <div class="flex justify-between items-center"> + <Text>{$t('shared_links')}</Text> + <Link href={AppRoute.SHARED_LINKS} class="text-sm">{$t('view_all')}</Link> + </div> + + <Stack gap={4}> + {#each sharedLinks as sharedLink (sharedLink.id)} + <AlbumSharedLink {album} {sharedLink} onViewQrCode={() => handleViewQrCode(sharedLink)} /> + {/each} + </Stack> + {/if} + + <Button leadingIcon={mdiLink} size="small" shape="round" fullWidth onclick={onShare}>{$t('create_link')}</Button> + </Stack> + </FullScreenModal> +{/if} 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 index 19bef9c7db..a87ca3da4a 100644 --- 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 @@ -1,20 +1,20 @@ <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 { copyToClipboard, makeSharedLinkUrl } from '$lib/utils'; + import { makeSharedLinkUrl } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk'; - import { Button, HStack, IconButton, Input } from '@immich/ui'; - import { mdiContentCopy, mdiLink } from '@mdi/js'; + 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'; - import QRCode from '$lib/components/shared-components/qrcode.svelte'; interface Props { onClose: () => void; @@ -41,7 +41,6 @@ let password = $state(''); let shouldChangeExpirationTime = $state(false); let enablePassword = $state(false); - let modalWidth = $state(0); const expirationOptions: [number, Intl.RelativeTimeFormatUnit][] = [ [30, 'minutes'], @@ -248,26 +247,5 @@ {/snippet} </FullScreenModal> {:else} - <FullScreenModal title={getTitle()} icon={mdiLink} {onClose}> - <div class="w-full"> - <div class="w-full py-2 px-10"> - <div bind:clientWidth={modalWidth} class="w-full"> - <QRCode value={sharedLink} width={modalWidth} /> - </div> - </div> - <HStack class="w-full pt-3" gap={1}> - <Input bind:value={sharedLink} disabled class="flex flex-row" /> - <div> - <IconButton - variant="ghost" - shape="round" - color="secondary" - icon={mdiContentCopy} - onclick={() => (sharedLink ? copyToClipboard(sharedLink) : '')} - aria-label={$t('copy_link_to_clipboard')} - /> - </div> - </HStack> - </div> - </FullScreenModal> + <QrCodeModal title={$t('view_link')} {onClose} value={sharedLink} /> {/if} diff --git a/web/src/lib/components/shared-components/qr-code-modal.svelte b/web/src/lib/components/shared-components/qr-code-modal.svelte new file mode 100644 index 0000000000..166b2837ee --- /dev/null +++ b/web/src/lib/components/shared-components/qr-code-modal.svelte @@ -0,0 +1,41 @@ +<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 { mdiContentCopy, mdiLink } from '@mdi/js'; + import { t } from 'svelte-i18n'; + + type Props = { + title: string; + onClose: () => void; + value: string; + }; + + let { onClose, title, value }: Props = $props(); + + let modalWidth = $state(0); +</script> + +<FullScreenModal {title} icon={mdiLink} {onClose}> + <div class="w-full"> + <div class="w-full py-2 px-10"> + <div bind:clientWidth={modalWidth} class="w-full"> + <QRCode {value} width={modalWidth} /> + </div> + </div> + <HStack class="w-full pt-3" gap={1}> + <Input bind:value disabled class="flex flex-row" /> + <div> + <IconButton + variant="ghost" + shape="round" + color="secondary" + icon={mdiContentCopy} + onclick={() => (value ? copyToClipboard(value) : '')} + aria-label={$t('copy_link_to_clipboard')} + /> + </div> + </HStack> + </div> +</FullScreenModal>