refactor: view shared link (#23766)

This commit is contained in:
Jason Rasmussen
2025-11-10 12:21:26 -05:00
committed by GitHub
parent a4e65a7ea8
commit 45304f1211
13 changed files with 74 additions and 76 deletions

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import SharedLinkCopy from '$lib/components/sharedlinks-page/actions/shared-link-copy.svelte';
import { handleViewSharedLinkQrCode } from '$lib/services/shared-link.service';
import { locale } from '$lib/stores/preferences.store';
import type { AlbumResponseDto, SharedLinkResponseDto } from '@immich/sdk';
import { IconButton, Text } from '@immich/ui';
@@ -10,10 +11,9 @@
type Props = {
album: AlbumResponseDto;
sharedLink: SharedLinkResponseDto;
onViewQrCode: () => void;
};
const { album, sharedLink, onViewQrCode }: Props = $props();
const { album, sharedLink }: Props = $props();
const getShareProperties = () =>
[
@@ -46,8 +46,8 @@
color="secondary"
variant="ghost"
icon={mdiQrcode}
onclick={onViewQrCode}
onclick={() => handleViewSharedLinkQrCode(sharedLink)}
/>
<SharedLinkCopy link={sharedLink} />
<SharedLinkCopy {sharedLink} />
</div>
</div>

View File

@@ -9,9 +9,9 @@
import { AppRoute } from '$lib/constants';
import AlbumEditModal from '$lib/modals/AlbumEditModal.svelte';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { handleConfirmAlbumDelete, handleDownloadAlbum } from '$lib/services/album.service';
import { handleViewSharedLinkQrCode } from '$lib/services/shared-link.service';
import {
AlbumFilter,
AlbumGroupBy,
@@ -23,7 +23,6 @@
} from '$lib/stores/preferences.store';
import { user } from '$lib/stores/user.store';
import { userInteraction } from '$lib/stores/user.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
import { getSelectedAlbumGroupOption, sortAlbums, stringToSortOrder, type AlbumGroup } from '$lib/utils/album-utils';
import type { ContextMenuPosition } from '$lib/utils/context-menu';
import { handleError } from '$lib/utils/handle-error';
@@ -259,7 +258,7 @@
const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: selectedAlbum.id });
if (sharedLink) {
handleSharedLinkCreated(selectedAlbum);
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
await handleViewSharedLinkQrCode(sharedLink);
}
break;
}

View File

@@ -1,7 +1,6 @@
<script lang="ts">
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
import { handleViewSharedLinkQrCode } from '$lib/services/shared-link.service';
import type { AssetResponseDto } from '@immich/sdk';
import { IconButton, modalManager } from '@immich/ui';
import { mdiShareVariantOutline } from '@mdi/js';
@@ -15,9 +14,8 @@
const handleClick = async () => {
const sharedLink = await modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] });
if (sharedLink) {
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
await handleViewSharedLinkQrCode(sharedLink);
}
};
</script>

View File

@@ -1,25 +1,21 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { copyToClipboard, makeSharedLinkUrl } from '$lib/utils';
import { handleCopySharedLinkUrl } from '$lib/services/shared-link.service';
import type { SharedLinkResponseDto } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiContentCopy } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
link: SharedLinkResponseDto;
sharedLink: SharedLinkResponseDto;
menuItem?: boolean;
}
let { link, menuItem = false }: Props = $props();
const handleCopy = async () => {
await copyToClipboard(makeSharedLinkUrl(link));
};
let { sharedLink, menuItem = false }: Props = $props();
</script>
{#if menuItem}
<MenuOption text={$t('copy_link')} icon={mdiContentCopy} onClick={handleCopy} />
<MenuOption text={$t('copy_link')} icon={mdiContentCopy} onClick={() => handleCopySharedLinkUrl(sharedLink)} />
{:else}
<IconButton
color="secondary"
@@ -27,6 +23,6 @@
variant="ghost"
aria-label={$t('copy_link')}
icon={mdiContentCopy}
onclick={handleCopy}
onclick={() => handleCopySharedLinkUrl(sharedLink)}
/>
{/if}

View File

@@ -10,7 +10,7 @@ vi.mock('$lib/utils');
describe('ShareCover component', () => {
it('renders an image when the shared link is an album', () => {
const component = render(ShareCover, {
link: sharedLinkFactory.build({ album: albumFactory.build({ albumName: '123' }) }),
sharedLink: sharedLinkFactory.build({ album: albumFactory.build({ albumName: '123' }) }),
preload: false,
class: 'text',
});
@@ -23,7 +23,7 @@ describe('ShareCover component', () => {
it('renders an image when the shared link is an individual share', () => {
vi.mocked(getAssetThumbnailUrl).mockReturnValue('/asdf');
const component = render(ShareCover, {
link: sharedLinkFactory.build({ assets: [assetFactory.build({ id: 'someId' })] }),
sharedLink: sharedLinkFactory.build({ assets: [assetFactory.build({ id: 'someId' })] }),
preload: false,
class: 'text',
});
@@ -37,7 +37,7 @@ describe('ShareCover component', () => {
it('renders an image when the shared link has no album or assets', () => {
const component = render(ShareCover, {
link: sharedLinkFactory.build(),
sharedLink: sharedLinkFactory.build(),
preload: false,
class: 'text',
});
@@ -48,9 +48,9 @@ describe('ShareCover component', () => {
});
it.skip('renders fallback image when asset is not resized', () => {
const link = sharedLinkFactory.build({ assets: [assetFactory.build()] });
const sharedLink = sharedLinkFactory.build({ assets: [assetFactory.build()] });
render(ShareCover, {
link,
sharedLink,
preload: false,
});

View File

@@ -1,29 +1,29 @@
<script lang="ts">
import type { SharedLinkResponseDto } from '@immich/sdk';
import AlbumCover from '$lib/components/album-page/album-cover.svelte';
import NoCover from '$lib/components/sharedlinks-page/covers/no-cover.svelte';
import AssetCover from '$lib/components/sharedlinks-page/covers/asset-cover.svelte';
import NoCover from '$lib/components/sharedlinks-page/covers/no-cover.svelte';
import { getAssetThumbnailUrl } from '$lib/utils';
import type { SharedLinkResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
interface Props {
link: SharedLinkResponseDto;
sharedLink: SharedLinkResponseDto;
preload?: boolean;
class?: string;
}
let { link, preload = false, class: className = '' }: Props = $props();
let { sharedLink, preload = false, class: className = '' }: Props = $props();
</script>
<div class="relative shrink-0 size-24">
{#if link?.album}
<AlbumCover album={link.album} class={className} {preload} />
{:else if link.assets[0]}
{#if sharedLink?.album}
<AlbumCover album={sharedLink.album} class={className} {preload} />
{:else if sharedLink.assets[0]}
<AssetCover
alt={$t('individual_share')}
class={className}
{preload}
src={getAssetThumbnailUrl(link.assets[0].id)}
src={getAssetThumbnailUrl(sharedLink.assets[0].id)}
/>
{:else}
<NoCover alt={$t('unnamed_share')} class={className} {preload} />

View File

@@ -13,14 +13,14 @@
import { t } from 'svelte-i18n';
interface Props {
link: SharedLinkResponseDto;
sharedLink: SharedLinkResponseDto;
onDelete: () => void;
}
let { link, onDelete }: Props = $props();
let { sharedLink, onDelete }: Props = $props();
let now = DateTime.now();
let expiresAt = $derived(link.expiresAt ? DateTime.fromISO(link.expiresAt) : undefined);
let expiresAt = $derived(sharedLink.expiresAt ? DateTime.fromISO(sharedLink.expiresAt) : undefined);
let isExpired = $derived(expiresAt ? now > expiresAt : false);
const getCountDownExpirationDate = (expiresAtDate: DateTime, now: DateTime) => {
@@ -41,10 +41,10 @@
>
<svelte:element
this={isExpired ? 'div' : 'a'}
href={isExpired ? undefined : `${AppRoute.SHARE}/${link.key}`}
href={isExpired ? undefined : `${AppRoute.SHARE}/${sharedLink.key}`}
class="flex gap-4 w-full py-4"
>
<ShareCover class="transition-all duration-300 hover:shadow-lg" {link} />
<ShareCover class="transition-all duration-300 hover:shadow-lg" {sharedLink} />
<div class="flex flex-col justify-between">
<div class="info-top">
@@ -62,34 +62,34 @@
<div class="text-sm pb-2">
<p class="flex place-items-center gap-2 text-primary break-all uppercase">
{#if link.type === SharedLinkType.Album}
{link.album?.albumName}
{:else if link.type === SharedLinkType.Individual}
{#if sharedLink.type === SharedLinkType.Album}
{sharedLink.album?.albumName}
{:else if sharedLink.type === SharedLinkType.Individual}
{$t('individual_share')}
{/if}
</p>
<p class="text-sm">{link.description ?? ''}</p>
<p class="text-sm">{sharedLink.description ?? ''}</p>
</div>
</div>
<div class="flex flex-wrap gap-2 text-xl">
{#if link.allowUpload}
{#if sharedLink.allowUpload}
<Badge rounded="full"><span class="text-xs px-1">{$t('upload')}</span></Badge>
{/if}
{#if link.allowDownload}
{#if sharedLink.allowDownload}
<Badge rounded="full"><span class="text-xs px-1">{$t('download')}</span></Badge>
{/if}
{#if link.showMetadata}
{#if sharedLink.showMetadata}
<Badge rounded="full"><span class="uppercase text-xs px-1">{$t('exif')}</span></Badge>
{/if}
{#if link.password}
{#if sharedLink.password}
<Badge rounded="full"><span class="text-xs px-1">{$t('password')}</span></Badge>
{/if}
{#if link.slug}
{#if sharedLink.slug}
<Badge rounded="full"><span class="text-xs px-1">{$t('custom_url')}</span></Badge>
{/if}
</div>
@@ -97,8 +97,8 @@
</svelte:element>
<div class="flex flex-auto flex-col place-content-center place-items-end text-end ms-4">
<div class="sm:flex hidden">
<SharedLinkEdit sharedLink={link} />
<SharedLinkCopy {link} />
<SharedLinkEdit {sharedLink} />
<SharedLinkCopy {sharedLink} />
<SharedLinkDelete {onDelete} />
</div>
@@ -110,8 +110,8 @@
size="large"
hideContent
>
<SharedLinkEdit menuItem sharedLink={link} />
<SharedLinkCopy menuItem {link} />
<SharedLinkEdit menuItem {sharedLink} />
<SharedLinkCopy menuItem {sharedLink} />
<SharedLinkDelete menuItem {onDelete} />
</ButtonContextMenu>
</div>

View File

@@ -1,8 +1,7 @@
<script lang="ts">
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
import { handleViewSharedLinkQrCode } from '$lib/services/shared-link.service';
import { IconButton, modalManager } from '@immich/ui';
import { mdiShareVariantOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -15,7 +14,7 @@
});
if (sharedLink) {
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
await handleViewSharedLinkQrCode(sharedLink);
}
};
</script>

View File

@@ -2,8 +2,6 @@
import AlbumSharedLink from '$lib/components/album-page/album-shared-link.svelte';
import { AppRoute } from '$lib/constants';
import Dropdown from '$lib/elements/Dropdown.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import { makeSharedLinkUrl } from '$lib/utils';
import {
AlbumUserRole,
getAllSharedLinks,
@@ -13,7 +11,7 @@
type SharedLinkResponseDto,
type UserResponseDto,
} from '@immich/sdk';
import { Button, Icon, Link, Modal, ModalBody, modalManager, Stack, Text } from '@immich/ui';
import { Button, Icon, Link, Modal, ModalBody, Stack, Text } from '@immich/ui';
import { mdiCheck, mdiEye, mdiLink, mdiPencil } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -29,13 +27,6 @@
let users: UserResponseDto[] = $state([]);
let selectedUsers: Record<string, { user: UserResponseDto; role: AlbumUserRole }> = $state({});
const handleViewQrCode = async (sharedLink: SharedLinkResponseDto) => {
await modalManager.show(QrCodeModal, {
title: $t('view_link'),
value: makeSharedLinkUrl(sharedLink),
});
};
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 },
@@ -174,7 +165,7 @@
<Stack gap={4}>
{#each sharedLinks as sharedLink (sharedLink.id)}
<AlbumSharedLink {album} {sharedLink} onViewQrCode={() => handleViewQrCode(sharedLink)} />
<AlbumSharedLink {album} {sharedLink} />
{/each}
</Stack>
{/if}

View File

@@ -0,0 +1,21 @@
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import { serverConfig } from '$lib/stores/server-config.store';
import { copyToClipboard } from '$lib/utils';
import { getFormatter } from '$lib/utils/i18n';
import type { SharedLinkResponseDto } from '@immich/sdk';
import { modalManager } from '@immich/ui';
import { get } from 'svelte/store';
const makeSharedLinkUrl = (sharedLink: SharedLinkResponseDto) => {
const path = sharedLink.slug ? `s/${sharedLink.slug}` : `share/${sharedLink.key}`;
return new URL(path, get(serverConfig).externalDomain || globalThis.location.origin).href;
};
export const handleViewSharedLinkQrCode = async (sharedLink: SharedLinkResponseDto) => {
const $t = await getFormatter();
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
};
export const handleCopySharedLinkUrl = async (sharedLink: SharedLinkResponseDto) => {
await copyToClipboard(makeSharedLinkUrl(sharedLink));
};

View File

@@ -1,7 +1,6 @@
import { defaultLang, langs, locales } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { lang } from '$lib/stores/preferences.store';
import { serverConfig } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import {
AssetJobName,
@@ -269,11 +268,6 @@ export const copyToClipboard = async (secret: string) => {
}
};
export const makeSharedLinkUrl = (sharedLink: SharedLinkResponseDto) => {
const path = sharedLink.slug ? `s/${sharedLink.slug}` : `share/${sharedLink.key}`;
return new URL(path, get(serverConfig).externalDomain || globalThis.location.origin).href;
};
export const oauth = {
isCallback: (location: Location) => {
const search = location.search;

View File

@@ -34,15 +34,15 @@
import AlbumOptionsModal from '$lib/modals/AlbumOptionsModal.svelte';
import AlbumShareModal from '$lib/modals/AlbumShareModal.svelte';
import AlbumUsersModal from '$lib/modals/AlbumUsersModal.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
import { handleConfirmAlbumDelete, handleDownloadAlbum } from '$lib/services/album.service';
import { handleViewSharedLinkQrCode } from '$lib/services/shared-link.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { featureFlags } 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, makeSharedLinkUrl } from '$lib/utils';
import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
@@ -388,7 +388,7 @@
const sharedLink = await modalManager.show(SharedLinkCreateModal, { albumId: album.id });
if (sharedLink) {
await refreshAlbum();
await modalManager.show(QrCodeModal, { title: $t('view_link'), value: makeSharedLinkUrl(sharedLink) });
await handleViewSharedLinkQrCode(sharedLink);
}
};

View File

@@ -109,8 +109,8 @@
</div>
{:else}
<div class="flex flex-col gap-2">
{#each filteredSharedLinks as link (link.id)}
<SharedLinkCard {link} onDelete={() => handleDeleteLink(link.id)} />
{#each filteredSharedLinks as sharedLink (sharedLink.id)}
<SharedLinkCard {sharedLink} onDelete={() => handleDeleteLink(sharedLink.id)} />
{/each}
</div>
{/if}