From e4311da1a4cafed1ca6217e170fe683ff632a6e1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 5 Jan 2026 10:03:35 -0500 Subject: [PATCH] fix: shared-link-mapper (#24794) --- e2e/src/api/specs/shared-link.e2e-spec.ts | 38 +-- server/src/dtos/shared-link.dto.ts | 41 +-- .../src/services/shared-link.service.spec.ts | 3 +- server/src/services/shared-link.service.ts | 15 +- server/test/fixtures/shared-link.stub.ts | 267 +++++------------- .../components/album-page/albums-list.svelte | 24 +- .../asset-viewer/actions/share-action.svelte | 26 -- .../asset-viewer/asset-viewer-nav-bar.svelte | 10 +- .../lib/modals/SharedLinkCreateModal.svelte | 6 +- web/src/lib/services/asset.service.ts | 19 +- web/src/lib/services/shared-link.service.ts | 7 +- 11 files changed, 152 insertions(+), 304 deletions(-) delete mode 100644 web/src/lib/components/asset-viewer/actions/share-action.svelte diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index f25a54786a..8c15a14da5 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -20,7 +20,6 @@ describe('/shared-links', () => { let user1: LoginResponseDto; let user2: LoginResponseDto; let album: AlbumResponseDto; - let metadataAlbum: AlbumResponseDto; let deletedAlbum: AlbumResponseDto; let linkWithDeletedAlbum: SharedLinkResponseDto; let linkWithPassword: SharedLinkResponseDto; @@ -41,18 +40,9 @@ describe('/shared-links', () => { [asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]); - [album, deletedAlbum, metadataAlbum] = await Promise.all([ + [album, deletedAlbum] = await Promise.all([ createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }), createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }), - createAlbum( - { - createAlbumDto: { - albumName: 'metadata album', - assetIds: [asset1.id], - }, - }, - { headers: asBearerAuth(user1.accessToken) }, - ), ]); [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = @@ -75,14 +65,14 @@ describe('/shared-links', () => { password: 'foo', }), utils.createSharedLink(user1.accessToken, { - type: SharedLinkType.Album, - albumId: metadataAlbum.id, + type: SharedLinkType.Individual, + assetIds: [asset1.id], showMetadata: true, - slug: 'metadata-album', + slug: 'metadata-slug', }), utils.createSharedLink(user1.accessToken, { - type: SharedLinkType.Album, - albumId: metadataAlbum.id, + type: SharedLinkType.Individual, + assetIds: [asset1.id], showMetadata: false, }), ]); @@ -95,9 +85,7 @@ describe('/shared-links', () => { const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`); expect(resp.status).toBe(200); expect(resp.header['content-type']).toContain('text/html'); - expect(resp.text).toContain( - ``, - ); + expect(resp.text).toContain(``); }); it('should have correct asset count in meta tag for empty album', async () => { @@ -144,9 +132,7 @@ describe('/shared-links', () => { const resp = await request(baseUrl).get(`/s/${linkWithMetadata.slug}`); expect(resp.status).toBe(200); expect(resp.header['content-type']).toContain('text/html'); - expect(resp.text).toContain( - ``, - ); + expect(resp.text).toContain(``); }); }); @@ -271,12 +257,12 @@ describe('/shared-links', () => { ); }); - it('should return metadata for album shared link', async () => { + it('should return metadata for individual shared link', async () => { const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key }); expect(status).toBe(200); - expect(body.assets).toHaveLength(0); - expect(body.album).toBeDefined(); + expect(body.assets).toHaveLength(1); + expect(body.album).not.toBeDefined(); }); it('should not return metadata for album shared link without metadata', async () => { @@ -284,7 +270,7 @@ describe('/shared-links', () => { expect(status).toBe(200); expect(body.assets).toHaveLength(1); - expect(body.album).toBeDefined(); + expect(body.album).not.toBeDefined(); const asset = body.assets[0]; expect(asset).not.toHaveProperty('exifInfo'); diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index b2aad8957e..82698ebddc 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsString } from 'class-validator'; -import _ from 'lodash'; import { SharedLink } from 'src/database'; import { HistoryBuilder, Property } from 'src/decorators'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; @@ -118,10 +117,10 @@ export class SharedLinkResponseDto { slug!: string | null; } -export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto { - const linkAssets = sharedLink.assets || []; +export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { + const assets = sharedLink.assets || []; - return { + const response = { id: sharedLink.id, description: sharedLink.description, password: sharedLink.password, @@ -130,35 +129,19 @@ export function mapSharedLink(sharedLink: SharedLink): SharedLinkResponseDto { type: sharedLink.type, createdAt: sharedLink.createdAt, expiresAt: sharedLink.expiresAt, - assets: linkAssets.map((asset) => mapAsset(asset)), - album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, - allowUpload: sharedLink.allowUpload, - allowDownload: sharedLink.allowDownload, - showMetadata: sharedLink.showExif, - slug: sharedLink.slug, - }; -} - -export function mapSharedLinkWithoutMetadata(sharedLink: SharedLink): SharedLinkResponseDto { - const linkAssets = sharedLink.assets || []; - const albumAssets = (sharedLink?.album?.assets || []).map((asset) => asset); - - const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id); - - return { - id: sharedLink.id, - description: sharedLink.description, - password: sharedLink.password, - userId: sharedLink.userId, - key: sharedLink.key.toString('base64url'), - type: sharedLink.type, - createdAt: sharedLink.createdAt, - expiresAt: sharedLink.expiresAt, - assets: assets.map((asset) => mapAsset(asset, { stripMetadata: true })), + assets: assets.map((asset) => mapAsset(asset, { stripMetadata: options.stripAssetMetadata })), album: sharedLink.album ? mapAlbumWithoutAssets(sharedLink.album) : undefined, allowUpload: sharedLink.allowUpload, allowDownload: sharedLink.allowDownload, showMetadata: sharedLink.showExif, slug: sharedLink.slug, }; + + // unless we select sharedLink.album.sharedLinks this will be wrong + if (response.album) { + response.album.hasSharedLink = true; + response.album.shared = true; + } + + return response; } diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index 062214b975..90c212650e 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -55,7 +55,8 @@ describe(SharedLinkService.name, () => { }, }); mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif); - await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.readonlyNoMetadata); + const response = await sut.getMine(authDto, {}); + expect(response.assets[0]).toMatchObject({ hasMetadata: false }); expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id); }); diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 199f0bf7a7..1440598084 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -6,7 +6,6 @@ import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { mapSharedLink, - mapSharedLinkWithoutMetadata, SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto, @@ -22,7 +21,7 @@ export class SharedLinkService extends BaseService { async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise { return this.sharedLinkRepository .getAll({ userId: auth.user.id, id, albumId }) - .then((links) => links.map((link) => mapSharedLink(link))); + .then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false }))); } async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise { @@ -31,7 +30,7 @@ export class SharedLinkService extends BaseService { } const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id); - const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif }); + const response = mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }); if (sharedLink.password) { response.token = this.validateAndRefreshToken(sharedLink, dto); } @@ -41,7 +40,7 @@ export class SharedLinkService extends BaseService { async get(auth: AuthDto, id: string): Promise { const sharedLink = await this.findOrFail(auth.user.id, id); - return this.mapToSharedLink(sharedLink, { withExif: true }); + return mapSharedLink(sharedLink, { stripAssetMetadata: false }); } async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise { @@ -81,7 +80,7 @@ export class SharedLinkService extends BaseService { slug: dto.slug || null, }); - return this.mapToSharedLink(sharedLink, { withExif: true }); + return mapSharedLink(sharedLink, { stripAssetMetadata: false }); } catch (error) { this.handleError(error); } @@ -108,7 +107,7 @@ export class SharedLinkService extends BaseService { showExif: dto.showMetadata, slug: dto.slug || null, }); - return this.mapToSharedLink(sharedLink, { withExif: true }); + return mapSharedLink(sharedLink, { stripAssetMetadata: false }); } catch (error) { this.handleError(error); } @@ -214,10 +213,6 @@ export class SharedLinkService extends BaseService { }; } - private mapToSharedLink(sharedLink: SharedLink, { withExif }: { withExif: boolean }) { - return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink); - } - private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string { const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`); const sharedLinkTokens = dto.token?.split(',') || []; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 19a62ad193..802b46a986 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -1,10 +1,7 @@ import { UserAdmin } from 'src/database'; -import { AlbumResponseDto } from 'src/dtos/album.dto'; -import { AssetResponseDto, MapAsset } from 'src/dtos/asset-response.dto'; -import { ExifResponseDto } from 'src/dtos/exif.dto'; +import { MapAsset } from 'src/dtos/asset-response.dto'; import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; -import { mapUser } from 'src/dtos/user.dto'; -import { AssetOrder, AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum'; +import { AssetStatus, AssetType, AssetVisibility, SharedLinkType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -20,89 +17,6 @@ const sharedLinkBytes = Buffer.from( 'hex', ); -const assetInfo: ExifResponseDto = { - make: 'camera-make', - model: 'camera-model', - exifImageWidth: 500, - exifImageHeight: 500, - fileSizeInByte: 100, - orientation: 'orientation', - dateTimeOriginal: today, - modifyDate: today, - timeZone: 'America/Los_Angeles', - lensModel: 'fancy', - fNumber: 100, - focalLength: 100, - iso: 100, - exposureTime: '1/16', - latitude: 100, - longitude: 100, - city: 'city', - state: 'state', - country: 'country', - description: 'description', - projectionType: null, -}; - -const assetResponse: AssetResponseDto = { - id: 'id_1', - createdAt: today, - deviceAssetId: 'device_asset_id_1', - ownerId: 'user_id_1', - deviceId: 'device_id_1', - type: AssetType.Video, - originalMimeType: 'image/jpeg', - originalPath: 'fake_path/jpeg', - originalFileName: 'asset_1.jpeg', - thumbhash: null, - fileModifiedAt: today, - isOffline: false, - fileCreatedAt: today, - localDateTime: today, - updatedAt: today, - isFavorite: false, - isArchived: false, - duration: '0:00:00.00000', - exifInfo: assetInfo, - livePhotoVideoId: null, - tags: [], - people: [], - checksum: 'ZmlsZSBoYXNo', - isTrashed: false, - libraryId: 'library-id', - hasMetadata: true, - visibility: AssetVisibility.Timeline, -}; - -const assetResponseWithoutMetadata = { - id: 'id_1', - type: AssetType.Video, - originalMimeType: 'image/jpeg', - thumbhash: null, - localDateTime: today, - duration: '0:00:00.00000', - livePhotoVideoId: null, - hasMetadata: false, -} as AssetResponseDto; - -const albumResponse: AlbumResponseDto = { - albumName: 'Test Album', - description: '', - albumThumbnailAssetId: null, - createdAt: today, - updatedAt: today, - id: 'album-123', - ownerId: 'admin_id', - owner: mapUser(userStub.admin), - albumUsers: [], - shared: false, - hasSharedLink: false, - assets: [], - assetCount: 1, - isActivityEnabled: true, - order: AssetOrder.Desc, -}; - export const sharedLinkStub = { individual: Object.freeze({ id: '123', @@ -161,7 +75,7 @@ export const sharedLinkStub = { id: '123', userId: authStub.admin.user.id, key: sharedLinkBytes, - type: SharedLinkType.Album, + type: SharedLinkType.Individual, createdAt: today, expiresAt: tomorrow, allowUpload: false, @@ -169,97 +83,80 @@ export const sharedLinkStub = { showExif: false, description: null, password: null, - assets: [], - slug: null, - albumId: 'album-123', - album: { - id: 'album-123', - updateId: '42', - ownerId: authStub.admin.user.id, - owner: userStub.admin, - albumName: 'Test Album', - description: '', - createdAt: today, - updatedAt: today, - deletedAt: null, - albumThumbnailAsset: null, - albumThumbnailAssetId: null, - albumUsers: [], - sharedLinks: [], - isActivityEnabled: true, - order: AssetOrder.Desc, - assets: [ - { - id: 'id_1', - status: AssetStatus.Active, - owner: undefined as unknown as UserAdmin, - ownerId: 'user_id_1', - deviceAssetId: 'device_asset_id_1', - deviceId: 'device_id_1', - type: AssetType.Video, - originalPath: 'fake_path/jpeg', - checksum: Buffer.from('file hash', 'utf8'), - fileModifiedAt: today, - fileCreatedAt: today, - localDateTime: today, - createdAt: today, + assets: [ + { + id: 'id_1', + status: AssetStatus.Active, + owner: undefined as unknown as UserAdmin, + ownerId: 'user_id_1', + deviceAssetId: 'device_asset_id_1', + deviceId: 'device_id_1', + type: AssetType.Video, + originalPath: 'fake_path/jpeg', + checksum: Buffer.from('file hash', 'utf8'), + fileModifiedAt: today, + fileCreatedAt: today, + localDateTime: today, + createdAt: today, + updatedAt: today, + isFavorite: false, + isArchived: false, + isExternal: false, + isOffline: false, + files: [], + thumbhash: null, + encodedVideoPath: '', + duration: null, + livePhotoVideo: null, + livePhotoVideoId: null, + originalFileName: 'asset_1.jpeg', + exifInfo: { + projectionType: null, + livePhotoCID: null, + assetId: 'id_1', + description: 'description', + exifImageWidth: 500, + exifImageHeight: 500, + fileSizeInByte: 100, + orientation: 'orientation', + dateTimeOriginal: today, + modifyDate: today, + timeZone: 'America/Los_Angeles', + latitude: 100, + longitude: 100, + city: 'city', + state: 'state', + country: 'country', + make: 'camera-make', + model: 'camera-model', + lensModel: 'fancy', + fNumber: 100, + focalLength: 100, + iso: 100, + exposureTime: '1/16', + fps: 100, + profileDescription: 'sRGB', + bitsPerSample: 8, + colorspace: 'sRGB', + autoStackId: null, + rating: 3, updatedAt: today, - isFavorite: false, - isArchived: false, - isExternal: false, - isOffline: false, - files: [], - thumbhash: null, - encodedVideoPath: '', - duration: null, - livePhotoVideo: null, - livePhotoVideoId: null, - originalFileName: 'asset_1.jpeg', - exifInfo: { - projectionType: null, - livePhotoCID: null, - assetId: 'id_1', - description: 'description', - exifImageWidth: 500, - exifImageHeight: 500, - fileSizeInByte: 100, - orientation: 'orientation', - dateTimeOriginal: today, - modifyDate: today, - timeZone: 'America/Los_Angeles', - latitude: 100, - longitude: 100, - city: 'city', - state: 'state', - country: 'country', - make: 'camera-make', - model: 'camera-model', - lensModel: 'fancy', - fNumber: 100, - focalLength: 100, - iso: 100, - exposureTime: '1/16', - fps: 100, - profileDescription: 'sRGB', - bitsPerSample: 8, - colorspace: 'sRGB', - autoStackId: null, - rating: 3, - updatedAt: today, - updateId: '42', - }, - sharedLinks: [], - faces: [], - sidecarPath: null, - deletedAt: null, - duplicateId: null, updateId: '42', - libraryId: null, - stackId: null, - visibility: AssetVisibility.Timeline, }, - ], - }, + sharedLinks: [], + faces: [], + sidecarPath: null, + deletedAt: null, + duplicateId: null, + updateId: '42', + libraryId: null, + stackId: null, + visibility: AssetVisibility.Timeline, + }, + ], + albumId: null, + album: null, + slug: null, }), passwordRequired: Object.freeze({ id: '123', @@ -312,20 +209,4 @@ export const sharedLinkResponseStub = { userId: 'admin_id', slug: null, }), - readonlyNoMetadata: Object.freeze({ - id: '123', - userId: 'admin_id', - key: sharedLinkBytes.toString('base64url'), - type: SharedLinkType.Album, - createdAt: today, - expiresAt: tomorrow, - description: null, - password: null, - allowUpload: false, - allowDownload: false, - showMetadata: false, - slug: null, - album: { ...albumResponse, startDate: assetResponse.localDateTime, endDate: assetResponse.localDateTime }, - assets: [{ ...assetResponseWithoutMetadata, exifInfo: undefined }], - }), }; diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index d3e0665de3..f6ca2b610e 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -13,8 +13,8 @@ AlbumGroupBy, AlbumSortBy, AlbumViewMode, - SortOrder, locale, + SortOrder, type AlbumViewSettings, } from '$lib/stores/preferences.store'; import { user } from '$lib/stores/user.store'; @@ -23,7 +23,12 @@ import type { ContextMenuPosition } from '$lib/utils/context-menu'; import { handleError } from '$lib/utils/handle-error'; import { normalizeSearchString } from '$lib/utils/string-utils'; - import { addUsersToAlbum, type AlbumResponseDto, type AlbumUserAddDto } from '@immich/sdk'; + import { + addUsersToAlbum, + type AlbumResponseDto, + type AlbumUserAddDto, + type SharedLinkResponseDto, + } from '@immich/sdk'; import { modalManager } from '@immich/ui'; import { mdiDeleteOutline, mdiDownload, mdiRenameOutline, mdiShareVariantOutline } from '@mdi/js'; import { groupBy } from 'lodash-es'; @@ -208,12 +213,7 @@ } case 'sharedLink': { - const success = await modalManager.show(SharedLinkCreateModal, { albumId: selectedAlbum.id }); - if (success) { - selectedAlbum.shared = true; - selectedAlbum.hasSharedLink = true; - onUpdate(selectedAlbum); - } + await modalManager.show(SharedLinkCreateModal, { albumId: selectedAlbum.id }); break; } } @@ -274,9 +274,15 @@ ownedAlbums = ownedAlbums.filter(({ id }) => id !== album.id); sharedAlbums = sharedAlbums.filter(({ id }) => id !== album.id); }; + + const onSharedLinkCreate = (sharedLink: SharedLinkResponseDto) => { + if (sharedLink.album) { + onUpdate(sharedLink.album); + } + }; - + {#if albums.length > 0} {#if userSettings.view === AlbumViewMode.Cover} diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte deleted file mode 100644 index bfd3312f3b..0000000000 --- a/web/src/lib/components/asset-viewer/actions/share-action.svelte +++ /dev/null @@ -1,26 +0,0 @@ - - - diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 78138ae4ae..3bd08f0a7b 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import { resolve } from '$app/paths'; import CastButton from '$lib/cast/cast-button.svelte'; + import ActionButton from '$lib/components/ActionButton.svelte'; import type { OnAction, PreAction } from '$lib/components/asset-viewer/actions/action'; import AddToAlbumAction from '$lib/components/asset-viewer/actions/add-to-album-action.svelte'; import AddToStackAction from '$lib/components/asset-viewer/actions/add-to-stack-action.svelte'; @@ -18,14 +19,13 @@ import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte'; import SetStackPrimaryAsset from '$lib/components/asset-viewer/actions/set-stack-primary-asset.svelte'; import SetVisibilityAction from '$lib/components/asset-viewer/actions/set-visibility-action.svelte'; - import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte'; import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte'; import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte'; 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 { AppRoute } from '$lib/constants'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; - import { handleReplaceAsset } from '$lib/services/asset.service'; + import { getAssetActions, handleReplaceAsset } from '$lib/services/asset.service'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { user } from '$lib/stores/user.store'; import { photoZoomState } from '$lib/stores/zoom-image.store'; @@ -113,6 +113,8 @@ let isLocked = $derived(asset.visibility === AssetVisibility.Locked); let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch); + const { Share } = $derived(getAssetActions($t, asset)); + // $: showEditorButton = // isOwner && // asset.type === AssetTypeEnum.Image && @@ -135,9 +137,7 @@
- {#if !asset.isTrashed && $user && !isLocked} - - {/if} + {#if asset.isOffline} void; + onClose: () => void; albumId?: string; assetIds?: string[]; } - let { onClose, albumId = $bindable(), assetIds = $bindable([]) }: Props = $props(); + let { onClose, albumId, assetIds }: Props = $props(); let description = $state(''); let allowDownload = $state(true); @@ -44,7 +44,7 @@ slug, }); if (success) { - onClose(true); + onClose(); } }; diff --git a/web/src/lib/services/asset.service.ts b/web/src/lib/services/asset.service.ts index 245b4888ca..a64da2a6d6 100644 --- a/web/src/lib/services/asset.service.ts +++ b/web/src/lib/services/asset.service.ts @@ -1,6 +1,23 @@ import { eventManager } from '$lib/managers/event-manager.svelte'; +import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte'; +import { user as authUser } from '$lib/stores/user.store'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; -import { copyAsset, deleteAssets } from '@immich/sdk'; +import { AssetVisibility, copyAsset, deleteAssets, type AssetResponseDto } from '@immich/sdk'; +import { modalManager, type ActionItem } from '@immich/ui'; +import { mdiShareVariantOutline } from '@mdi/js'; +import type { MessageFormatter } from 'svelte-i18n'; +import { get } from 'svelte/store'; + +export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) => { + const Share: ActionItem = { + title: $t('share'), + icon: mdiShareVariantOutline, + $if: () => !!(get(authUser) && !asset.isTrashed && asset.visibility !== AssetVisibility.Locked), + onAction: () => modalManager.show(SharedLinkCreateModal, { assetIds: [asset.id] }), + }; + + return { Share }; +}; export const handleReplaceAsset = async (oldAssetId: string) => { const [newAssetId] = await openFileUploadDialog({ multiple: false }); diff --git a/web/src/lib/services/shared-link.service.ts b/web/src/lib/services/shared-link.service.ts index 9f70024193..50069dc6d8 100644 --- a/web/src/lib/services/shared-link.service.ts +++ b/web/src/lib/services/shared-link.service.ts @@ -9,6 +9,7 @@ import { handleError } from '$lib/utils/handle-error'; import { getFormatter } from '$lib/utils/i18n'; import { createSharedLink, + getSharedLinkById, removeSharedLink, removeSharedLinkAssets, updateSharedLink, @@ -58,7 +59,11 @@ export const handleCreateSharedLink = async (dto: SharedLinkCreateDto) => { const $t = await getFormatter(); try { - const sharedLink = await createSharedLink({ sharedLinkCreateDto: dto }); + let sharedLink = await createSharedLink({ sharedLinkCreateDto: dto }); + if (dto.albumId) { + // fetch album details, for event + sharedLink = await getSharedLinkById({ id: sharedLink.id }); + } eventManager.emit('SharedLinkCreate', sharedLink);