1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-07 20:36:48 +01:00

refactor(web): shared link key auth (#3855)

This commit is contained in:
Jason Rasmussen 2023-08-25 00:03:28 -04:00 committed by GitHub
parent 10c2bda3a9
commit 9bbef4a97b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 115 additions and 108 deletions

View file

@ -39,6 +39,11 @@ export class ImmichApi {
public userApi: UserApi; public userApi: UserApi;
private config: Configuration; private config: Configuration;
private key?: string;
get isSharedLink() {
return !!this.key;
}
constructor(params: ConfigurationParameters) { constructor(params: ConfigurationParameters) {
this.config = new Configuration(params); this.config = new Configuration(params);
@ -73,6 +78,14 @@ export class ImmichApi {
return (this.config.basePath || BASE_PATH) + toPathString(url); 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) { public setAccessToken(accessToken: string) {
this.config.accessToken = accessToken; this.config.accessToken = accessToken;
} }
@ -85,14 +98,14 @@ export class ImmichApi {
this.config.basePath = baseUrl; 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}`; 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}`; 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'>) { public getProfileImageUrl(...[userId]: ApiParams<typeof UserApiFp, 'getProfileImage'>) {

View file

@ -27,13 +27,13 @@
let { isViewing: showAssetViewer } = assetViewingStore; 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 assetInteractionStore = createAssetInteractionStore();
const { isMultiSelectState, selectedAssets } = assetInteractionStore; const { isMultiSelectState, selectedAssets } = assetInteractionStore;
dragAndDropFilesStore.subscribe((value) => { dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) { if (value.isDragging && value.files.length > 0) {
fileUploadHandler(value.files, album.id, sharedLink.key); fileUploadHandler(value.files, album.id);
dragAndDropFilesStore.set({ isDragging: false, files: [] }); dragAndDropFilesStore.set({ isDragging: false, files: [] });
} }
}); });
@ -88,7 +88,7 @@
}; };
const downloadAlbum = async () => { const downloadAlbum = async () => {
await downloadArchive(`${album.albumName}.zip`, { albumId: album.id }, sharedLink.key); await downloadArchive(`${album.albumName}.zip`, { albumId: album.id });
}; };
</script> </script>
@ -97,7 +97,7 @@
<AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}>
<SelectAllAssets {assetStore} {assetInteractionStore} /> <SelectAllAssets {assetStore} {assetInteractionStore} />
{#if sharedLink.allowDownload} {#if sharedLink.allowDownload}
<DownloadAction filename="{album.albumName}.zip" sharedLinkKey={sharedLink.key} /> <DownloadAction filename="{album.albumName}.zip" />
{/if} {/if}
</AssetSelectControlBar> </AssetSelectControlBar>
{:else} {:else}
@ -117,7 +117,7 @@
{#if sharedLink.allowUpload} {#if sharedLink.allowUpload}
<CircleIconButton <CircleIconButton
title="Add Photos" title="Add Photos"
on:click={() => openFileUploadDialog(album.id, sharedLink.key)} on:click={() => openFileUploadDialog(album.id)}
logo={FileImagePlusOutline} logo={FileImagePlusOutline}
/> />
{/if} {/if}
@ -135,7 +135,7 @@
<main <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" 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"> <section class="pt-24">
<!-- ALBUM TITLE --> <!-- ALBUM TITLE -->
<p <p

View file

@ -26,7 +26,6 @@
export let assetStore: AssetStore | null = null; export let assetStore: AssetStore | null = null;
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let publicSharedKey = '';
export let showNavigation = true; export let showNavigation = true;
export let sharedLink: SharedLinkResponseDto | undefined = undefined; export let sharedLink: SharedLinkResponseDto | undefined = undefined;
@ -72,6 +71,10 @@
$: asset.id && !sharedLink && getAllAlbums(); // Update the album information when the asset ID changes $: asset.id && !sharedLink && getAllAlbums(); // Update the album information when the asset ID changes
const getAllAlbums = async () => { const getAllAlbums = async () => {
if (api.isSharedLink) {
return;
}
try { try {
const { data } = await api.albumApi.getAllAlbums({ assetId: asset.id }); const { data } = await api.albumApi.getAllAlbums({ assetId: asset.id });
appearsInAlbums = data; appearsInAlbums = data;
@ -84,7 +87,9 @@
switch (key) { switch (key) {
case 'a': case 'a':
case 'A': case 'A':
if (shiftKey) toggleArchive(); if (shiftKey) {
toggleArchive();
}
return; return;
case 'ArrowLeft': case 'ArrowLeft':
navigateAssetBackward(); navigateAssetBackward();
@ -94,7 +99,9 @@
return; return;
case 'd': case 'd':
case 'D': case 'D':
if (shiftKey) downloadFile(asset, publicSharedKey); if (shiftKey) {
downloadFile(asset);
}
return; return;
case 'Delete': case 'Delete':
isShowDeleteConfirmation = true; isShowDeleteConfirmation = true;
@ -272,7 +279,7 @@
showDownloadButton={shouldShowDownloadButton} showDownloadButton={shouldShowDownloadButton}
on:goBack={closeViewer} on:goBack={closeViewer}
on:showDetail={showDetailInfoHandler} on:showDetail={showDetailInfoHandler}
on:download={() => downloadFile(asset, publicSharedKey)} on:download={() => downloadFile(asset)}
on:delete={() => (isShowDeleteConfirmation = true)} on:delete={() => (isShowDeleteConfirmation = true)}
on:favorite={toggleFavorite} on:favorite={toggleFavorite}
on:addToAlbum={() => openAlbumPicker(false)} on:addToAlbum={() => openAlbumPicker(false)}
@ -304,7 +311,6 @@
{:else if asset.type === AssetTypeEnum.Image} {:else if asset.type === AssetTypeEnum.Image}
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId} {#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
<VideoViewer <VideoViewer
{publicSharedKey}
assetId={asset.livePhotoVideoId} assetId={asset.livePhotoVideoId}
on:close={closeViewer} on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
@ -312,12 +318,12 @@
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || asset.originalPath
.toLowerCase() .toLowerCase()
.endsWith('.insp')} .endsWith('.insp')}
<PanoramaViewer {publicSharedKey} {asset} /> <PanoramaViewer {asset} />
{:else} {:else}
<PhotoViewer {publicSharedKey} {asset} on:close={closeViewer} /> <PhotoViewer {asset} on:close={closeViewer} />
{/if} {/if}
{:else} {:else}
<VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} /> <VideoViewer assetId={asset.id} on:close={closeViewer} />
{/if} {/if}
{/key} {/key}
</div> </div>
@ -338,7 +344,6 @@
<DetailPanel <DetailPanel
{asset} {asset}
albums={appearsInAlbums} albums={appearsInAlbums}
{sharedLink}
on:close={() => ($isShowDetail = false)} on:close={() => ($isShowDetail = false)}
on:close-viewer={handleCloseViewer} on:close-viewer={handleCloseViewer}
on:description-focus-in={disableKeyDownEvent} on:description-focus-in={disableKeyDownEvent}

View file

@ -9,21 +9,20 @@
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte'; import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte'; import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
import { createEventDispatcher } from '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 { asByteUnitString } from '../../utils/byte-units';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import { getAssetFilename } from '$lib/utils/asset-utils'; import { getAssetFilename } from '$lib/utils/asset-utils';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let albums: AlbumResponseDto[] = []; export let albums: AlbumResponseDto[] = [];
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
let textarea: HTMLTextAreaElement; let textarea: HTMLTextAreaElement;
let description: string; let description: string;
$: { $: {
// Get latest description from server // Get latest description from server
if (asset.id && !sharedLink) { if (asset.id && !api.isSharedLink) {
api.assetApi.getAssetById({ id: asset.id }).then((res) => { api.assetApi.getAssetById({ id: asset.id }).then((res) => {
people = res.data?.people || []; people = res.data?.people || [];
textarea.value = res.data?.exifInfo?.description || ''; textarea.value = res.data?.exifInfo?.description || '';
@ -91,13 +90,15 @@
<p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p> <p class="text-lg text-immich-fg dark:text-immich-dark-fg">Info</p>
</div> </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 <textarea
bind:this={textarea} bind:this={textarea}
class="max-h-[500px] 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" 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'} 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:focusin={handleFocusIn}
on:focusout={handleFocusOut} on:focusout={handleFocusOut}
on:input={autoGrowHeight} on:input={autoGrowHeight}
@ -106,7 +107,7 @@
/> />
</section> </section>
{#if people.length > 0} {#if !api.isSharedLink && people.length > 0}
<section class="px-4 py-4 text-sm"> <section class="px-4 py-4 text-sm">
<h2>PEOPLE</h2> <h2>PEOPLE</h2>

View file

@ -4,14 +4,16 @@
import { api, AssetResponseDto } from '@api'; import { api, AssetResponseDto } from '@api';
import View360, { EquirectProjection } from '@egjs/svelte-view360'; import View360, { EquirectProjection } from '@egjs/svelte-view360';
import './panorama-viewer.css'; import './panorama-viewer.css';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let publicSharedKey = '';
let dataUrl = ''; let dataUrl = '';
let errorMessage = ''; let errorMessage = '';
const loadAssetData = async () => { const loadAssetData = async () => {
try { try {
const { data } = await api.assetApi.serveFile( 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' }, { responseType: 'blob' },
); );
if (data instanceof Blob) { if (data instanceof Blob) {

View file

@ -8,12 +8,10 @@
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
export let asset: AssetResponseDto; export let asset: AssetResponseDto;
export let publicSharedKey = '';
export let element: HTMLDivElement | undefined = undefined; export let element: HTMLDivElement | undefined = undefined;
let imgElement: HTMLDivElement; let imgElement: HTMLDivElement;
let assetData: string; let assetData: string;
let copyImageToClipboard: (src: string) => Promise<Blob>; let copyImageToClipboard: (src: string) => Promise<Blob>;
let canCopyImagesToClipboard: () => boolean; let canCopyImagesToClipboard: () => boolean;
@ -28,7 +26,7 @@
const loadAssetData = async () => { const loadAssetData = async () => {
try { try {
const { data } = await api.assetApi.serveFile( 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', responseType: 'blob',
}, },

View file

@ -7,7 +7,6 @@
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
export let assetId: string; export let assetId: string;
export let publicSharedKey: string | undefined = undefined;
let isVideoLoading = true; let isVideoLoading = true;
const dispatch = createEventDispatcher<{ onVideoEnded: void }>(); const dispatch = createEventDispatcher<{ onVideoEnded: void }>();
@ -37,7 +36,7 @@
bind:volume={$videoViewerVolume} bind:volume={$videoViewerVolume}
poster={api.getAssetThumbnailUrl(assetId, ThumbnailFormat.Jpeg)} 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" /> <track kind="captions" />
</video> </video>

View file

@ -27,7 +27,6 @@
export let selectionCandidate = false; export let selectionCandidate = false;
export let disabled = false; export let disabled = false;
export let readonly = false; export let readonly = false;
export let publicSharedKey: string | undefined = undefined;
export let showArchiveIcon = false; export let showArchiveIcon = false;
let mouseOver = false; let mouseOver = false;
@ -118,13 +117,13 @@
/> />
<!-- Favorite asset star --> <!-- Favorite asset star -->
{#if asset.isFavorite && !publicSharedKey} {#if !api.isSharedLink && asset.isFavorite}
<div class="absolute bottom-2 left-2 z-10"> <div class="absolute bottom-2 left-2 z-10">
<Heart size="24" class="text-white" /> <Heart size="24" class="text-white" />
</div> </div>
{/if} {/if}
{#if showArchiveIcon && asset.isArchived} {#if !api.isSharedLink && showArchiveIcon && asset.isArchived}
<div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10"> <div class="absolute {asset.isFavorite ? 'bottom-10' : 'bottom-2'} left-2 z-10">
<ArchiveArrowDownOutline size="24" class="text-white" /> <ArchiveArrowDownOutline size="24" class="text-white" />
</div> </div>
@ -140,7 +139,7 @@
{#if asset.resized} {#if asset.resized}
<ImageThumbnail <ImageThumbnail
url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)} url={api.getAssetThumbnailUrl(asset.id, format)}
altText={asset.originalFileName} altText={asset.originalFileName}
widthStyle="{width}px" widthStyle="{width}px"
heightStyle="{height}px" heightStyle="{height}px"
@ -156,7 +155,7 @@
{#if asset.type === AssetTypeEnum.Video} {#if asset.type === AssetTypeEnum.Video}
<div class="absolute top-0 h-full w-full"> <div class="absolute top-0 h-full w-full">
<VideoThumbnail <VideoThumbnail
url={api.getAssetFileUrl(asset.id, false, true, publicSharedKey)} url={api.getAssetFileUrl(asset.id, false, true)}
enablePlayback={mouseOver} enablePlayback={mouseOver}
curve={selected} curve={selected}
durationInSeconds={timeToSeconds(asset.duration)} durationInSeconds={timeToSeconds(asset.duration)}
@ -167,7 +166,7 @@
{#if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId} {#if asset.type === AssetTypeEnum.Image && asset.livePhotoVideoId}
<div class="absolute top-0 h-full w-full"> <div class="absolute top-0 h-full w-full">
<VideoThumbnail <VideoThumbnail
url={api.getAssetFileUrl(asset.livePhotoVideoId, false, true, publicSharedKey)} url={api.getAssetFileUrl(asset.livePhotoVideoId, false, true)}
pauseIcon={MotionPauseOutline} pauseIcon={MotionPauseOutline}
playIcon={MotionPlayOutline} playIcon={MotionPlayOutline}
showTime={false} showTime={false}

View file

@ -6,7 +6,6 @@
import { getAssetControlContext } from '../asset-select-control-bar.svelte'; import { getAssetControlContext } from '../asset-select-control-bar.svelte';
export let filename = 'immich.zip'; export let filename = 'immich.zip';
export let sharedLinkKey: string | undefined = undefined;
export let menuItem = false; export let menuItem = false;
const { getAssets, clearSelect } = getAssetControlContext(); const { getAssets, clearSelect } = getAssetControlContext();
@ -15,12 +14,12 @@
const assets = Array.from(getAssets()); const assets = Array.from(getAssets());
if (assets.length === 1) { if (assets.length === 1) {
clearSelect(); clearSelect();
await downloadFile(assets[0], sharedLinkKey); await downloadFile(assets[0]);
return; return;
} }
clearSelect(); clearSelect();
await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, sharedLinkKey); await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) });
}; };
</script> </script>

View file

@ -20,7 +20,7 @@
assetIdsDto: { assetIdsDto: {
assetIds: Array.from(getAssets()).map((asset) => asset.id), assetIds: Array.from(getAssets()).map((asset) => asset.id),
}, },
key: sharedLink.key, key: api.getKey(),
}); });
for (const result of results) { for (const result of results) {

View file

@ -21,7 +21,6 @@
export let isSelectionMode = false; export let isSelectionMode = false;
export let viewport: Viewport; export let viewport: Viewport;
export let singleSelect = false; export let singleSelect = false;
export let publicSharedKey: string | undefined = undefined;
export let assetStore: AssetStore; export let assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore; export let assetInteractionStore: AssetInteractionStore;
@ -96,7 +95,7 @@
return; return;
} }
assetViewingStore.setAssetId(asset.id, publicSharedKey); assetViewingStore.setAssetId(asset.id);
}; };
const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets }); const handleSelectGroup = (title: string, assets: AssetResponseDto[]) => dispatch('select', { title, assets });
@ -189,7 +188,6 @@
disabled={$assetStore.albumAssets.has(asset.id)} disabled={$assetStore.albumAssets.has(asset.id)}
thumbnailWidth={box.width} thumbnailWidth={box.width}
thumbnailHeight={box.height} thumbnailHeight={box.height}
{publicSharedKey}
/> />
</div> </div>
{/each} {/each}

View file

@ -23,7 +23,7 @@
export let assetStore: AssetStore; export let assetStore: AssetStore;
export let assetInteractionStore: AssetInteractionStore; export let assetInteractionStore: AssetInteractionStore;
export let removeAction: AssetAction | null = null; export let removeAction: AssetAction | null = null;
export let publicSharedKey: string | undefined = undefined;
const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } = const { assetSelectionCandidates, assetSelectionStart, selectedGroup, selectedAssets, isMultiSelectState } =
assetInteractionStore; assetInteractionStore;
const viewport: Viewport = { width: 0, height: 0 }; const viewport: Viewport = { width: 0, height: 0 };
@ -97,7 +97,7 @@
const handlePrevious = async () => { const handlePrevious = async () => {
const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id); const previousAsset = await assetStore.getPreviousAssetId($viewingAsset.id);
if (previousAsset) { if (previousAsset) {
assetViewingStore.setAssetId(previousAsset, publicSharedKey); assetViewingStore.setAssetId(previousAsset);
} }
return !!previousAsset; return !!previousAsset;
@ -106,7 +106,7 @@
const handleNext = async () => { const handleNext = async () => {
const nextAsset = await assetStore.getNextAssetId($viewingAsset.id); const nextAsset = await assetStore.getNextAssetId($viewingAsset.id);
if (nextAsset) { if (nextAsset) {
assetViewingStore.setAssetId(nextAsset, publicSharedKey); assetViewingStore.setAssetId(nextAsset);
} }
return !!nextAsset; return !!nextAsset;
@ -349,7 +349,6 @@
bucketDate={bucket.bucketDate} bucketDate={bucket.bucketDate}
bucketHeight={bucket.bucketHeight} bucketHeight={bucket.bucketHeight}
{viewport} {viewport}
{publicSharedKey}
/> />
{/if} {/if}
</div> </div>
@ -371,7 +370,6 @@
on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)} on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)}
on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)} on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)}
on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)} on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)}
{publicSharedKey}
/> />
{/if} {/if}
</Portal> </Portal>

View file

@ -15,7 +15,6 @@
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte'; import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import SelectAll from 'svelte-material-icons/SelectAll.svelte'; import SelectAll from 'svelte-material-icons/SelectAll.svelte';
import ImmichLogo from '../shared-components/immich-logo.svelte'; import ImmichLogo from '../shared-components/immich-logo.svelte';
import { notificationController, NotificationType } from '../shared-components/notification/notification'; import { notificationController, NotificationType } from '../shared-components/notification/notification';
import { handleError } from '../../utils/handle-error'; import { handleError } from '../../utils/handle-error';
@ -35,23 +34,23 @@
}); });
const downloadAssets = async () => { 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[] = []) => { const handleUploadAssets = async (files: File[] = []) => {
try { try {
let results: (string | undefined)[] = []; let results: (string | undefined)[] = [];
if (!files || files.length === 0 || !Array.isArray(files)) { if (!files || files.length === 0 || !Array.isArray(files)) {
results = await openFileUploadDialog(undefined, sharedLink.key); results = await openFileUploadDialog(undefined);
} else { } else {
results = await fileUploadHandler(files, undefined, sharedLink.key); results = await fileUploadHandler(files, undefined);
} }
const { data } = await api.sharedLinkApi.addSharedLinkAssets({ const { data } = await api.sharedLinkApi.addSharedLinkAssets({
id: sharedLink.id, id: sharedLink.id,
assetIdsDto: { assetIdsDto: {
assetIds: results.filter((id) => !!id) as string[], assetIds: results.filter((id) => !!id) as string[],
}, },
key: sharedLink.key, key: api.getKey(),
}); });
const added = data.filter((item) => item.success).length; const added = data.filter((item) => item.success).length;
@ -75,7 +74,7 @@
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
<CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} /> <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} />
{#if sharedLink?.allowDownload} {#if sharedLink?.allowDownload}
<DownloadAction filename="immich-shared.zip" sharedLinkKey={sharedLink.key} /> <DownloadAction filename="immich-shared.zip" />
{/if} {/if}
{#if isOwned} {#if isOwned}
<RemoveFromSharedLink bind:sharedLink /> <RemoveFromSharedLink bind:sharedLink />
@ -106,6 +105,6 @@
</ControlAppBar> </ControlAppBar>
{/if} {/if}
<section class="my-[160px] flex flex-col px-6 sm:px-12 md:px-24 lg:px-40"> <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>
</section> </section>

View file

@ -2,14 +2,13 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { handleError } from '$lib/utils/handle-error'; 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 AssetViewer from '../../asset-viewer/asset-viewer.svelte';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { getThumbnailSize } from '$lib/utils/thumbnail-util'; import { getThumbnailSize } from '$lib/utils/thumbnail-util';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
export let assets: AssetResponseDto[]; export let assets: AssetResponseDto[];
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
export let selectedAssets: Set<AssetResponseDto> = new Set(); export let selectedAssets: Set<AssetResponseDto> = new Set();
export let disableAssetSelect = false; export let disableAssetSelect = false;
export let showArchiveIcon = false; export let showArchiveIcon = false;
@ -90,7 +89,6 @@
{asset} {asset}
{thumbnailSize} {thumbnailSize}
readonly={disableAssetSelect} readonly={disableAssetSelect}
publicSharedKey={sharedLink?.key}
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp} format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))} on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
on:select={selectAssetHandler} on:select={selectAssetHandler}
@ -106,8 +104,6 @@
{#if $showAssetViewer} {#if $showAssetViewer}
<AssetViewer <AssetViewer
asset={selectedAsset} asset={selectedAsset}
publicSharedKey={sharedLink?.key}
{sharedLink}
on:previous={navigateAssetBackward} on:previous={navigateAssetBackward}
on:next={navigateAssetForward} on:next={navigateAssetForward}
on:close={closeViewer} on:close={closeViewer}

View file

@ -5,8 +5,8 @@ function createAssetViewingStore() {
const viewingAssetStoreState = writable<AssetResponseDto>(); const viewingAssetStoreState = writable<AssetResponseDto>();
const viewState = writable<boolean>(false); const viewState = writable<boolean>(false);
const setAssetId = async (id: string, key?: string) => { const setAssetId = async (id: string) => {
const { data } = await api.assetApi.getAssetById({ id, key }); const { data } = await api.assetApi.getAssetById({ id, key: api.getKey() });
viewingAssetStoreState.set(data); viewingAssetStoreState.set(data);
viewState.set(true); viewState.set(true);
}; };

View file

@ -58,7 +58,10 @@ export class AssetStore {
this.assetToBucket = {}; this.assetToBucket = {};
this.albumAssets = new Set(); 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) => { this.buckets = buckets.map((bucket) => {
const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10); const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10);
@ -107,7 +110,11 @@ export class AssetStore {
bucket.cancelToken = new AbortController(); bucket.cancelToken = new AbortController();
const { data: assets } = await api.assetApi.getByTimeBucket( const { data: assets } = await api.assetApi.getByTimeBucket(
{ ...this.options, timeBucket: bucketDate }, {
...this.options,
timeBucket: bucketDate,
key: api.getKey(),
},
{ signal: bucket.cancelToken.signal }, { signal: bucket.cancelToken.signal },
); );
@ -117,7 +124,7 @@ export class AssetStore {
albumId: this.albumId, albumId: this.albumId,
timeBucket: bucketDate, timeBucket: bucketDate,
size: this.options.size, size: this.options.size,
key: this.options.key, key: api.getKey(),
}, },
{ signal: bucket.cancelToken.signal }, { signal: bucket.cancelToken.signal },
); );

View file

@ -3,12 +3,14 @@ import { downloadManager } from '$lib/stores/download';
import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api'; import { api, BulkIdResponseDto, AssetResponseDto, DownloadResponseDto, DownloadInfoDto } from '@api';
import { handleError } from './handle-error'; import { handleError } from './handle-error';
export const addAssetsToAlbum = async ( export const addAssetsToAlbum = async (albumId: string, assetIds: Array<string>): Promise<BulkIdResponseDto[]> =>
albumId: string, api.albumApi
assetIds: Array<string>, .addAssetsToAlbum({
key: string | undefined = undefined, id: albumId,
): Promise<BulkIdResponseDto[]> => bulkIdsDto: { ids: assetIds },
api.albumApi.addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetIds }, key }).then(({ data: results }) => { key: api.getKey(),
})
.then(({ data: results }) => {
const count = results.filter(({ success }) => success).length; const count = results.filter(({ success }) => success).length;
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
@ -32,11 +34,11 @@ const downloadBlob = (data: Blob, filename: string) => {
URL.revokeObjectURL(url); 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; let downloadInfo: DownloadResponseDto | null = null;
try { try {
const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key }); const { data } = await api.assetApi.getDownloadInfo({ downloadInfoDto: options, key: api.getKey() });
downloadInfo = data; downloadInfo = data;
} catch (error) { } catch (error) {
handleError(error, 'Unable to download files'); handleError(error, 'Unable to download files');
@ -61,7 +63,7 @@ export const downloadArchive = async (fileName: string, options: DownloadInfoDto
try { try {
const { data } = await api.assetApi.downloadArchive( const { data } = await api.assetApi.downloadArchive(
{ assetIdsDto: { assetIds: archive.assetIds }, key }, { assetIdsDto: { assetIds: archive.assetIds }, key: api.getKey() },
{ {
responseType: 'blob', responseType: 'blob',
signal: abort.signal, 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 = [ const assets = [
{ {
filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`, filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`,
@ -104,7 +106,7 @@ export const downloadFile = async (asset: AssetResponseDto, key?: string) => {
downloadManager.add(downloadKey, size, abort); downloadManager.add(downloadKey, size, abort);
const { data } = await api.assetApi.downloadFile( const { data } = await api.assetApi.downloadFile(
{ id, key }, { id, key: api.getKey() },
{ {
responseType: 'blob', responseType: 'blob',
onDownloadProgress: (event: ProgressEvent) => { onDownloadProgress: (event: ProgressEvent) => {

View file

@ -14,10 +14,7 @@ const getExtensions = async () => {
return _extensions; return _extensions;
}; };
export const openFileUploadDialog = async ( export const openFileUploadDialog = async (albumId: string | undefined = undefined) => {
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
) => {
const extensions = await getExtensions(); const extensions = await getExtensions();
return new Promise<(string | undefined)[]>((resolve, reject) => { return new Promise<(string | undefined)[]>((resolve, reject) => {
@ -34,7 +31,7 @@ export const openFileUploadDialog = async (
} }
const files = Array.from(target.files); const files = Array.from(target.files);
resolve(fileUploadHandler(files, albumId, sharedKey)); resolve(fileUploadHandler(files, albumId));
}; };
fileSelector.click(); fileSelector.click();
@ -45,18 +42,14 @@ export const openFileUploadDialog = async (
}); });
}; };
export const fileUploadHandler = async ( export const fileUploadHandler = async (files: File[], albumId: string | undefined = undefined) => {
files: File[],
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
) => {
const extensions = await getExtensions(); const extensions = await getExtensions();
const iterable = { const iterable = {
files: files.filter((file) => extensions.some((ext) => file.name.toLowerCase().endsWith(ext)))[Symbol.iterator](), files: files.filter((file) => extensions.some((ext) => file.name.toLowerCase().endsWith(ext)))[Symbol.iterator](),
async *[Symbol.asyncIterator]() { async *[Symbol.asyncIterator]() {
for (const file of this.files) { 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 // TODO: should probably use the @api SDK
async function fileUploader( async function fileUploader(asset: File, albumId: string | undefined = undefined): Promise<string | undefined> {
asset: File,
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
): Promise<string | undefined> {
const formData = new FormData(); const formData = new FormData();
const fileCreatedAt = new Date(asset.lastModified).toISOString(); const fileCreatedAt = new Date(asset.lastModified).toISOString();
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified; const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
@ -103,9 +92,7 @@ async function fileUploader(
}); });
const response = await axios.post('/api/asset/upload', formData, { const response = await axios.post('/api/asset/upload', formData, {
params: { params: { key: api.getKey() },
key: sharedKey,
},
onUploadProgress: (event) => { onUploadProgress: (event) => {
const percentComplete = Math.floor((event.loaded / event.total) * 100); const percentComplete = Math.floor((event.loaded / event.total) * 100);
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete); uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
@ -120,7 +107,7 @@ async function fileUploader(
} }
if (albumId && res.id) { if (albumId && res.id) {
await addAssetsToAlbum(albumId, [res.id], sharedKey); await addAssetsToAlbum(albumId, [res.id]);
} }
setTimeout(() => { setTimeout(() => {

View file

@ -174,7 +174,7 @@
}; };
const handleSelectFromComputer = async () => { const handleSelectFromComputer = async () => {
await openFileUploadDialog(album.id, ''); await openFileUploadDialog(album.id);
timelineInteractionStore.clearMultiselect(); timelineInteractionStore.clearMultiselect();
viewMode = ViewMode.VIEW; viewMode = ViewMode.VIEW;
}; };

View file

@ -8,7 +8,6 @@
{#if data.asset && data.key} {#if data.asset && data.key}
<AssetViewer <AssetViewer
asset={data.asset} asset={data.asset}
publicSharedKey={data.key}
showNavigation={false} showNavigation={false}
on:previous={() => null} on:previous={() => null}
on:next={() => null} on:next={() => null}

View file

@ -17,11 +17,16 @@
import { loadFeatureFlags } from '$lib/stores/feature-flags.store'; import { loadFeatureFlags } from '$lib/stores/feature-flags.store';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { api } from '@api';
let showNavigationLoadingBar = false; let showNavigationLoadingBar = false;
export let data: LayoutData; export let data: LayoutData;
let albumId: string | undefined; let albumId: string | undefined;
if ($page.route.id?.startsWith('/(user)/share/[key]')) {
api.setKey($page.params.key);
}
beforeNavigate(() => { beforeNavigate(() => {
showNavigationLoadingBar = true; showNavigationLoadingBar = true;
}); });