mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	refactor(web): shared link key auth (#3855)
This commit is contained in:
		| @@ -39,6 +39,11 @@ export class ImmichApi { | ||||
|   public userApi: UserApi; | ||||
|  | ||||
|   private config: Configuration; | ||||
|   private key?: string; | ||||
|  | ||||
|   get isSharedLink() { | ||||
|     return !!this.key; | ||||
|   } | ||||
|  | ||||
|   constructor(params: ConfigurationParameters) { | ||||
|     this.config = new Configuration(params); | ||||
| @@ -73,6 +78,14 @@ export class ImmichApi { | ||||
|     return (this.config.basePath || BASE_PATH) + toPathString(url); | ||||
|   } | ||||
|  | ||||
|   public setKey(key: string) { | ||||
|     this.key = key; | ||||
|   } | ||||
|  | ||||
|   public getKey(): string | undefined { | ||||
|     return this.key; | ||||
|   } | ||||
|  | ||||
|   public setAccessToken(accessToken: string) { | ||||
|     this.config.accessToken = accessToken; | ||||
|   } | ||||
| @@ -85,14 +98,14 @@ export class ImmichApi { | ||||
|     this.config.basePath = baseUrl; | ||||
|   } | ||||
|  | ||||
|   public getAssetFileUrl(...[assetId, isThumb, isWeb, key]: ApiParams<typeof AssetApiFp, 'serveFile'>) { | ||||
|   public getAssetFileUrl(...[assetId, isThumb, isWeb]: ApiParams<typeof AssetApiFp, 'serveFile'>) { | ||||
|     const path = `/asset/file/${assetId}`; | ||||
|     return this.createUrl(path, { isThumb, isWeb, key }); | ||||
|     return this.createUrl(path, { isThumb, isWeb, key: this.getKey() }); | ||||
|   } | ||||
|  | ||||
|   public getAssetThumbnailUrl(...[assetId, format, key]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>) { | ||||
|   public getAssetThumbnailUrl(...[assetId, format]: ApiParams<typeof AssetApiFp, 'getAssetThumbnail'>) { | ||||
|     const path = `/asset/thumbnail/${assetId}`; | ||||
|     return this.createUrl(path, { format, key }); | ||||
|     return this.createUrl(path, { format, key: this.getKey() }); | ||||
|   } | ||||
|  | ||||
|   public getProfileImageUrl(...[userId]: ApiParams<typeof UserApiFp, 'getProfileImage'>) { | ||||
|   | ||||
| @@ -27,13 +27,13 @@ | ||||
|  | ||||
|   let { isViewing: showAssetViewer } = assetViewingStore; | ||||
|  | ||||
|   const assetStore = new AssetStore({ size: TimeBucketSize.Month, albumId: album.id, key: sharedLink.key }); | ||||
|   const assetStore = new AssetStore({ size: TimeBucketSize.Month, albumId: album.id }); | ||||
|   const assetInteractionStore = createAssetInteractionStore(); | ||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; | ||||
|  | ||||
|   dragAndDropFilesStore.subscribe((value) => { | ||||
|     if (value.isDragging && value.files.length > 0) { | ||||
|       fileUploadHandler(value.files, album.id, sharedLink.key); | ||||
|       fileUploadHandler(value.files, album.id); | ||||
|       dragAndDropFilesStore.set({ isDragging: false, files: [] }); | ||||
|     } | ||||
|   }); | ||||
| @@ -88,7 +88,7 @@ | ||||
|   }; | ||||
|  | ||||
|   const downloadAlbum = async () => { | ||||
|     await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }, sharedLink.key); | ||||
|     await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| @@ -97,7 +97,7 @@ | ||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> | ||||
|       <SelectAllAssets {assetStore} {assetInteractionStore} /> | ||||
|       {#if sharedLink.allowDownload} | ||||
|         <DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink.key} /> | ||||
|         <DownloadAction filename="{album.albumName}.zip" /> | ||||
|       {/if} | ||||
|     </AssetSelectControlBar> | ||||
|   {:else} | ||||
| @@ -117,7 +117,7 @@ | ||||
|         {#if sharedLink.allowUpload} | ||||
|           <CircleIconButton | ||||
|             title="Add Photos" | ||||
|             on:click={() => openFileUploadDialog(album.id, sharedLink.key)} | ||||
|             on:click={() => openFileUploadDialog(album.id)} | ||||
|             logo={FileImagePlusOutline} | ||||
|           /> | ||||
|         {/if} | ||||
| @@ -135,7 +135,7 @@ | ||||
| <main | ||||
|   class="relative h-screen overflow-hidden bg-immich-bg px-6 pt-[var(--navbar-height)] dark:bg-immich-dark-bg sm:px-12 md:px-24 lg:px-40" | ||||
| > | ||||
|   <AssetGrid {assetStore} {assetInteractionStore} publicSharedKey={sharedLink.key}> | ||||
|   <AssetGrid {assetStore} {assetInteractionStore}> | ||||
|     <section class="pt-24"> | ||||
|       <!-- ALBUM TITLE --> | ||||
|       <p | ||||
|   | ||||
| @@ -26,7 +26,6 @@ | ||||
|  | ||||
|   export let assetStore: AssetStore | null = null; | ||||
|   export let asset: AssetResponseDto; | ||||
|   export let publicSharedKey = ''; | ||||
|   export let showNavigation = true; | ||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
|  | ||||
| @@ -72,6 +71,10 @@ | ||||
|   $: asset.id && !sharedLink && getAllAlbums(); // Update the album information when the asset ID changes | ||||
|  | ||||
|   const getAllAlbums = async () => { | ||||
|     if (api.isSharedLink) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     try { | ||||
|       const { data } = await api.albumApi.getAllAlbums({ assetId: asset.id }); | ||||
|       appearsInAlbums = data; | ||||
| @@ -84,7 +87,9 @@ | ||||
|     switch (key) { | ||||
|       case 'a': | ||||
|       case 'A': | ||||
|         if (shiftKey) toggleArchive(); | ||||
|         if (shiftKey) { | ||||
|           toggleArchive(); | ||||
|         } | ||||
|         return; | ||||
|       case 'ArrowLeft': | ||||
|         navigateAssetBackward(); | ||||
| @@ -94,7 +99,9 @@ | ||||
|         return; | ||||
|       case 'd': | ||||
|       case 'D': | ||||
|         if (shiftKey) downloadFile(asset, publicSharedKey); | ||||
|         if (shiftKey) { | ||||
|           downloadFile(asset); | ||||
|         } | ||||
|         return; | ||||
|       case 'Delete': | ||||
|         isShowDeleteConfirmation = true; | ||||
| @@ -272,7 +279,7 @@ | ||||
|       showDownloadButton={shouldShowDownloadButton} | ||||
|       on:goBack={closeViewer} | ||||
|       on:showDetail={showDetailInfoHandler} | ||||
|       on:download={() => downloadFile(asset, publicSharedKey)} | ||||
|       on:download={() => downloadFile(asset)} | ||||
|       on:delete={() => (isShowDeleteConfirmation = true)} | ||||
|       on:favorite={toggleFavorite} | ||||
|       on:addToAlbum={() => openAlbumPicker(false)} | ||||
| @@ -304,7 +311,6 @@ | ||||
|       {:else if asset.type === AssetTypeEnum.Image} | ||||
|         {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} | ||||
|           <VideoViewer | ||||
|             {publicSharedKey} | ||||
|             assetId={asset.livePhotoVideoId} | ||||
|             on:close={closeViewer} | ||||
|             on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} | ||||
| @@ -312,12 +318,12 @@ | ||||
|         {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath | ||||
|             .toLowerCase() | ||||
|             .endsWith('.insp')} | ||||
|           <PanoramaViewer {publicSharedKey} {asset} /> | ||||
|           <PanoramaViewer {asset} /> | ||||
|         {:else} | ||||
|           <PhotoViewer {publicSharedKey} {asset} on:close={closeViewer} /> | ||||
|           <PhotoViewer {asset} on:close={closeViewer} /> | ||||
|         {/if} | ||||
|       {:else} | ||||
|         <VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} /> | ||||
|         <VideoViewer assetId={asset.id} on:close={closeViewer} /> | ||||
|       {/if} | ||||
|     {/key} | ||||
|   </div> | ||||
| @@ -338,7 +344,6 @@ | ||||
|       <DetailPanel | ||||
|         {asset} | ||||
|         albums={appearsInAlbums} | ||||
|         {sharedLink} | ||||
|         on:close={() => ($isShowDetail = false)} | ||||
|         on:close-viewer={handleCloseViewer} | ||||
|         on:description-focus-in={disableKeyDownEvent} | ||||
|   | ||||
| @@ -9,21 +9,20 @@ | ||||
|   import ImageOutline from 'svelte-material-icons/ImageOutline.svelte'; | ||||
|   import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import { AssetResponseDto, AlbumResponseDto, api, ThumbnailFormat, SharedLinkResponseDto } from '@api'; | ||||
|   import { AssetResponseDto, AlbumResponseDto, api, ThumbnailFormat } from '@api'; | ||||
|   import { asByteUnitString } from '../../utils/byte-units'; | ||||
|   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; | ||||
|   import { getAssetFilename } from '$lib/utils/asset-utils'; | ||||
|  | ||||
|   export let asset: AssetResponseDto; | ||||
|   export let albums: AlbumResponseDto[] = []; | ||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
|  | ||||
|   let textarea: HTMLTextAreaElement; | ||||
|   let description: string; | ||||
|  | ||||
|   $: { | ||||
|     // Get latest description from server | ||||
|     if (asset.id && !sharedLink) { | ||||
|     if (asset.id && !api.isSharedLink) { | ||||
|       api.assetApi.getAssetById({ id: asset.id }).then((res) => { | ||||
|         people = res.data?.people || []; | ||||
|         textarea.value = res.data?.exifInfo?.description || ''; | ||||
| @@ -91,13 +90,15 @@ | ||||
|     <p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p> | ||||
|   </div> | ||||
|  | ||||
|   <section class="mx-4 mt-10"> | ||||
|   <section | ||||
|     class="mx-4 mt-10" | ||||
|     style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' ? 'none' : 'block'} | ||||
|   > | ||||
|     <textarea | ||||
|       bind:this={textarea} | ||||
|       class="max-h-[500px] | ||||
|       w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary" | ||||
|       placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'} | ||||
|       style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' ? 'none' : 'block'} | ||||
|       on:focusin={handleFocusIn} | ||||
|       on:focusout={handleFocusOut} | ||||
|       on:input={autoGrowHeight} | ||||
| @@ -106,7 +107,7 @@ | ||||
|     /> | ||||
|   </section> | ||||
|  | ||||
|   {#if people.length > 0} | ||||
|   {#if !api.isSharedLink && people.length > 0} | ||||
|     <section class="px-4 py-4 text-sm"> | ||||
|       <h2>PEOPLE</h2> | ||||
|  | ||||
|   | ||||
| @@ -4,14 +4,16 @@ | ||||
|   import { api, AssetResponseDto } from '@api'; | ||||
|   import View360, { EquirectProjection } from '@egjs/svelte-view360'; | ||||
|   import './panorama-viewer.css'; | ||||
|  | ||||
|   export let asset: AssetResponseDto; | ||||
|   export let publicSharedKey = ''; | ||||
|  | ||||
|   let dataUrl = ''; | ||||
|   let errorMessage = ''; | ||||
|  | ||||
|   const loadAssetData = async () => { | ||||
|     try { | ||||
|       const { data } = await api.assetApi.serveFile( | ||||
|         { id: asset.id, isThumb: false, isWeb: false, key: publicSharedKey }, | ||||
|         { id: asset.id, isThumb: false, isWeb: false, key: api.getKey() }, | ||||
|         { responseType: 'blob' }, | ||||
|       ); | ||||
|       if (data instanceof Blob) { | ||||
|   | ||||
| @@ -8,12 +8,10 @@ | ||||
|   import { photoZoomState } from '$lib/stores/zoom-image.store'; | ||||
|  | ||||
|   export let asset: AssetResponseDto; | ||||
|   export let publicSharedKey = ''; | ||||
|   export let element: HTMLDivElement | undefined = undefined; | ||||
|  | ||||
|   let imgElement: HTMLDivElement; | ||||
|  | ||||
|   let assetData: string; | ||||
|  | ||||
|   let copyImageToClipboard: (src: string) => Promise<Blob>; | ||||
|   let canCopyImagesToClipboard: () => boolean; | ||||
|  | ||||
| @@ -28,7 +26,7 @@ | ||||
|   const loadAssetData = async () => { | ||||
|     try { | ||||
|       const { data } = await api.assetApi.serveFile( | ||||
|         { id: asset.id, isThumb: false, isWeb: true, key: publicSharedKey }, | ||||
|         { id: asset.id, isThumb: false, isWeb: true, key: api.getKey() }, | ||||
|         { | ||||
|           responseType: 'blob', | ||||
|         }, | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
|   import { handleError } from '../../utils/handle-error'; | ||||
|  | ||||
|   export let assetId: string; | ||||
|   export let publicSharedKey: string | undefined = undefined; | ||||
|  | ||||
|   let isVideoLoading = true; | ||||
|   const dispatch = createEventDispatcher<{ onVideoEnded: void }>(); | ||||
| @@ -37,7 +36,7 @@ | ||||
|     bind:volume={$videoViewerVolume} | ||||
|     poster={api.getAssetThumbnailUrl(assetId, ThumbnailFormat.Jpeg)} | ||||
|   > | ||||
|     <source src={api.getAssetFileUrl(assetId, false, true, publicSharedKey)} type="video/mp4" /> | ||||
|     <source src={api.getAssetFileUrl(assetId, false, true)} type="video/mp4" /> | ||||
|     <track kind="captions" /> | ||||
|   </video> | ||||
|  | ||||
|   | ||||
| @@ -27,7 +27,6 @@ | ||||
|   export let selectionCandidate = false; | ||||
|   export let disabled = false; | ||||
|   export let readonly = false; | ||||
|   export let publicSharedKey: string | undefined = undefined; | ||||
|   export let showArchiveIcon = false; | ||||
|  | ||||
|   let mouseOver = false; | ||||
| @@ -118,13 +117,13 @@ | ||||
|         /> | ||||
|  | ||||
|         <!-- Favorite asset star --> | ||||
|         {#if asset.isFavorite && !publicSharedKey} | ||||
|         {#if !api.isSharedLink && asset.isFavorite} | ||||
|           <div class="absolute bottom-2 left-2 z-10"> | ||||
|             <Heart size="24" class="text-white" /> | ||||
|           </div> | ||||
|         {/if} | ||||
|  | ||||
|         {#if showArchiveIcon && asset.isArchived} | ||||
|         {#if !api.isSharedLink && showArchiveIcon && asset.isArchived} | ||||
|           <div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10"> | ||||
|             <ArchiveArrowDownOutline size="24" class="text-white" /> | ||||
|           </div> | ||||
| @@ -140,7 +139,7 @@ | ||||
|  | ||||
|         {#if asset.resized} | ||||
|           <ImageThumbnail | ||||
|             url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)} | ||||
|             url={api.getAssetThumbnailUrl(asset.id, format)} | ||||
|             altText={asset.originalFileName} | ||||
|             widthStyle="{width}px" | ||||
|             heightStyle="{height}px" | ||||
| @@ -156,7 +155,7 @@ | ||||
|         {#if asset.type === AssetTypeEnum.Video} | ||||
|           <div class="absolute top-0 h-full w-full"> | ||||
|             <VideoThumbnail | ||||
|               url={api.getAssetFileUrl(asset.id, false, true, publicSharedKey)} | ||||
|               url={api.getAssetFileUrl(asset.id, false, true)} | ||||
|               enablePlayback={mouseOver} | ||||
|               curve={selected} | ||||
|               durationInSeconds={timeToSeconds(asset.duration)} | ||||
| @@ -167,7 +166,7 @@ | ||||
|         {#if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId} | ||||
|           <div class="absolute top-0 h-full w-full"> | ||||
|             <VideoThumbnail | ||||
|               url={api.getAssetFileUrl(asset.livePhotoVideoId, false, true, publicSharedKey)} | ||||
|               url={api.getAssetFileUrl(asset.livePhotoVideoId, false, true)} | ||||
|               pauseIcon={MotionPauseOutline} | ||||
|               playIcon={MotionPlayOutline} | ||||
|               showTime={false} | ||||
|   | ||||
| @@ -6,7 +6,6 @@ | ||||
|   import { getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|  | ||||
|   export let filename = 'immich.zip'; | ||||
|   export let sharedLinkKey: string | undefined = undefined; | ||||
|   export let menuItem = false; | ||||
|  | ||||
|   const { getAssets, clearSelect } = getAssetControlContext(); | ||||
| @@ -15,12 +14,12 @@ | ||||
|     const assets = Array.from(getAssets()); | ||||
|     if (assets.length === 1) { | ||||
|       clearSelect(); | ||||
|       await downloadFile(assets[0], sharedLinkKey); | ||||
|       await downloadFile(assets[0]); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     clearSelect(); | ||||
|     await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, sharedLinkKey); | ||||
|     await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
|   | ||||
| @@ -20,7 +20,7 @@ | ||||
|         assetIdsDto: { | ||||
|           assetIds: Array.from(getAssets()).map((asset) => asset.id), | ||||
|         }, | ||||
|         key: sharedLink.key, | ||||
|         key: api.getKey(), | ||||
|       }); | ||||
|  | ||||
|       for (const result of results) { | ||||
|   | ||||
| @@ -21,7 +21,6 @@ | ||||
|   export let isSelectionMode = false; | ||||
|   export let viewport: Viewport; | ||||
|   export let singleSelect = false; | ||||
|   export let publicSharedKey: string | undefined = undefined; | ||||
|  | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
| @@ -96,7 +95,7 @@ | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     assetViewingStore.setAssetId(asset.id, publicSharedKey); | ||||
|     assetViewingStore.setAssetId(asset.id); | ||||
|   }; | ||||
|  | ||||
|   const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets }); | ||||
| @@ -189,7 +188,6 @@ | ||||
|               disabled={$assetStore.albumAssets.has(asset.id)} | ||||
|               thumbnailWidth={box.width} | ||||
|               thumbnailHeight={box.height} | ||||
|               {publicSharedKey} | ||||
|             /> | ||||
|           </div> | ||||
|         {/each} | ||||
|   | ||||
| @@ -23,7 +23,7 @@ | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
|   export let removeAction: AssetAction | null = null; | ||||
|   export let publicSharedKey: string | undefined = undefined; | ||||
|  | ||||
|   const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = | ||||
|     assetInteractionStore; | ||||
|   const viewport: Viewport = { width: 0, height: 0 }; | ||||
| @@ -97,7 +97,7 @@ | ||||
|   const handlePrevious = async () => { | ||||
|     const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id); | ||||
|     if (previousAsset) { | ||||
|       assetViewingStore.setAssetId(previousAsset, publicSharedKey); | ||||
|       assetViewingStore.setAssetId(previousAsset); | ||||
|     } | ||||
|  | ||||
|     return !!previousAsset; | ||||
| @@ -106,7 +106,7 @@ | ||||
|   const handleNext = async () => { | ||||
|     const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); | ||||
|     if (nextAsset) { | ||||
|       assetViewingStore.setAssetId(nextAsset, publicSharedKey); | ||||
|       assetViewingStore.setAssetId(nextAsset); | ||||
|     } | ||||
|  | ||||
|     return !!nextAsset; | ||||
| @@ -349,7 +349,6 @@ | ||||
|                 bucketDate={bucket.bucketDate} | ||||
|                 bucketHeight={bucket.bucketHeight} | ||||
|                 {viewport} | ||||
|                 {publicSharedKey} | ||||
|               /> | ||||
|             {/if} | ||||
|           </div> | ||||
| @@ -371,7 +370,6 @@ | ||||
|       on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)} | ||||
|       on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)} | ||||
|       on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)} | ||||
|       {publicSharedKey} | ||||
|     /> | ||||
|   {/if} | ||||
| </Portal> | ||||
|   | ||||
| @@ -15,7 +15,6 @@ | ||||
|   import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||
|   import SelectAll from 'svelte-material-icons/SelectAll.svelte'; | ||||
|   import ImmichLogo from '../shared-components/immich-logo.svelte'; | ||||
|  | ||||
|   import { notificationController, NotificationType } from '../shared-components/notification/notification'; | ||||
|   import { handleError } from '../../utils/handle-error'; | ||||
|  | ||||
| @@ -35,23 +34,23 @@ | ||||
|   }); | ||||
|  | ||||
|   const downloadAssets = async () => { | ||||
|     await downloadArchive(`immich-shared.zip`, { assetIds: assets.map((asset) => asset.id) }, sharedLink.key); | ||||
|     await downloadArchive(`immich-shared.zip`, { assetIds: assets.map((asset) => asset.id) }); | ||||
|   }; | ||||
|  | ||||
|   const handleUploadAssets = async (files: File[] = []) => { | ||||
|     try { | ||||
|       let results: (string | undefined)[] = []; | ||||
|       if (!files || files.length === 0 || !Array.isArray(files)) { | ||||
|         results = await openFileUploadDialog(undefined, sharedLink.key); | ||||
|         results = await openFileUploadDialog(undefined); | ||||
|       } else { | ||||
|         results = await fileUploadHandler(files, undefined, sharedLink.key); | ||||
|         results = await fileUploadHandler(files, undefined); | ||||
|       } | ||||
|       const { data } = await api.sharedLinkApi.addSharedLinkAssets({ | ||||
|         id: sharedLink.id, | ||||
|         assetIdsDto: { | ||||
|           assetIds: results.filter((id) => !!id) as string[], | ||||
|         }, | ||||
|         key: sharedLink.key, | ||||
|         key: api.getKey(), | ||||
|       }); | ||||
|  | ||||
|       const added = data.filter((item) => item.success).length; | ||||
| @@ -75,7 +74,7 @@ | ||||
|     <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> | ||||
|       <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} /> | ||||
|       {#if sharedLink?.allowDownload} | ||||
|         <DownloadAction filename="immich-shared.zip" sharedLinkKey={sharedLink.key} /> | ||||
|         <DownloadAction filename="immich-shared.zip" /> | ||||
|       {/if} | ||||
|       {#if isOwned} | ||||
|         <RemoveFromSharedLink bind:sharedLink /> | ||||
| @@ -106,6 +105,6 @@ | ||||
|     </ControlAppBar> | ||||
|   {/if} | ||||
|   <section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40"> | ||||
|     <GalleryViewer {assets} {sharedLink} bind:selectedAssets /> | ||||
|     <GalleryViewer {assets} bind:selectedAssets /> | ||||
|   </section> | ||||
| </section> | ||||
|   | ||||
| @@ -2,14 +2,13 @@ | ||||
|   import { page } from '$app/stores'; | ||||
|   import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { AssetResponseDto, SharedLinkResponseDto, ThumbnailFormat } from '@api'; | ||||
|   import { AssetResponseDto, ThumbnailFormat } from '@api'; | ||||
|   import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; | ||||
|   import { flip } from 'svelte/animate'; | ||||
|   import { getThumbnailSize } from '$lib/utils/thumbnail-util'; | ||||
|   import { assetViewingStore } from '$lib/stores/asset-viewing.store'; | ||||
|  | ||||
|   export let assets: AssetResponseDto[]; | ||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||
|   export let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
|   export let disableAssetSelect = false; | ||||
|   export let showArchiveIcon = false; | ||||
| @@ -90,7 +89,6 @@ | ||||
|           {asset} | ||||
|           {thumbnailSize} | ||||
|           readonly={disableAssetSelect} | ||||
|           publicSharedKey={sharedLink?.key} | ||||
|           format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp} | ||||
|           on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} | ||||
|           on:select={selectAssetHandler} | ||||
| @@ -106,8 +104,6 @@ | ||||
| {#if $showAssetViewer} | ||||
|   <AssetViewer | ||||
|     asset={selectedAsset} | ||||
|     publicSharedKey={sharedLink?.key} | ||||
|     {sharedLink} | ||||
|     on:previous={navigateAssetBackward} | ||||
|     on:next={navigateAssetForward} | ||||
|     on:close={closeViewer} | ||||
|   | ||||
| @@ -5,8 +5,8 @@ function createAssetViewingStore() { | ||||
|   const viewingAssetStoreState = writable<AssetResponseDto>(); | ||||
|   const viewState = writable<boolean>(false); | ||||
|  | ||||
|   const setAssetId = async (id: string, key?: string) => { | ||||
|     const { data } = await api.assetApi.getAssetById({ id, key }); | ||||
|   const setAssetId = async (id: string) => { | ||||
|     const { data } = await api.assetApi.getAssetById({ id, key: api.getKey() }); | ||||
|     viewingAssetStoreState.set(data); | ||||
|     viewState.set(true); | ||||
|   }; | ||||
|   | ||||
| @@ -58,7 +58,10 @@ export class AssetStore { | ||||
|     this.assetToBucket = {}; | ||||
|     this.albumAssets = new Set(); | ||||
|  | ||||
|     const { data: buckets } = await api.assetApi.getTimeBuckets(this.options); | ||||
|     const { data: buckets } = await api.assetApi.getTimeBuckets({ | ||||
|       ...this.options, | ||||
|       key: api.getKey(), | ||||
|     }); | ||||
|  | ||||
|     this.buckets = buckets.map((bucket) => { | ||||
|       const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10); | ||||
| @@ -107,7 +110,11 @@ export class AssetStore { | ||||
|       bucket.cancelToken = new AbortController(); | ||||
|  | ||||
|       const { data: assets } = await api.assetApi.getByTimeBucket( | ||||
|         { ...this.options, timeBucket: bucketDate }, | ||||
|         { | ||||
|           ...this.options, | ||||
|           timeBucket: bucketDate, | ||||
|           key: api.getKey(), | ||||
|         }, | ||||
|         { signal: bucket.cancelToken.signal }, | ||||
|       ); | ||||
|  | ||||
| @@ -117,7 +124,7 @@ export class AssetStore { | ||||
|             albumId: this.albumId, | ||||
|             timeBucket: bucketDate, | ||||
|             size: this.options.size, | ||||
|             key: this.options.key, | ||||
|             key: api.getKey(), | ||||
|           }, | ||||
|           { signal: bucket.cancelToken.signal }, | ||||
|         ); | ||||
|   | ||||
| @@ -3,20 +3,22 @@ import { downloadManager } from '$lib/stores/download'; | ||||
| import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api'; | ||||
| import { handleError } from './handle-error'; | ||||
|  | ||||
| export const addAssetsToAlbum = async ( | ||||
|   albumId: string, | ||||
|   assetIds: Array<string>, | ||||
|   key: string | undefined = undefined, | ||||
| ): Promise<BulkIdResponseDto[]> => | ||||
|   api.albumApi.addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetIds }, key }).then(({ data: results }) => { | ||||
|     const count = results.filter(({ success }) => success).length; | ||||
|     notificationController.show({ | ||||
|       type: NotificationType.Info, | ||||
|       message: `Added ${count} asset${count === 1 ? '' : 's'}`, | ||||
|     }); | ||||
| export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> => | ||||
|   api.albumApi | ||||
|     .addAssetsToAlbum({ | ||||
|       id: albumId, | ||||
|       bulkIdsDto: { ids: assetIds }, | ||||
|       key: api.getKey(), | ||||
|     }) | ||||
|     .then(({ data: results }) => { | ||||
|       const count = results.filter(({ success }) => success).length; | ||||
|       notificationController.show({ | ||||
|         type: NotificationType.Info, | ||||
|         message: `Added ${count} asset${count === 1 ? '' : 's'}`, | ||||
|       }); | ||||
|  | ||||
|     return results; | ||||
|   }); | ||||
|       return results; | ||||
|     }); | ||||
|  | ||||
| const downloadBlob = (data: Blob, filename: string) => { | ||||
|   const url = URL.createObjectURL(data); | ||||
| @@ -32,11 +34,11 @@ const downloadBlob = (data: Blob, filename: string) => { | ||||
|   URL.revokeObjectURL(url); | ||||
| }; | ||||
|  | ||||
| export const downloadArchive = async (fileName: string, options: DownloadInfoDto, key?: string) => { | ||||
| export const downloadArchive = async (fileName: string, options: DownloadInfoDto) => { | ||||
|   let downloadInfo: DownloadResponseDto | null = null; | ||||
|  | ||||
|   try { | ||||
|     const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key }); | ||||
|     const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key: api.getKey() }); | ||||
|     downloadInfo = data; | ||||
|   } catch (error) { | ||||
|     handleError(error, 'Unable to download files'); | ||||
| @@ -61,7 +63,7 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto | ||||
|  | ||||
|     try { | ||||
|       const { data } = await api.assetApi.downloadArchive( | ||||
|         { assetIdsDto: { assetIds: archive.assetIds }, key }, | ||||
|         { assetIdsDto: { assetIds: archive.assetIds }, key: api.getKey() }, | ||||
|         { | ||||
|           responseType: 'blob', | ||||
|           signal: abort.signal, | ||||
| @@ -80,7 +82,7 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const downloadFile = async (asset: AssetResponseDto, key?: string) => { | ||||
| export const downloadFile = async (asset: AssetResponseDto) => { | ||||
|   const assets = [ | ||||
|     { | ||||
|       filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`, | ||||
| @@ -104,7 +106,7 @@ export const downloadFile = async (asset: AssetResponseDto, key?: string) => { | ||||
|       downloadManager.add(downloadKey, size, abort); | ||||
|  | ||||
|       const { data } = await api.assetApi.downloadFile( | ||||
|         { id, key }, | ||||
|         { id, key: api.getKey() }, | ||||
|         { | ||||
|           responseType: 'blob', | ||||
|           onDownloadProgress: (event: ProgressEvent) => { | ||||
|   | ||||
| @@ -14,10 +14,7 @@ const getExtensions = async () => { | ||||
|   return _extensions; | ||||
| }; | ||||
|  | ||||
| export const openFileUploadDialog = async ( | ||||
|   albumId: string | undefined = undefined, | ||||
|   sharedKey: string | undefined = undefined, | ||||
| ) => { | ||||
| export const openFileUploadDialog = async (albumId: string | undefined = undefined) => { | ||||
|   const extensions = await getExtensions(); | ||||
|  | ||||
|   return new Promise<(string | undefined)[]>((resolve, reject) => { | ||||
| @@ -34,7 +31,7 @@ export const openFileUploadDialog = async ( | ||||
|         } | ||||
|         const files = Array.from(target.files); | ||||
|  | ||||
|         resolve(fileUploadHandler(files, albumId, sharedKey)); | ||||
|         resolve(fileUploadHandler(files, albumId)); | ||||
|       }; | ||||
|  | ||||
|       fileSelector.click(); | ||||
| @@ -45,18 +42,14 @@ export const openFileUploadDialog = async ( | ||||
|   }); | ||||
| }; | ||||
|  | ||||
| export const fileUploadHandler = async ( | ||||
|   files: File[], | ||||
|   albumId: string | undefined = undefined, | ||||
|   sharedKey: string | undefined = undefined, | ||||
| ) => { | ||||
| export const fileUploadHandler = async (files: File[], albumId: string | undefined = undefined) => { | ||||
|   const extensions = await getExtensions(); | ||||
|   const iterable = { | ||||
|     files: files.filter((file) => extensions.some((ext) => file.name.toLowerCase().endsWith(ext)))[Symbol.iterator](), | ||||
|  | ||||
|     async *[Symbol.asyncIterator]() { | ||||
|       for (const file of this.files) { | ||||
|         yield fileUploader(file, albumId, sharedKey); | ||||
|         yield fileUploader(file, albumId); | ||||
|       } | ||||
|     }, | ||||
|   }; | ||||
| @@ -78,11 +71,7 @@ const fromAsync = async function <T>(iterable: AsyncIterable<T>) { | ||||
| }; | ||||
|  | ||||
| // TODO: should probably use the @api SDK | ||||
| async function fileUploader( | ||||
|   asset: File, | ||||
|   albumId: string | undefined = undefined, | ||||
|   sharedKey: string | undefined = undefined, | ||||
| ): Promise<string | undefined> { | ||||
| async function fileUploader(asset: File, albumId: string | undefined = undefined): Promise<string | undefined> { | ||||
|   const formData = new FormData(); | ||||
|   const fileCreatedAt = new Date(asset.lastModified).toISOString(); | ||||
|   const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified; | ||||
| @@ -103,9 +92,7 @@ async function fileUploader( | ||||
|     }); | ||||
|  | ||||
|     const response = await axios.post('/api/asset/upload', formData, { | ||||
|       params: { | ||||
|         key: sharedKey, | ||||
|       }, | ||||
|       params: { key: api.getKey() }, | ||||
|       onUploadProgress: (event) => { | ||||
|         const percentComplete = Math.floor((event.loaded / event.total) * 100); | ||||
|         uploadAssetsStore.updateProgress(deviceAssetId, percentComplete); | ||||
| @@ -120,7 +107,7 @@ async function fileUploader( | ||||
|       } | ||||
|  | ||||
|       if (albumId && res.id) { | ||||
|         await addAssetsToAlbum(albumId, [res.id], sharedKey); | ||||
|         await addAssetsToAlbum(albumId, [res.id]); | ||||
|       } | ||||
|  | ||||
|       setTimeout(() => { | ||||
|   | ||||
| @@ -174,7 +174,7 @@ | ||||
|   }; | ||||
|  | ||||
|   const handleSelectFromComputer = async () => { | ||||
|     await openFileUploadDialog(album.id, ''); | ||||
|     await openFileUploadDialog(album.id); | ||||
|     timelineInteractionStore.clearMultiselect(); | ||||
|     viewMode = ViewMode.VIEW; | ||||
|   }; | ||||
|   | ||||
| @@ -8,7 +8,6 @@ | ||||
| {#if data.asset && data.key} | ||||
|   <AssetViewer | ||||
|     asset={data.asset} | ||||
|     publicSharedKey={data.key} | ||||
|     showNavigation={false} | ||||
|     on:previous={() => null} | ||||
|     on:next={() => null} | ||||
|   | ||||
| @@ -17,11 +17,16 @@ | ||||
|   import { loadFeatureFlags } from '$lib/stores/feature-flags.store'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; | ||||
|   import { api } from '@api'; | ||||
|  | ||||
|   let showNavigationLoadingBar = false; | ||||
|   export let data: LayoutData; | ||||
|   let albumId: string | undefined; | ||||
|  | ||||
|   if ($page.route.id?.startsWith('/(user)/share/[key]')) { | ||||
|     api.setKey($page.params.key); | ||||
|   } | ||||
|  | ||||
|   beforeNavigate(() => { | ||||
|     showNavigationLoadingBar = true; | ||||
|   }); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user