feat(web/server) public album sharing (#1266)

This commit is contained in:
Alex
2023-01-09 14:16:08 -06:00
committed by GitHub
parent fd15cdbf40
commit 10789503c1
101 changed files with 4879 additions and 347 deletions

View File

@@ -9,6 +9,7 @@ import {
JobApi,
OAuthApi,
ServerInfoApi,
ShareApi,
SystemConfigApi,
UserApi
} from './open-api';
@@ -24,6 +25,7 @@ class ImmichApi {
public jobApi: JobApi;
public keyApi: APIKeyApi;
public systemConfigApi: SystemConfigApi;
public shareApi: ShareApi;
private config = new Configuration({ basePath: '/api' });
@@ -38,6 +40,7 @@ class ImmichApi {
this.jobApi = new JobApi(this.config);
this.keyApi = new APIKeyApi(this.config);
this.systemConfigApi = new SystemConfigApi(this.config);
this.shareApi = new ShareApi(this.config);
}
public setAccessToken(accessToken: string) {

View File

@@ -671,6 +671,37 @@ export interface CreateAlbumDto {
*/
'assetIds'?: Array<string>;
}
/**
*
* @export
* @interface CreateAlbumShareLinkDto
*/
export interface CreateAlbumShareLinkDto {
/**
*
* @type {string}
* @memberof CreateAlbumShareLinkDto
*/
'albumId': string;
/**
*
* @type {string}
* @memberof CreateAlbumShareLinkDto
*/
'expiredAt'?: string;
/**
*
* @type {boolean}
* @memberof CreateAlbumShareLinkDto
*/
'allowUpload'?: boolean;
/**
*
* @type {string}
* @memberof CreateAlbumShareLinkDto
*/
'description'?: string;
}
/**
*
* @export
@@ -918,6 +949,50 @@ export const DeviceTypeEnum = {
export type DeviceTypeEnum = typeof DeviceTypeEnum[keyof typeof DeviceTypeEnum];
/**
*
* @export
* @interface DownloadFilesDto
*/
export interface DownloadFilesDto {
/**
*
* @type {Array<string>}
* @memberof DownloadFilesDto
*/
'assetIds': Array<string>;
}
/**
*
* @export
* @interface EditSharedLinkDto
*/
export interface EditSharedLinkDto {
/**
*
* @type {string}
* @memberof EditSharedLinkDto
*/
'description'?: string;
/**
*
* @type {string}
* @memberof EditSharedLinkDto
*/
'expiredAt'?: string;
/**
*
* @type {boolean}
* @memberof EditSharedLinkDto
*/
'allowUpload'?: boolean;
/**
*
* @type {boolean}
* @memberof EditSharedLinkDto
*/
'isEditExpireTime'?: boolean;
}
/**
*
* @export
@@ -1477,6 +1552,87 @@ export interface ServerVersionReponseDto {
*/
'build': number;
}
/**
*
* @export
* @interface SharedLinkResponseDto
*/
export interface SharedLinkResponseDto {
/**
*
* @type {SharedLinkType}
* @memberof SharedLinkResponseDto
*/
'type': SharedLinkType;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'id': string;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'description'?: string;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'userId': string;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'key': string;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'createdAt': string;
/**
*
* @type {string}
* @memberof SharedLinkResponseDto
*/
'expiresAt': string | null;
/**
*
* @type {Array<string>}
* @memberof SharedLinkResponseDto
*/
'assets': Array<string>;
/**
*
* @type {AlbumResponseDto}
* @memberof SharedLinkResponseDto
*/
'album'?: AlbumResponseDto;
/**
*
* @type {boolean}
* @memberof SharedLinkResponseDto
*/
'allowUpload': boolean;
}
/**
*
* @export
* @enum {string}
*/
export const SharedLinkType = {
Album: 'ALBUM',
Individual: 'INDIVIDUAL'
} as const;
export type SharedLinkType = typeof SharedLinkType[keyof typeof SharedLinkType];
/**
*
* @export
@@ -2554,6 +2710,45 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createAlbumSharedLink: async (createAlbumShareLinkDto: CreateAlbumShareLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createAlbumShareLinkDto' is not null or undefined
assertParamExists('createAlbumSharedLink', 'createAlbumShareLinkDto', createAlbumShareLinkDto)
const localVarPath = `/album/create-shared-link`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(createAlbumShareLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} albumId
@@ -2915,6 +3110,16 @@ export const AlbumApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbum(createAlbumDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbumSharedLink(createAlbumShareLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} albumId
@@ -3038,6 +3243,15 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
createAlbum(createAlbumDto: CreateAlbumDto, options?: any): AxiosPromise<AlbumResponseDto> {
return localVarFp.createAlbum(createAlbumDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.createAlbumSharedLink(createAlbumShareLinkDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} albumId
@@ -3159,6 +3373,17 @@ export class AlbumApi extends BaseAPI {
return AlbumApiFp(this.configuration).createAlbum(createAlbumDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AlbumApi
*/
public createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: AxiosRequestConfig) {
return AlbumApiFp(this.configuration).createAlbumSharedLink(createAlbumShareLinkDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} albumId
@@ -3423,6 +3648,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions,
};
},
/**
*
* @param {DownloadFilesDto} downloadFilesDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
downloadFiles: async (downloadFilesDto: DownloadFilesDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'downloadFilesDto' is not null or undefined
assertParamExists('downloadFiles', 'downloadFilesDto', downloadFilesDto)
const localVarPath = `/asset/download-files`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'POST', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(downloadFilesDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {number} [skip]
@@ -4050,6 +4314,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {DownloadFilesDto} downloadFilesDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async downloadFiles(downloadFilesDto: DownloadFilesDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFiles(downloadFilesDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {number} [skip]
@@ -4248,6 +4522,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath));
},
/**
*
* @param {DownloadFilesDto} downloadFilesDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
downloadFiles(downloadFilesDto: DownloadFilesDto, options?: any): AxiosPromise<object> {
return localVarFp.downloadFiles(downloadFilesDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {number} [skip]
@@ -4439,6 +4722,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {DownloadFilesDto} downloadFilesDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public downloadFiles(downloadFilesDto: DownloadFilesDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).downloadFiles(downloadFilesDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {number} [skip]
@@ -6052,6 +6346,354 @@ export class ServerInfoApi extends BaseAPI {
}
/**
* ShareApi - axios parameter creator
* @export
*/
export const ShareApiAxiosParamCreator = function (configuration?: Configuration) {
return {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('editSharedLink', 'id', id)
// verify required parameter 'editSharedLinkDto' is not null or undefined
assertParamExists('editSharedLink', 'editSharedLinkDto', editSharedLinkDto)
const localVarPath = `/share/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
localVarHeaderParameter['Content-Type'] = 'application/json';
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAllSharedLinks: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/share`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMySharedLink: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/share/me`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSharedLinkById: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('getSharedLinkById', 'id', id)
const localVarPath = `/share/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeSharedLink: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'id' is not null or undefined
assertParamExists('removeSharedLink', 'id', id)
const localVarPath = `/share/{id}`
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'DELETE', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
}
};
/**
* ShareApi - functional programming interface
* @export
*/
export const ShareApiFp = function(configuration?: Configuration) {
const localVarAxiosParamCreator = ShareApiAxiosParamCreator(configuration)
return {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.editSharedLink(id, editSharedLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getAllSharedLinks(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SharedLinkResponseDto>>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllSharedLinks(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getMySharedLink(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getSharedLinkById(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getSharedLinkById(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async removeSharedLink(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLink(id, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
};
/**
* ShareApi - factory interface
* @export
*/
export const ShareApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
const localVarFp = ShareApiFp(configuration)
return {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.editSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getAllSharedLinks(options?: any): AxiosPromise<Array<SharedLinkResponseDto>> {
return localVarFp.getAllSharedLinks(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getMySharedLink(options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.getMySharedLink(options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getSharedLinkById(id: string, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.getSharedLinkById(id, options).then((request) => request(axios, basePath));
},
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
removeSharedLink(id: string, options?: any): AxiosPromise<string> {
return localVarFp.removeSharedLink(id, options).then((request) => request(axios, basePath));
},
};
};
/**
* ShareApi - object-oriented interface
* @export
* @class ShareApi
* @extends {BaseAPI}
*/
export class ShareApi extends BaseAPI {
/**
*
* @param {string} id
* @param {EditSharedLinkDto} editSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).editSharedLink(id, editSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public getAllSharedLinks(options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).getAllSharedLinks(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public getMySharedLink(options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).getMySharedLink(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public getSharedLinkById(id: string, options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).getSharedLinkById(id, options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {string} id
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof ShareApi
*/
public removeSharedLink(id: string, options?: AxiosRequestConfig) {
return ShareApiFp(this.configuration).removeSharedLink(id, options).then((request) => request(this.axios, this.basePath));
}
}
/**
* SystemConfigApi - axios parameter creator
* @export

View File

@@ -4,13 +4,14 @@ import { UserResponseDto } from './open-api';
const _basePath = '/api';
export function getFileUrl(assetId: string, isThumb?: boolean, isWeb?: boolean) {
export function getFileUrl(assetId: string, isThumb?: boolean, isWeb?: boolean, key?: string) {
const urlObj = new URL(`${window.location.origin}${_basePath}/asset/file/${assetId}`);
if (isThumb !== undefined && isThumb !== null)
urlObj.searchParams.append('isThumb', `${isThumb}`);
if (isWeb !== undefined && isWeb !== null) urlObj.searchParams.append('isWeb', `${isWeb}`);
if (key !== undefined && key !== null) urlObj.searchParams.append('key', key);
return urlObj.href;
}

View File

@@ -1,15 +1,13 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body class="bg-immich-bg dark:bg-immich-dark-bg">
<div>%sveltekit.body%</div>
</body>
</html>
<body class="bg-immich-bg dark:bg-immich-dark-bg">
<div>%sveltekit.body%</div>
</body>
</html>

View File

@@ -93,7 +93,7 @@
>.
</p>
<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
<hr />
<SettingInputField
inputType={SettingInputFieldType.TEXT}

View File

@@ -25,7 +25,7 @@
<div class="w-full">
<div class={`flex place-items-center gap-1 h-[26px]`}>
<label class={`immich-form-label text-xs`} for={label}>{label}</label>
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
{#if required}
<div class="text-red-400">*</div>
{/if}

View File

@@ -8,13 +8,13 @@
<div class="flex justify-between place-items-center">
<div>
<h2 class="immich-form-label text-sm">
{title.toUpperCase()}
{title}
</h2>
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
</div>
<label class="relative inline-block w-[36px] h-[10px]" {disabled}>
<label class="relative inline-block w-[36px] h-[10px]">
<input
class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
type="checkbox"

View File

@@ -93,6 +93,7 @@ describe('AlbumCard component', () => {
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
'thumbnailIdOne',
ThumbnailFormat.Jpeg,
'',
{ responseType: 'blob' }
);
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailBlob);

View File

@@ -1,7 +1,15 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import { AlbumResponseDto, api, AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
import {
AlbumResponseDto,
api,
AssetResponseDto,
SharedLinkResponseDto,
SharedLinkType,
ThumbnailFormat,
UserResponseDto
} from '@api';
import { onMount } from 'svelte';
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
@@ -23,20 +31,30 @@
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
import ThumbnailSelection from './thumbnail-selection.svelte';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
import { browser } from '$app/environment';
import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
import CreateSharedLinkModal from '../shared-components/create-share-link-modal/create-shared-link-modal.svelte';
import ThemeButton from '../shared-components/theme-button.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { bulkDownload } from '$lib/utils/asset-utils';
export let album: AlbumResponseDto;
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
let isShowAssetViewer = false;
let isShowAssetSelection = false;
let isShowShareLinkModal = false;
$: $isAlbumAssetSelectionOpen = isShowAssetSelection;
$: {
if (browser) {
@@ -65,6 +83,7 @@
let titleInput: HTMLInputElement;
let contextMenuPosition = { x: 0, y: 0 };
$: isPublicShared = sharedLink;
$: isOwned = currentUser?.id == album.ownerId;
let multiSelectAsset: Set<AssetResponseDto> = new Set();
@@ -82,7 +101,11 @@
if (album.assets?.length < 6) {
thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
} else {
thumbnailSize = Math.floor(viewWidth / 6 - 6);
if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6);
else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6);
else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6);
}
}
@@ -219,9 +242,17 @@
const createAlbumHandler = async (event: CustomEvent) => {
const { assets }: { assets: AssetResponseDto[] } = event.detail;
try {
const { data } = await api.albumApi.addAssetsToAlbum(album.id, {
assetIds: assets.map((a) => a.id)
});
const { data } = await api.albumApi.addAssetsToAlbum(
album.id,
{
assetIds: assets.map((a) => a.id)
},
{
params: {
key: sharedLink?.key
}
}
);
if (data.album) {
album = data.album;
@@ -316,6 +347,9 @@
album.id,
skip || undefined,
{
params: {
key: sharedLink?.key
},
responseType: 'blob',
onDownloadProgress: function (progressEvent) {
const request = this as XMLHttpRequest;
@@ -397,6 +431,23 @@
isShowThumbnailSelection = false;
};
const onSharedLinkClickHandler = () => {
isShowShareUserSelection = false;
isShowShareLinkModal = true;
};
const handleDownloadSelectedAssets = async () => {
await bulkDownload(
album.albumName,
Array.from(multiSelectAsset),
() => {
isMultiSelectionMode = false;
clearMultiSelectAssetAssetHandler();
},
sharedLink?.key
);
};
</script>
<section class="bg-immich-bg dark:bg-immich-dark-bg">
@@ -413,6 +464,11 @@
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Download"
on:click={handleDownloadSelectedAssets}
logo={CloudDownloadOutline}
/>
{#if isOwned}
<CircleIconButton
title="Remove from album"
@@ -426,14 +482,45 @@
<!-- Default app bar -->
{#if !isMultiSelectionMode}
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
<ControlAppBar
on:close-button-click={() => goto(backUrl)}
backIcon={ArrowLeft}
showBackButton={(!isPublicShared && isOwned) ||
(!isPublicShared && !isOwned) ||
(isPublicShared && isOwned)}
>
<svelte:fragment slot="leading">
{#if isPublicShared && !isOwned}
<a
data-sveltekit-preload-data="hover"
class="flex gap-2 place-items-center hover:cursor-pointer ml-6"
href="https://immich.app"
>
<img src="/immich-logo.svg" alt="immich logo" height="30" width="30" />
<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
IMMICH
</h1>
</a>
{/if}
</svelte:fragment>
<svelte:fragment slot="trailing">
{#if album.assetCount > 0}
<CircleIconButton
title="Add Photos"
on:click={() => (isShowAssetSelection = true)}
logo={FileImagePlusOutline}
/>
{#if !sharedLink}
<CircleIconButton
title="Add Photos"
on:click={() => (isShowAssetSelection = true)}
logo={FileImagePlusOutline}
/>
{/if}
{#if sharedLink?.allowUpload}
<CircleIconButton
title="Add Photos"
on:click={() => openFileUploadDialog(album.id, sharedLink?.key)}
logo={FileImagePlusOutline}
/>
{/if}
<!-- Share and remove album -->
{#if isOwned}
@@ -451,11 +538,17 @@
logo={FolderDownloadOutline}
/>
<CircleIconButton
title="Album options"
on:click={(event) => showAlbumOptionsMenu(event)}
logo={DotsVertical}
/>
{#if !isPublicShared}
<CircleIconButton
title="Album options"
on:click={(event) => showAlbumOptionsMenu(event)}
logo={DotsVertical}
/>
{/if}
{#if isPublicShared}
<ThemeButton />
{/if}
{/if}
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
@@ -470,7 +563,7 @@
</ControlAppBar>
{/if}
<section class="m-auto my-[160px] w-[60%]">
<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40">
<input
on:keydown={(e) => {
if (e.key == 'Enter') {
@@ -492,7 +585,6 @@
{#if album.assetCount > 0}
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
{/if}
{#if album.shared}
<div class="my-6 flex">
{#each album.sharedUsers as user}
@@ -521,6 +613,7 @@
<ImmichThumbnail
{asset}
{thumbnailSize}
publicSharedKey={sharedLink?.key}
format={ThumbnailFormat.Jpeg}
on:click={(e) =>
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
@@ -531,6 +624,7 @@
<ImmichThumbnail
{asset}
{thumbnailSize}
publicSharedKey={sharedLink?.key}
on:click={(e) =>
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
on:select={selectAssetHandler}
@@ -564,6 +658,7 @@
{#if isShowAssetViewer}
<AssetViewer
asset={selectedAsset}
publicSharedKey={sharedLink?.key}
on:navigate-previous={navigateAssetBackward}
on:navigate-next={navigateAssetForward}
on:close={closeViewer}
@@ -581,12 +676,21 @@
{#if isShowShareUserSelection}
<UserSelectionModal
{album}
on:close={() => (isShowShareUserSelection = false)}
on:add-user={addUserHandler}
on:sharedlinkclick={onSharedLinkClickHandler}
sharedUsersInAlbum={new Set(album.sharedUsers)}
/>
{/if}
{#if isShowShareLinkModal}
<CreateSharedLinkModal
on:close={() => (isShowShareLinkModal = false)}
shareType={SharedLinkType.Album}
{album}
/>
{/if}
{#if isShowShareInfoModal}
<ShareInfoModal
on:close={() => (isShowShareInfoModal = false)}

View File

@@ -51,7 +51,7 @@
<svelte:fragment slot="trailing">
<button
on:click={() =>
openFileUploadDialog(albumId, () => {
openFileUploadDialog(albumId, '', () => {
assetInteractionStore.clearMultiselect();
dispatch('go-back');
})}

View File

@@ -1,16 +1,21 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import { api, UserResponseDto } from '@api';
import { AlbumResponseDto, api, SharedLinkResponseDto, UserResponseDto } from '@api';
import BaseModal from '../shared-components/base-modal.svelte';
import CircleAvatar from '../shared-components/circle-avatar.svelte';
import Link from 'svelte-material-icons/Link.svelte';
import ShareCircle from 'svelte-material-icons/ShareCircle.svelte';
import { goto } from '$app/navigation';
export let album: AlbumResponseDto;
export let sharedUsersInAlbum: Set<UserResponseDto>;
let users: UserResponseDto[] = [];
let selectedUsers: UserResponseDto[] = [];
const dispatch = createEventDispatcher();
let sharedLinks: SharedLinkResponseDto[] = [];
onMount(async () => {
await getSharedLinks();
const { data } = await api.userApi.getAllUsers(false);
// remove soft deleted users
@@ -22,6 +27,12 @@
});
});
const getSharedLinks = async () => {
const { data } = await api.shareApi.getAllSharedLinks();
sharedLinks = data.filter((link) => link.album?.id === album.id);
};
const selectUser = (user: UserResponseDto) => {
if (selectedUsers.includes(user)) {
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
@@ -33,6 +44,10 @@
const deselectUser = (user: UserResponseDto) => {
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
};
const onSharedLinkClick = () => {
dispatch('sharedlinkclick');
};
</script>
<BaseModal on:close={() => dispatch('close')}>
@@ -93,7 +108,7 @@
{/each}
</div>
{:else}
<p class="text-sm px-5">
<p class="text-sm p-5">
Looks like you have shared this album with all users or you don't have any user to share
with.
</p>
@@ -109,4 +124,25 @@
</div>
{/if}
</div>
<hr />
<div id="shared-buttons" class="flex my-4 justify-around place-items-center place-content-center">
<button
class="flex flex-col gap-2 place-items-center place-content-center hover:cursor-pointer"
on:click={onSharedLinkClick}
>
<Link size={24} />
<p class="text-sm">Create link</p>
</button>
{#if sharedLinks.length}
<button
class="flex flex-col gap-2 place-items-center place-content-center hover:cursor-pointer"
on:click={() => goto('/sharing/sharedlinks')}
>
<ShareCircle size={24} />
<p class="text-sm">View links</p>
</button>
{/if}
</div>
</BaseModal>

View File

@@ -23,7 +23,7 @@
export let showMotionPlayButton: boolean;
export let isMotionPhotoPlaying = false;
const isOwner = asset.ownerId === $page.data.user.id;
const isOwner = asset.ownerId === $page.data.user?.id;
const dispatch = createEventDispatcher();
@@ -94,12 +94,15 @@
title="Favorite"
/>
{/if}
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
<CircleIconButton
logo={DotsVertical}
on:click={(event) => showOptionsMenu(event)}
title="More"
/>
{#if isOwner}
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
<CircleIconButton
logo={DotsVertical}
on:click={(event) => showOptionsMenu(event)}
title="More"
/>
{/if}
</div>
</div>

View File

@@ -10,12 +10,7 @@
import { downloadAssets } from '$lib/stores/download';
import VideoViewer from './video-viewer.svelte';
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
import {
api,
AssetResponseDto,
AssetTypeEnum,
AlbumResponseDto
} from '@api';
import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api';
import {
notificationController,
NotificationType
@@ -25,6 +20,9 @@
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
export let asset: AssetResponseDto;
export let publicSharedKey = '';
export let showNavigation = true;
$: {
appearsInAlbums = [];
@@ -91,12 +89,12 @@
const handleDownload = () => {
if (asset.livePhotoVideoId) {
downloadFile(asset.livePhotoVideoId, true);
downloadFile(asset.id, false);
downloadFile(asset.livePhotoVideoId, true, publicSharedKey);
downloadFile(asset.id, false, publicSharedKey);
return;
}
downloadFile(asset.id, false);
downloadFile(asset.id, false, publicSharedKey);
};
/**
@@ -111,7 +109,7 @@
};
};
const downloadFile = async (assetId: string, isLivePhoto: boolean) => {
const downloadFile = async (assetId: string, isLivePhoto: boolean, key: string) => {
try {
const { filenameWithoutExtension } = getTemplateFilename();
@@ -126,6 +124,9 @@
$downloadAssets[imageFileName] = 0;
const { data, status } = await api.assetApi.downloadFile(assetId, false, false, {
params: {
key
},
responseType: 'blob',
onDownloadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
@@ -251,69 +252,74 @@
/>
</div>
<div
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 mb-[60px] ${
asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
}`}
on:mouseenter={() => {
halfLeftHover = true;
halfRightHover = false;
}}
on:mouseleave={() => {
halfLeftHover = false;
}}
on:click={navigateAssetBackward}
on:keydown={navigateAssetBackward}
>
<button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
class:navigation-button-hover={halfLeftHover}
{#if showNavigation}
<div
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 mb-[60px] ${
asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
}`}
on:mouseenter={() => {
halfLeftHover = true;
halfRightHover = false;
}}
on:mouseleave={() => {
halfLeftHover = false;
}}
on:click={navigateAssetBackward}
on:keydown={navigateAssetBackward}
>
<ChevronLeft size="36" />
</button>
</div>
<button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
class:navigation-button-hover={halfLeftHover}
on:click={navigateAssetBackward}
>
<ChevronLeft size="36" />
</button>
</div>
{/if}
<div class="row-start-1 row-span-full col-start-1 col-span-4">
{#key asset.id}
{#if asset.type === AssetTypeEnum.Image}
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
<VideoViewer
{publicSharedKey}
assetId={asset.livePhotoVideoId}
on:close={closeViewer}
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
/>
{:else}
<PhotoViewer assetId={asset.id} on:close={closeViewer} />
<PhotoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />
{/if}
{:else}
<VideoViewer assetId={asset.id} on:close={closeViewer} />
<VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />
{/if}
{/key}
</div>
<div
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end mb-[60px] ${
asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
}`}
on:click={navigateAssetForward}
on:keydown={navigateAssetForward}
on:mouseenter={() => {
halfLeftHover = false;
halfRightHover = true;
}}
on:mouseleave={() => {
halfRightHover = false;
}}
>
<button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
class:navigation-button-hover={halfRightHover}
{#if showNavigation}
<div
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end mb-[60px] ${
asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
}`}
on:click={navigateAssetForward}
on:keydown={navigateAssetForward}
on:mouseenter={() => {
halfLeftHover = false;
halfRightHover = true;
}}
on:mouseleave={() => {
halfRightHover = false;
}}
>
<ChevronRight size="36" />
</button>
</div>
<button
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
class:navigation-button-hover={halfRightHover}
on:click={navigateAssetForward}
>
<ChevronRight size="36" />
</button>
</div>
{/if}
{#if isShowDetail}
<div

View File

@@ -11,6 +11,7 @@
} from '../shared-components/notification/notification';
export let assetId: string;
export let publicSharedKey = '';
let assetInfo: AssetResponseDto;
let assetData: string;
@@ -18,7 +19,11 @@
let copyImageToClipboard: (src: string) => Promise<Blob>;
onMount(async () => {
const { data } = await api.assetApi.getAssetById(assetId);
const { data } = await api.assetApi.getAssetById(assetId, {
params: {
key: publicSharedKey
}
});
assetInfo = data;
//Import hack :( see https://github.com/vadimkorr/svelte-carousel/issues/27#issuecomment-851022295
@@ -29,6 +34,9 @@
const loadAssetData = async () => {
try {
const { data } = await api.assetApi.serveFile(assetInfo.id, false, true, {
params: {
key: publicSharedKey
},
responseType: 'blob'
});

View File

@@ -6,7 +6,7 @@
import { api, AssetResponseDto, getFileUrl } from '@api';
export let assetId: string;
export let publicSharedKey = '';
let asset: AssetResponseDto;
let videoPlayerNode: HTMLVideoElement;
@@ -15,7 +15,11 @@
const dispatch = createEventDispatcher();
onMount(async () => {
const { data: assetInfo } = await api.assetApi.getAssetById(assetId);
const { data: assetInfo } = await api.assetApi.getAssetById(assetId, {
params: {
key: publicSharedKey
}
});
await loadVideoData(assetInfo);
@@ -25,7 +29,7 @@
const loadVideoData = async (assetInfo: AssetResponseDto) => {
isVideoLoading = true;
videoUrl = getFileUrl(assetInfo.id, false, true);
videoUrl = getFileUrl(assetInfo.id, false, true, publicSharedKey);
return assetInfo;
};

View File

@@ -5,6 +5,8 @@
import Close from 'svelte-material-icons/Close.svelte';
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
import { fly } from 'svelte/transition';
export let showBackButton = true;
export let backIcon = Close;
export let tailwindClasses = '';
@@ -42,14 +44,15 @@
class={`flex justify-between ${appBarBorder} rounded-lg p-2 mx-2 mt-2 transition-all place-items-center ${tailwindClasses} dark:bg-immich-dark-gray`}
>
<div class="flex place-items-center gap-6 dark:text-immich-dark-fg">
<CircleIconButton
on:click={() => dispatch('close-button-click')}
logo={backIcon}
backgroundColor={'transparent'}
hoverColor={'#e2e7e9'}
size={'24'}
/>
{#if showBackButton}
<CircleIconButton
on:click={() => dispatch('close-button-click')}
logo={backIcon}
backgroundColor={'transparent'}
hoverColor={'#e2e7e9'}
size={'24'}
/>
{/if}
<slot name="leading" />
</div>

View File

@@ -0,0 +1,243 @@
<script lang="ts">
import { createEventDispatcher, onMount } from 'svelte';
import BaseModal from '../base-modal.svelte';
import Link from 'svelte-material-icons/Link.svelte';
import { AlbumResponseDto, api, SharedLinkResponseDto, SharedLinkType } from '@api';
import { notificationController, NotificationType } from '../notification/notification';
import { ImmichDropDownOption } from '../dropdown-button.svelte';
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
import DropdownButton from '../dropdown-button.svelte';
import SettingInputField, {
SettingInputFieldType
} from '$lib/components/admin-page/settings/setting-input-field.svelte';
export let shareType: SharedLinkType;
export let album: AlbumResponseDto | undefined;
export let editingLink: SharedLinkResponseDto | undefined = undefined;
let isLoading = false;
let isShowSharedLink = false;
let expirationTime = '';
let isAllowUpload = false;
let sharedLink = '';
let description = '';
let shouldChangeExpirationTime = false;
const dispatch = createEventDispatcher();
const expiredDateOption: ImmichDropDownOption = {
default: 'Never',
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days']
};
onMount(() => {
if (editingLink) {
if (editingLink.description) {
description = editingLink.description;
}
isAllowUpload = editingLink.allowUpload;
}
});
const createAlbumSharedLink = async () => {
if (album) {
isLoading = true;
try {
const expirationTime = getExpirationTimeInMillisecond();
const currentTime = new Date().getTime();
const expirationDate = expirationTime
? new Date(currentTime + expirationTime).toISOString()
: undefined;
const { data } = await api.albumApi.createAlbumSharedLink({
albumId: album.id,
expiredAt: expirationDate,
allowUpload: isAllowUpload,
description: description
});
buildSharedLink(data);
isLoading = false;
isShowSharedLink = true;
} catch (e) {
console.error('[createAlbumSharedLink] Error: ', e);
notificationController.show({
type: NotificationType.Error,
message: 'Failed to create shared link'
});
isLoading = false;
}
}
};
const buildSharedLink = (createdLink: SharedLinkResponseDto) => {
sharedLink = `${window.location.origin}/share/${createdLink.key}`;
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(sharedLink);
notificationController.show({
message: 'Copied to clipboard!',
type: NotificationType.Info
});
} catch (error) {
console.error('Error', error);
}
};
const getExpirationTimeInMillisecond = () => {
switch (expirationTime) {
case '30 minutes':
return 30 * 60 * 1000;
case '1 hour':
return 60 * 60 * 1000;
case '6 hours':
return 6 * 60 * 60 * 1000;
case '1 day':
return 24 * 60 * 60 * 1000;
case '7 days':
return 7 * 24 * 60 * 60 * 1000;
case '30 days':
return 30 * 24 * 60 * 60 * 1000;
default:
return 0;
}
};
const handleEditLink = async () => {
if (editingLink) {
try {
const expirationTime = getExpirationTimeInMillisecond();
const currentTime = new Date().getTime();
let expirationDate = expirationTime
? new Date(currentTime + expirationTime).toISOString()
: undefined;
if (expirationTime === 0) {
expirationDate = undefined;
}
await api.shareApi.editSharedLink(editingLink.id, {
description: description,
expiredAt: expirationDate,
allowUpload: isAllowUpload,
isEditExpireTime: shouldChangeExpirationTime
});
notificationController.show({
type: NotificationType.Info,
message: 'Edited'
});
dispatch('close');
} catch (e) {
console.error('[handleEditLink]', e);
notificationController.show({
type: NotificationType.Error,
message: 'Failed to edit shared link'
});
}
}
};
</script>
<BaseModal on:close={() => dispatch('close')}>
<svelte:fragment slot="title">
<span class="flex gap-2 place-items-center">
<Link size={24} />
{#if editingLink}
<p class="font-medium text-immich-fg dark:text-immich-dark-fg">Edit link</p>
{:else}
<p class="font-medium text-immich-fg dark:text-immich-dark-fg">Create link to share</p>
{/if}
</span>
</svelte:fragment>
<section class="mx-6 mb-6">
{#if shareType == SharedLinkType.Album}
{#if !editingLink}
<div>Let anyone with the link see photos and people in this album.</div>
{:else}
<div class="text-sm">
Public album | <span class="text-immich-primary dark:text-immich-dark-primary"
>{editingLink.album?.albumName}</span
>
</div>
{/if}
{/if}
<div class="mt-6 mb-2">
<p class="text-xs">LINK OPTIONS</p>
</div>
<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg">
<div class="flex flex-col">
<div class="mb-4">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="Description"
bind:value={description}
/>
</div>
<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} />
<div class="text-sm mt-4">
{#if editingLink}
<p class="my-2 immich-form-label">
<SettingSwitch
bind:checked={shouldChangeExpirationTime}
title={'Change expiration time'}
/>
</p>
{:else}
<p class="my-2 immich-form-label">Expire after</p>
{/if}
<DropdownButton
options={expiredDateOption}
bind:selected={expirationTime}
disabled={editingLink && !shouldChangeExpirationTime}
/>
</div>
</div>
</div>
</section>
<hr />
<section class="m-6">
{#if !isShowSharedLink}
{#if editingLink}
<div class="flex justify-end">
<button
on:click={handleEditLink}
class="text-white dark:text-black bg-immich-primary px-4 py-2 rounded-lg text-sm transition-colors hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:hover:bg-immich-dark-primary/75"
>
Confirm
</button>
</div>
{:else}
<div class="flex justify-end">
<button
on:click={createAlbumSharedLink}
class="text-white dark:text-black bg-immich-primary px-4 py-2 rounded-lg text-sm transition-colors hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:hover:bg-immich-dark-primary/75"
>
Create Link
</button>
</div>
{/if}
{/if}
{#if isShowSharedLink}
<div class="flex w-full gap-4">
<input class="immich-form-input w-full" bind:value={sharedLink} />
<button
on:click={() => handleCopy()}
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-2 text-white rounded-full shadow-md w-full font-medium"
>Copy</button
>
</div>
{/if}
</section>
</BaseModal>

View File

@@ -0,0 +1,76 @@
<script lang="ts" context="module">
export type ImmichDropDownOption = {
default: string;
options: string[];
};
</script>
<script lang="ts">
import { onMount } from 'svelte';
export let options: ImmichDropDownOption;
export let selected: string;
export let disabled = false;
onMount(() => {
selected = options.default;
});
export let isOpen = false;
const toggle = () => (isOpen = !isOpen);
</script>
<div id="immich-dropdown" class="relative">
<button
{disabled}
on:click={toggle}
aria-expanded={isOpen}
class="bg-gray-200 w-full flex p-2 rounded-lg dark:bg-gray-600 place-items-center justify-between disabled:cursor-not-allowed dark:disabled:bg-gray-300 disabled:bg-gray-600 "
>
<div>
{selected}
</div>
<div>
<svg
style="tran"
width="20"
height="20"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{#if isOpen}
<div class="flex flex-col mt-2 absolute w-full">
{#each options.options as option}
<button
on:click={() => {
selected = option;
isOpen = false;
}}
class="bg-gray-200 dark:bg-gray-500 dark:hover:bg-gray-700 w-full flex p-2 hover:bg-gray-300 transition-all "
>
{option}
</button>
{/each}
</div>
{/if}
</div>
<style>
svg {
transition: transform 0.2s ease-in;
}
[aria-expanded='true'] svg {
transform: rotate(0.5turn);
}
</style>

View File

@@ -18,6 +18,9 @@
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
export let selected = false;
export let disabled = false;
export let publicSharedKey = '';
export let isRoundedCorner = false;
let imageData: string;
let mouseOver = false;
@@ -35,10 +38,9 @@
isThumbnailVideoPlaying = false;
if (isLivePhoto && asset.livePhotoVideoId) {
console.log('get file url');
videoUrl = getFileUrl(asset.livePhotoVideoId, false, true);
videoUrl = getFileUrl(asset.livePhotoVideoId, false, true, publicSharedKey);
} else {
videoUrl = getFileUrl(asset.id, false, true);
videoUrl = getFileUrl(asset.id, false, true, publicSharedKey);
}
};
@@ -118,6 +120,8 @@
return 'border-[20px] border-immich-primary/20';
} else if (disabled) {
return 'border-[20px] border-gray-300';
} else if (isRoundedCorner) {
return 'rounded-[20px]';
} else {
return '';
}
@@ -244,7 +248,7 @@
style:width={`${thumbnailSize}px`}
style:height={`${thumbnailSize}px`}
in:fade={{ duration: 150 }}
src={`/api/asset/thumbnail/${asset.id}?format=${format}`}
src={`/api/asset/thumbnail/${asset.id}?format=${format}&key=${publicSharedKey}`}
alt={asset.id}
class={`object-cover ${getSize()} transition-all z-0 ${getThumbnailBorderStyle()}`}
loading="lazy"

View File

@@ -49,7 +49,7 @@
on:click={toggleTheme}
id="theme-toggle"
type="button"
class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full text-sm p-2.5"
>
<svg
id="theme-toggle-dark-icon"

View File

@@ -0,0 +1,142 @@
<script lang="ts">
import { api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType } from '@api';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import OpenInNew from 'svelte-material-icons/OpenInNew.svelte';
import Delete from 'svelte-material-icons/TrashCanOutline.svelte';
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
import CircleEditOutline from 'svelte-material-icons/CircleEditOutline.svelte';
import * as luxon from 'luxon';
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
import { createEventDispatcher } from 'svelte';
import { goto } from '$app/navigation';
export let link: SharedLinkResponseDto;
let expirationCountdown: luxon.DurationObjectUnits;
const dispatch = createEventDispatcher();
const getAssetInfo = async (): Promise<AssetResponseDto> => {
let assetId = '';
if (link.album?.albumThumbnailAssetId) {
assetId = link.album.albumThumbnailAssetId;
} else if (link.assets.length > 0) {
assetId = link.assets[0];
}
const { data } = await api.assetApi.getAssetById(assetId);
return data;
};
const getCountDownExpirationDate = () => {
if (!link.expiresAt) {
return;
}
const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString());
const now = luxon.DateTime.now();
expirationCountdown = expiresAtDate
.diff(now, ['days', 'hours', 'minutes', 'seconds'])
.toObject();
if (expirationCountdown.days && expirationCountdown.days > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
} else if (expirationCountdown.hours && expirationCountdown.hours > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'hours' });
} else if (expirationCountdown.minutes && expirationCountdown.minutes > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'minutes' });
} else if (expirationCountdown.seconds && expirationCountdown.seconds > 0) {
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'seconds' });
}
};
const isExpired = (expiresAt: string) => {
const now = new Date().getTime();
const expiration = new Date(expiresAt).getTime();
return now > expiration;
};
</script>
<div
class="w-full flex gap-4 dark:text-immich-gray transition-all border-b border-gray-200 dark:border-gray-600 hover:border-immich-primary dark:hover:border-immich-dark-primary py-4"
>
<div>
{#await getAssetInfo()}
<LoadingSpinner />
{:then asset}
<img
id={asset.id}
src={`/api/asset/thumbnail/${asset.id}?format=WEBP`}
alt={asset.id}
class="object-cover w-[100px] h-[100px] rounded-lg"
loading="lazy"
/>
{/await}
</div>
<div class="flex flex-col justify-between">
<div class="info-top">
<div class="text-xs font-mono font-semibold text-gray-500 dark:text-gray-400">
{#if link.expiresAt}
{#if isExpired(link.expiresAt)}
<p class="text-red-600 dark:text-red-400 font-bold">Expired</p>
{:else}
<p>
Expires {getCountDownExpirationDate()}
</p>
{/if}
{:else}
<p>Expires ∞</p>
{/if}
</div>
<div class="text-sm">
<div
class="flex gap-2 place-items-center text-immich-primary dark:text-immich-dark-primary"
>
{#if link.type === SharedLinkType.Album}
<p>
{link.album?.albumName.toUpperCase()}
</p>
{:else if link.type === SharedLinkType.Individual}
<p>INDIVIDUAL SHARE</p>
{/if}
{#if !link.expiresAt || !isExpired(link.expiresAt)}
<div
class="hover:cursor-pointer"
title="Go to share page"
on:click={() => goto(`/share/${link.key}`)}
on:keydown={() => goto(`/share/${link.key}`)}
>
<OpenInNew />
</div>
{/if}
</div>
<p class="text-sm">{link.description ?? ''}</p>
</div>
</div>
<div class="info-bottom">
{#if link.allowUpload}
<div
class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[100px]"
>
Allow upload
</div>
{/if}
</div>
</div>
<div class="flex-auto flex flex-col place-content-center place-items-end text-right">
<div class="flex">
<CircleIconButton logo={Delete} on:click={() => dispatch('delete')} />
<CircleIconButton logo={CircleEditOutline} on:click={() => dispatch('edit')} />
<CircleIconButton logo={ContentCopy} on:click={() => dispatch('copy')} />
</div>
</div>
</div>

View File

@@ -1,21 +1,106 @@
import { api, AddAssetsResponseDto } from '@api';
import { api, AddAssetsResponseDto, AssetResponseDto } from '@api';
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { downloadAssets } from '$lib/stores/download';
import { get } from 'svelte/store';
export const addAssetsToAlbum = async (
albumId: string,
assetIds: Array<string>
assetIds: Array<string>,
key: string | undefined = undefined
): Promise<AddAssetsResponseDto> =>
api.albumApi.addAssetsToAlbum(albumId, { assetIds }).then(({ data: dto }) => {
if (dto.successfullyAdded > 0) {
// This might be 0 if the user tries to add an asset that is already in the album
notificationController.show({
message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
type: NotificationType.Info
});
}
api.albumApi
.addAssetsToAlbum(albumId, { assetIds }, { params: { key } })
.then(({ data: dto }) => {
if (dto.successfullyAdded > 0) {
// This might be 0 if the user tries to add an asset that is already in the album
notificationController.show({
message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
type: NotificationType.Info
});
}
return dto;
});
return dto;
});
export async function bulkDownload(
fileName: string,
assets: AssetResponseDto[],
onDone: () => void,
key?: string
) {
const assetIds = assets.map((asset) => asset.id);
try {
let skip = 0;
let count = 0;
let done = false;
while (!done) {
count++;
const downloadFileName = fileName + `${count === 1 ? '' : count}.zip`;
downloadAssets.set({ [downloadFileName]: 0 });
let total = 0;
const { data, status, headers } = await api.assetApi.downloadFiles(
{ assetIds },
{
params: { key },
responseType: 'blob',
onDownloadProgress: function (progressEvent) {
const request = this as XMLHttpRequest;
if (!total) {
total = Number(request.getResponseHeader('X-Immich-Content-Length-Hint')) || 0;
}
if (total) {
const current = progressEvent.loaded;
downloadAssets.set({ [downloadFileName]: Math.floor((current / total) * 100) });
}
}
}
);
const isNotComplete = headers['x-immich-archive-complete'] === 'false';
const fileCount = Number(headers['x-immich-archive-file-count']) || 0;
if (isNotComplete && fileCount > 0) {
skip += fileCount;
} else {
onDone();
done = true;
}
if (!(data instanceof Blob)) {
return;
}
if (status === 201) {
const fileUrl = URL.createObjectURL(data);
const anchor = document.createElement('a');
anchor.href = fileUrl;
anchor.download = downloadFileName;
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(fileUrl);
// Remove item from download list
setTimeout(() => {
downloadAssets.set({});
}, 2000);
}
}
} catch (e) {
console.error('Error downloading file ', e);
notificationController.show({
type: NotificationType.Error,
message: 'Error downloading file, check console for more details.'
});
}
}

View File

@@ -11,6 +11,7 @@ import { addAssetsToAlbum } from '$lib/utils/asset-utils';
export const openFileUploadDialog = (
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
callback?: () => void
) => {
try {
@@ -27,7 +28,7 @@ export const openFileUploadDialog = (
}
const files = Array.from<File>(target.files);
await fileUploadHandler(files, albumId);
await fileUploadHandler(files, albumId, sharedKey);
callback && callback();
};
@@ -37,7 +38,11 @@ export const openFileUploadDialog = (
}
};
export const fileUploadHandler = async (files: File[], albumId: string | undefined = undefined) => {
export const fileUploadHandler = async (
files: File[],
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined
) => {
if (files.length > 50) {
notificationController.show({
type: NotificationType.Error,
@@ -49,18 +54,22 @@ export const fileUploadHandler = async (files: File[], albumId: string | undefin
return;
}
console.log('fileUploadHandler');
const acceptedFile = files.filter(
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
);
for (const asset of acceptedFile) {
await fileUploader(asset, albumId);
await fileUploader(asset, albumId, sharedKey);
}
};
//TODO: should probably use the @api SDK
async function fileUploader(asset: File, albumId: string | undefined = undefined) {
async function fileUploader(
asset: File,
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined
) {
const assetType = asset.type.split('/')[0].toUpperCase();
const temp = asset.name.split('.');
const fileExtension = temp[temp.length - 1];
@@ -108,10 +117,17 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
formData.append('assetData', asset);
// Check if asset upload on server before performing upload
const { data, status } = await api.assetApi.checkDuplicateAsset({
deviceAssetId: String(deviceAssetId),
deviceId: 'WEB'
});
const { data, status } = await api.assetApi.checkDuplicateAsset(
{
deviceAssetId: String(deviceAssetId),
deviceId: 'WEB'
},
{
params: {
key: sharedKey
}
}
);
if (status === 200) {
if (data.isExist) {
@@ -124,7 +140,6 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
}
const request = new XMLHttpRequest();
request.upload.onloadstart = () => {
const newUploadAsset: UploadAsset = {
id: deviceAssetId,
@@ -144,7 +159,7 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
try {
const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
if (res.id) {
addAssetsToAlbum(albumId, [res.id]);
addAssetsToAlbum(albumId, [res.id], sharedKey);
}
} catch (e) {
console.error('ERROR parsing data JSON in upload onload');
@@ -171,7 +186,7 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
};
request.open('POST', `/api/asset/upload`);
request.open('POST', `/api/asset/upload?key=${sharedKey ?? ''}`);
request.send(formData);
} catch (e) {

View File

@@ -17,6 +17,7 @@
} from '$lib/stores/asset-interaction.store';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import Close from 'svelte-material-icons/Close.svelte';
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import Plus from 'svelte-material-icons/Plus.svelte';
@@ -26,7 +27,7 @@
NotificationType
} from '$lib/components/shared-components/notification/notification';
import { assetStore } from '$lib/stores/assets.store';
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
export let data: PageData;
@@ -106,6 +107,12 @@
assetInteractionStore.clearMultiselect();
});
};
const handleDownloadFiles = async () => {
await bulkDownload('immich', Array.from($selectedAssets), () => {
assetInteractionStore.clearMultiselect();
});
};
</script>
<svelte:head>
@@ -125,6 +132,11 @@
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Download"
logo={CloudDownloadOutline}
on:click={handleDownloadFiles}
/>
<CircleIconButton title="Add" logo={Plus} on:click={handleShowMenu} />
<CircleIconButton
title="Delete"

View File

@@ -0,0 +1,9 @@
<svelte:head>
<title>Opps! Error - Immich</title>
</svelte:head>
<section class="w-screen h-screen flex place-items-center place-content-center">
<div class="p-20 text-4xl dark:text-immich-dark-primary text-immich-primary">
Page not found :/
</div>
</section>

View File

@@ -0,0 +1,18 @@
export const prerender = false;
import { error } from '@sveltejs/kit';
import { serverApi } from '@api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
const { key } = params;
try {
const { data: sharedLink } = await serverApi.shareApi.getMySharedLink({ params: { key } });
return { sharedLink };
} catch (e) {
throw error(404, {
message: 'Invalid shared link'
});
}
};

View File

@@ -0,0 +1,22 @@
<script lang="ts">
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
import { AlbumResponseDto } from '../../../api';
import type { PageData } from './$types';
export let data: PageData;
let album: AlbumResponseDto | null = null;
if (data.sharedLink.album) {
album = { ...data.sharedLink.album, assets: data.sharedLink.assets };
}
</script>
<svelte:head>
<title>{data.sharedLink.album?.albumName || 'Public Shared'} - Immich</title>
</svelte:head>
{#if album}
<div class="immich-scrollbar">
<AlbumViewer {album} sharedLink={data.sharedLink} />
</div>
{/if}

View File

@@ -0,0 +1,21 @@
export const prerender = false;
import { error } from '@sveltejs/kit';
import { serverApi } from '@api';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => {
try {
const { key, assetId } = params;
const { data: asset } = await serverApi.assetApi.getAssetById(assetId, {
params: { key }
});
if (!asset) {
return error(404, 'Asset not found');
}
return { asset, key };
} catch (e) {
console.log('Error', e);
}
};

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
import type { PageData } from './$types';
import { goto } from '$app/navigation';
export let data: PageData;
</script>
{#if data.asset && data.key}
<AssetViewer
asset={data.asset}
publicSharedKey={data.key}
on:navigate-previous={() => null}
on:navigate-next={() => null}
showNavigation={false}
on:close={() => goto(`/share/${data.key}`)}
/>
{/if}

View File

@@ -2,6 +2,8 @@
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
import Link from 'svelte-material-icons/Link.svelte';
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
import { goto } from '$app/navigation';
import { api } from '@api';
@@ -55,7 +57,7 @@
<p class="font-medium">Sharing</p>
</div>
<div>
<div class="flex">
<button
on:click={createSharedAlbum}
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700 dark:hover:bg-immich-dark-primary/25 dark:text-immich-dark-fg"
@@ -65,6 +67,16 @@
</span>
<p>Create shared album</p>
</button>
<button
on:click={() => goto('/sharing/sharedlinks')}
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700 dark:hover:bg-immich-dark-primary/25 dark:text-immich-dark-fg"
>
<span>
<Link size="18" />
</span>
<p>Shared links</p>
</button>
</div>
</div>

View File

@@ -0,0 +1,18 @@
import { redirect } from '@sveltejs/kit';
export const prerender = false;
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent }) => {
try {
const { user } = await parent();
if (!user) {
throw redirect(302, '/auth/login');
}
return {
user
};
} catch (e) {
throw redirect(302, '/auth/login');
}
};

View File

@@ -0,0 +1,109 @@
<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';
import {
notificationController,
NotificationType
} 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';
let sharedLinks: SharedLinkResponseDto[] = [];
let showEditForm = false;
let editSharedLink: SharedLinkResponseDto;
onMount(async () => {
sharedLinks = await getSharedLinks();
});
const getSharedLinks = async () => {
const { data: sharedLinks } = await api.shareApi.getAllSharedLinks();
return sharedLinks;
};
const handleDeleteLink = async (linkId: string) => {
if (window.confirm('Do you want to delete the shared link? ')) {
try {
await api.shareApi.removeSharedLink(linkId);
notificationController.show({
message: 'Shared link deleted',
type: NotificationType.Info
});
sharedLinks = await getSharedLinks();
} catch (e) {
console.error(e);
notificationController.show({
message: 'Failed to delete shared link',
type: NotificationType.Error
});
}
}
};
const handleEditLink = async (id: string) => {
const { data } = await api.shareApi.getSharedLinkById(id);
editSharedLink = data;
showEditForm = true;
};
const handleEditDone = async () => {
sharedLinks = await getSharedLinks();
showEditForm = false;
};
const handleCopy = async (key: string) => {
const link = `${window.location.origin}/share/${key}`;
await navigator.clipboard.writeText(link);
notificationController.show({
message: 'Link copied to clipboard',
type: NotificationType.Info
});
};
</script>
<svelte:head>
<title>Shared links - Immich</title>
</svelte:head>
<ControlAppBar backIcon={ArrowLeft} on:close-button-click={() => goto('/sharing')}>
<svelte:fragment slot="leading">Shared links</svelte:fragment>
</ControlAppBar>
<section class="flex flex-col pb-[120px] mt-[120px]">
<div class="w-[50%] m-auto mb-4 dark:text-immich-gray">
<p>Manage shared links</p>
</div>
{#if sharedLinks.length === 0}
<div
class="w-[50%] m-auto bg-gray-100 flex place-items-center place-content-center rounded-lg p-12"
>
<p>You don't have any shared links</p>
</div>
{:else}
<div class="flex flex-col w-[50%] m-auto">
{#each sharedLinks as link (link.id)}
<SharedLinkCard
{link}
on:delete={() => handleDeleteLink(link.id)}
on:edit={() => handleEditLink(link.id)}
on:copy={() => handleCopy(link.key)}
/>
{/each}
</div>
{/if}
</section>
{#if showEditForm}
<CreateSharedLinkModal
editingLink={editSharedLink}
shareType={editSharedLink.type}
album={editSharedLink.album}
on:close={handleEditDone}
/>
{/if}