mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 09:13:13 +03:00
refactor: view shared link (#23766)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
21
web/src/lib/services/shared-link.service.ts
Normal file
21
web/src/lib/services/shared-link.service.ts
Normal 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));
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user