mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(web/server) public album sharing (#1266)
This commit is contained in:
@@ -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) {
|
||||
|
||||
642
web/src/api/open-api/api.ts
generated
642
web/src/api/open-api/api.ts
generated
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
>.
|
||||
</p>
|
||||
|
||||
<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
|
||||
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -93,6 +93,7 @@ describe('AlbumCard component', () => {
|
||||
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
|
||||
'thumbnailIdOne',
|
||||
ThumbnailFormat.Jpeg,
|
||||
'',
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailBlob);
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
<svelte:fragment slot="trailing">
|
||||
<button
|
||||
on:click={() =>
|
||||
openFileUploadDialog(albumId, () => {
|
||||
openFileUploadDialog(albumId, '', () => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
dispatch('go-back');
|
||||
})}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
142
web/src/lib/components/sharedlinks-page/shared-link-card.svelte
Normal file
142
web/src/lib/components/sharedlinks-page/shared-link-card.svelte
Normal 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>
|
||||
@@ -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.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
|
||||
9
web/src/routes/share/[key]/+error.svelte
Normal file
9
web/src/routes/share/[key]/+error.svelte
Normal 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>
|
||||
18
web/src/routes/share/[key]/+page.server.ts
Normal file
18
web/src/routes/share/[key]/+page.server.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
};
|
||||
22
web/src/routes/share/[key]/+page.svelte
Normal file
22
web/src/routes/share/[key]/+page.svelte
Normal 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}
|
||||
21
web/src/routes/share/[key]/photos/[assetId]/+page.server.ts
Normal file
21
web/src/routes/share/[key]/photos/[assetId]/+page.server.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
17
web/src/routes/share/[key]/photos/[assetId]/+page.svelte
Normal file
17
web/src/routes/share/[key]/photos/[assetId]/+page.svelte
Normal 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}
|
||||
@@ -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>
|
||||
|
||||
|
||||
18
web/src/routes/sharing/sharedlinks/+page.server.ts
Normal file
18
web/src/routes/sharing/sharedlinks/+page.server.ts
Normal 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');
|
||||
}
|
||||
};
|
||||
109
web/src/routes/sharing/sharedlinks/+page.svelte
Normal file
109
web/src/routes/sharing/sharedlinks/+page.svelte
Normal 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}
|
||||
Reference in New Issue
Block a user