mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
refactor(server, web): create shared link (#2879)
* refactor: shared links * chore: open api * fix: tsc error
This commit is contained in:
@@ -7,16 +7,16 @@ import {
|
||||
Configuration,
|
||||
ConfigurationParameters,
|
||||
JobApi,
|
||||
JobName,
|
||||
OAuthApi,
|
||||
PersonApi,
|
||||
PartnerApi,
|
||||
PersonApi,
|
||||
SearchApi,
|
||||
ServerInfoApi,
|
||||
ShareApi,
|
||||
SharedLinkApi,
|
||||
SystemConfigApi,
|
||||
UserApi,
|
||||
UserApiFp,
|
||||
JobName
|
||||
UserApiFp
|
||||
} from './open-api';
|
||||
import { BASE_PATH } from './open-api/base';
|
||||
import { DUMMY_BASE_URL, toPathString } from './open-api/common';
|
||||
@@ -32,7 +32,7 @@ export class ImmichApi {
|
||||
public partnerApi: PartnerApi;
|
||||
public searchApi: SearchApi;
|
||||
public serverInfoApi: ServerInfoApi;
|
||||
public shareApi: ShareApi;
|
||||
public sharedLinkApi: SharedLinkApi;
|
||||
public personApi: PersonApi;
|
||||
public systemConfigApi: SystemConfigApi;
|
||||
public userApi: UserApi;
|
||||
@@ -51,7 +51,7 @@ export class ImmichApi {
|
||||
this.partnerApi = new PartnerApi(this.config);
|
||||
this.searchApi = new SearchApi(this.config);
|
||||
this.serverInfoApi = new ServerInfoApi(this.config);
|
||||
this.shareApi = new ShareApi(this.config);
|
||||
this.sharedLinkApi = new SharedLinkApi(this.config);
|
||||
this.personApi = new PersonApi(this.config);
|
||||
this.systemConfigApi = new SystemConfigApi(this.config);
|
||||
this.userApi = new UserApi(this.config);
|
||||
|
||||
1045
web/src/api/open-api/api.ts
generated
1045
web/src/api/open-api/api.ts
generated
File diff suppressed because it is too large
Load Diff
@@ -31,7 +31,7 @@
|
||||
});
|
||||
|
||||
const getSharedLinks = async () => {
|
||||
const { data } = await api.shareApi.getAllSharedLinks();
|
||||
const { data } = await api.sharedLinkApi.getAllSharedLinks();
|
||||
|
||||
sharedLinks = data.filter((link) => link.album?.id === album.id);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,65 @@
|
||||
<script lang="ts">
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { AssetResponseDto, SharedLinkResponseDto, api } from '@api';
|
||||
import { SharedLinkResponseDto, api } from '@api';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import ConfirmDialogue from '../../shared-components/confirm-dialogue.svelte';
|
||||
import { getAssetControlContext } from '../asset-select-control-bar.svelte';
|
||||
import {
|
||||
NotificationType,
|
||||
notificationController
|
||||
} from '../../shared-components/notification/notification';
|
||||
import { handleError } from '../../../utils/handle-error';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let allAssets: AssetResponseDto[];
|
||||
|
||||
let removing = false;
|
||||
|
||||
const { getAssets, clearSelect } = getAssetControlContext();
|
||||
|
||||
const handleRemoveAssetsFromSharedLink = async () => {
|
||||
if (window.confirm('Do you want to remove selected assets from the shared link?')) {
|
||||
// TODO: Rename API method or change functionality. The assetIds passed
|
||||
// in are kept instead of removed.
|
||||
const assetsToKeep = allAssets.filter((a) => !getAssets().has(a));
|
||||
await api.assetApi.removeAssetsFromSharedLink({
|
||||
removeAssetsDto: {
|
||||
assetIds: assetsToKeep.map((a) => a.id)
|
||||
const handleRemove = async () => {
|
||||
try {
|
||||
const { data: results } = await api.sharedLinkApi.removeSharedLinkAssets({
|
||||
id: sharedLink.id,
|
||||
assetIdsDto: {
|
||||
assetIds: Array.from(getAssets()).map((asset) => asset.id)
|
||||
},
|
||||
key: sharedLink?.key
|
||||
key: sharedLink.key
|
||||
});
|
||||
|
||||
for (const result of results) {
|
||||
if (!result.success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sharedLink.assets = sharedLink.assets.filter((asset) => asset.id !== result.assetId);
|
||||
}
|
||||
|
||||
const count = results.filter((item) => item.success).length;
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: `Removed ${count} assets`
|
||||
});
|
||||
|
||||
sharedLink.assets = assetsToKeep;
|
||||
clearSelect();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to remove assets from shared link');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<CircleIconButton
|
||||
title="Remove from album"
|
||||
on:click={handleRemoveAssetsFromSharedLink}
|
||||
title="Remove from shared link"
|
||||
on:click={() => (removing = true)}
|
||||
logo={DeleteOutline}
|
||||
/>
|
||||
|
||||
{#if removing}
|
||||
<ConfirmDialogue
|
||||
title="Remove Assets?"
|
||||
prompt="Are you sure you want to remove {getAssets().size} asset(s) from this shared link?"
|
||||
confirmText="Remove"
|
||||
on:confirm={() => handleRemove()}
|
||||
on:cancel={() => (removing = false)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
import { handleError } from '../../utils/handle-error';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
@@ -26,43 +27,40 @@
|
||||
$: assets = sharedLink.assets;
|
||||
$: isMultiSelectionMode = selectedAssets.size > 0;
|
||||
|
||||
const clearMultiSelectAssetAssetHandler = () => {
|
||||
selectedAssets = new Set();
|
||||
};
|
||||
|
||||
const downloadAssets = async () => {
|
||||
await bulkDownload('immich-shared', assets, undefined, sharedLink?.key);
|
||||
await bulkDownload('immich-shared', assets, undefined, sharedLink.key);
|
||||
};
|
||||
|
||||
const handleUploadAssets = async () => {
|
||||
try {
|
||||
const results = await openFileUploadDialog(undefined, sharedLink?.key);
|
||||
const results = await openFileUploadDialog(undefined, sharedLink.key);
|
||||
|
||||
const assetIds = results.filter((id) => !!id) as string[];
|
||||
|
||||
await api.assetApi.addAssetsToSharedLink({
|
||||
addAssetsDto: {
|
||||
assetIds
|
||||
const { data } = await api.sharedLinkApi.addSharedLinkAssets({
|
||||
id: sharedLink.id,
|
||||
assetIdsDto: {
|
||||
assetIds: results.filter((id) => !!id) as string[]
|
||||
},
|
||||
key: sharedLink?.key
|
||||
key: sharedLink.key
|
||||
});
|
||||
|
||||
const added = data.filter((item) => item.success).length;
|
||||
|
||||
notificationController.show({
|
||||
message: `Successfully add ${assetIds.length} to the shared link`,
|
||||
message: `Added ${added} assets`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('handleUploadAssets', e);
|
||||
handleError(e, 'Unable to add assets to shared link');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
{#if isMultiSelectionMode}
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={clearMultiSelectAssetAssetHandler}>
|
||||
<AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}>
|
||||
<DownloadAction filename="immich-shared" sharedLinkKey={sharedLink.key} />
|
||||
{#if isOwned}
|
||||
<RemoveFromSharedLink bind:sharedLink allAssets={assets} />
|
||||
<RemoveFromSharedLink bind:sharedLink />
|
||||
{/if}
|
||||
</AssetSelectControlBar>
|
||||
{:else}
|
||||
|
||||
@@ -7,31 +7,31 @@
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import {
|
||||
AlbumResponseDto,
|
||||
api,
|
||||
AssetResponseDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkType,
|
||||
api
|
||||
SharedLinkType
|
||||
} from '@api';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import Link from 'svelte-material-icons/Link.svelte';
|
||||
import BaseModal from '../base-modal.svelte';
|
||||
import type { ImmichDropDownOption } from '../dropdown-button.svelte';
|
||||
import DropdownButton from '../dropdown-button.svelte';
|
||||
import { NotificationType, notificationController } from '../notification/notification';
|
||||
import { notificationController, NotificationType } from '../notification/notification';
|
||||
|
||||
export let shareType: SharedLinkType;
|
||||
export let sharedAssets: AssetResponseDto[] = [];
|
||||
export let album: AlbumResponseDto | undefined = undefined;
|
||||
export let editingLink: SharedLinkResponseDto | undefined = undefined;
|
||||
|
||||
let isShowSharedLink = false;
|
||||
let expirationTime = '';
|
||||
let isAllowUpload = false;
|
||||
let sharedLink = '';
|
||||
let sharedLink: string | null = null;
|
||||
let description = '';
|
||||
let allowDownload = true;
|
||||
let allowUpload = false;
|
||||
let showExif = true;
|
||||
let expirationTime = '';
|
||||
let shouldChangeExpirationTime = false;
|
||||
let isAllowDownload = true;
|
||||
let shouldShowExif = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const expiredDateOption: ImmichDropDownOption = {
|
||||
@@ -44,9 +44,9 @@
|
||||
if (editingLink.description) {
|
||||
description = editingLink.description;
|
||||
}
|
||||
isAllowUpload = editingLink.allowUpload;
|
||||
isAllowDownload = editingLink.allowDownload;
|
||||
shouldShowExif = editingLink.showExif;
|
||||
allowUpload = editingLink.allowUpload;
|
||||
allowDownload = editingLink.allowDownload;
|
||||
showExif = editingLink.showExif;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -58,49 +58,32 @@
|
||||
: undefined;
|
||||
|
||||
try {
|
||||
if (shareType === SharedLinkType.Album && album) {
|
||||
const { data } = await api.albumApi.createAlbumSharedLink({
|
||||
createAlbumShareLinkDto: {
|
||||
albumId: album.id,
|
||||
expiresAt: expirationDate,
|
||||
allowUpload: isAllowUpload,
|
||||
description: description,
|
||||
allowDownload: isAllowDownload,
|
||||
showExif: shouldShowExif
|
||||
}
|
||||
});
|
||||
buildSharedLink(data);
|
||||
} else {
|
||||
const { data } = await api.assetApi.createAssetsSharedLink({
|
||||
createAssetsShareLinkDto: {
|
||||
assetIds: sharedAssets.map((a) => a.id),
|
||||
expiresAt: expirationDate,
|
||||
allowUpload: isAllowUpload,
|
||||
description: description,
|
||||
allowDownload: isAllowDownload,
|
||||
showExif: shouldShowExif
|
||||
}
|
||||
});
|
||||
buildSharedLink(data);
|
||||
}
|
||||
const { data } = await api.sharedLinkApi.createSharedLink({
|
||||
sharedLinkCreateDto: {
|
||||
type: shareType,
|
||||
albumId: album ? album.id : undefined,
|
||||
assetIds: sharedAssets.map((a) => a.id),
|
||||
expiresAt: expirationDate,
|
||||
allowUpload,
|
||||
description,
|
||||
allowDownload,
|
||||
showExif
|
||||
}
|
||||
});
|
||||
sharedLink = `${window.location.origin}/share/${data.key}`;
|
||||
} catch (e) {
|
||||
handleError(e, 'Failed to create shared link');
|
||||
}
|
||||
|
||||
isShowSharedLink = true;
|
||||
};
|
||||
|
||||
const buildSharedLink = (createdLink: SharedLinkResponseDto) => {
|
||||
sharedLink = `${window.location.origin}/share/${createdLink.key}`;
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if (!sharedLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(sharedLink);
|
||||
notificationController.show({
|
||||
message: 'Copied to clipboard!',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
notificationController.show({ message: 'Copied to clipboard!', type: NotificationType.Info });
|
||||
} catch (e) {
|
||||
handleError(
|
||||
e,
|
||||
@@ -129,34 +112,36 @@
|
||||
};
|
||||
|
||||
const handleEditLink = async () => {
|
||||
if (editingLink) {
|
||||
try {
|
||||
const expirationTime = getExpirationTimeInMillisecond();
|
||||
const currentTime = new Date().getTime();
|
||||
const expirationDate: string | null = expirationTime
|
||||
? new Date(currentTime + expirationTime).toISOString()
|
||||
: null;
|
||||
if (!editingLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
await api.shareApi.updateSharedLink({
|
||||
id: editingLink.id,
|
||||
editSharedLinkDto: {
|
||||
description,
|
||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||
allowUpload: isAllowUpload,
|
||||
allowDownload: isAllowDownload,
|
||||
showExif: shouldShowExif
|
||||
}
|
||||
});
|
||||
try {
|
||||
const expirationTime = getExpirationTimeInMillisecond();
|
||||
const currentTime = new Date().getTime();
|
||||
const expirationDate: string | null = expirationTime
|
||||
? new Date(currentTime + expirationTime).toISOString()
|
||||
: null;
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Edited'
|
||||
});
|
||||
await api.sharedLinkApi.updateSharedLink({
|
||||
id: editingLink.id,
|
||||
sharedLinkEditDto: {
|
||||
description,
|
||||
expiresAt: shouldChangeExpirationTime ? expirationDate : undefined,
|
||||
allowUpload: allowUpload,
|
||||
allowDownload: allowDownload,
|
||||
showExif: showExif
|
||||
}
|
||||
});
|
||||
|
||||
dispatch('close');
|
||||
} catch (e) {
|
||||
handleError(e, 'Failed to edit shared link');
|
||||
}
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Edited'
|
||||
});
|
||||
|
||||
dispatch('close');
|
||||
} catch (e) {
|
||||
handleError(e, 'Failed to edit shared link');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -212,15 +197,15 @@
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={shouldShowExif} title={'Show metadata'} />
|
||||
<SettingSwitch bind:checked={showExif} title={'Show metadata'} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={isAllowDownload} title={'Allow public user to download'} />
|
||||
<SettingSwitch bind:checked={allowDownload} title={'Allow public user to download'} />
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} />
|
||||
<SettingSwitch bind:checked={allowUpload} title={'Allow public user to upload'} />
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
@@ -248,7 +233,7 @@
|
||||
<hr />
|
||||
|
||||
<section class="m-6">
|
||||
{#if !isShowSharedLink}
|
||||
{#if !sharedLink}
|
||||
{#if editingLink}
|
||||
<div class="flex justify-end">
|
||||
<Button size="sm" rounded="lg" on:click={handleEditLink}>Confirm</Button>
|
||||
@@ -258,9 +243,7 @@
|
||||
<Button size="sm" rounded="lg" on:click={handleCreateSharedLink}>Create link</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isShowSharedLink}
|
||||
{:else}
|
||||
<div class="flex w-full gap-4">
|
||||
<input class="immich-form-input w-full" bind:value={sharedLink} disabled />
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export const load = (async ({ params, locals: { api } }) => {
|
||||
const { key } = params;
|
||||
|
||||
try {
|
||||
const { data: sharedLink } = await api.shareApi.getMySharedLink({ key });
|
||||
const { data: sharedLink } = await api.sharedLinkApi.getMySharedLink({ key });
|
||||
|
||||
const assetCount = sharedLink.assets.length;
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
|
||||
import { api, SharedLinkResponseDto } from '@api';
|
||||
import { goto } from '$app/navigation';
|
||||
import SharedLinkCard from '$lib/components/sharedlinks-page/shared-link-card.svelte';
|
||||
@@ -11,53 +10,45 @@
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { onMount } from 'svelte';
|
||||
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
let showEditForm = false;
|
||||
let editSharedLink: SharedLinkResponseDto;
|
||||
let editSharedLink: SharedLinkResponseDto | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
sharedLinks = await getSharedLinks();
|
||||
});
|
||||
let deleteLinkId: string | null = null;
|
||||
|
||||
const getSharedLinks = async () => {
|
||||
const { data: sharedLinks } = await api.shareApi.getAllSharedLinks();
|
||||
|
||||
return sharedLinks;
|
||||
const refresh = async () => {
|
||||
const { data } = await api.sharedLinkApi.getAllSharedLinks();
|
||||
sharedLinks = data;
|
||||
};
|
||||
|
||||
const handleDeleteLink = async (linkId: string) => {
|
||||
if (window.confirm('Do you want to delete the shared link? ')) {
|
||||
try {
|
||||
await api.shareApi.removeSharedLink({ id: linkId });
|
||||
notificationController.show({
|
||||
message: 'Shared link deleted',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
onMount(async () => {
|
||||
await refresh();
|
||||
});
|
||||
|
||||
sharedLinks = await getSharedLinks();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
notificationController.show({
|
||||
message: 'Failed to delete shared link',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
const handleDeleteLink = async () => {
|
||||
if (!deleteLinkId) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.sharedLinkApi.removeSharedLink({ id: deleteLinkId });
|
||||
notificationController.show({ message: 'Deleted shared link', type: NotificationType.Info });
|
||||
deleteLinkId = null;
|
||||
refresh();
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to delete shared link');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditLink = async (id: string) => {
|
||||
const { data } = await api.shareApi.getSharedLinkById({ id });
|
||||
editSharedLink = data;
|
||||
showEditForm = true;
|
||||
};
|
||||
|
||||
const handleEditDone = async () => {
|
||||
sharedLinks = await getSharedLinks();
|
||||
showEditForm = false;
|
||||
refresh();
|
||||
editSharedLink = null;
|
||||
};
|
||||
|
||||
const handleCopy = async (key: string) => {
|
||||
const handleCopyLink = async (key: string) => {
|
||||
const link = `${window.location.origin}/share/${key}`;
|
||||
await navigator.clipboard.writeText(link);
|
||||
notificationController.show({
|
||||
@@ -67,7 +58,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto('/sharing')}>
|
||||
<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto(AppRoute.SHARING)}>
|
||||
<svelte:fragment slot="leading">Shared links</svelte:fragment>
|
||||
</ControlAppBar>
|
||||
|
||||
@@ -86,16 +77,16 @@
|
||||
{#each sharedLinks as link (link.id)}
|
||||
<SharedLinkCard
|
||||
{link}
|
||||
on:delete={() => handleDeleteLink(link.id)}
|
||||
on:edit={() => handleEditLink(link.id)}
|
||||
on:copy={() => handleCopy(link.key)}
|
||||
on:delete={() => (deleteLinkId = link.id)}
|
||||
on:edit={() => (editSharedLink = link)}
|
||||
on:copy={() => handleCopyLink(link.key)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#if showEditForm}
|
||||
{#if editSharedLink}
|
||||
<CreateSharedLinkModal
|
||||
editingLink={editSharedLink}
|
||||
shareType={editSharedLink.type}
|
||||
@@ -103,3 +94,13 @@
|
||||
on:close={handleEditDone}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if deleteLinkId}
|
||||
<ConfirmDialogue
|
||||
title="Delete Shared Link"
|
||||
prompt="Are you want to delete this shared link?"
|
||||
confirmText="Delete"
|
||||
on:confirm={() => handleDeleteLink()}
|
||||
on:cancel={() => (deleteLinkId = null)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user