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.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/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 0dad2793bf..5900e2a568 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'; @@ -110,6 +110,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 && @@ -132,9 +134,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);