1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-01 08:31:59 +00:00

feat(web) Individual assets shared mechanism (#1317)

* Create shared link modal for individual asset

* Added API to create asset shared link

* Added viewer for individual shared link

* Added multiselection app bar

* Refactor gallery viewer to its own component

* Refactor

* Refactor

* Add and remove asset from shared link

* Fixed test

* Fixed notification card doesn't wrap

* Add check asset access when created asset shared link

* pr feedback
This commit is contained in:
Alex 2023-01-14 23:49:47 -06:00 committed by GitHub
parent b9b2b559a1
commit e9fda40b2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 791 additions and 168 deletions

View file

@ -31,6 +31,7 @@ doc/CheckExistingAssetsDto.md
doc/CheckExistingAssetsResponseDto.md doc/CheckExistingAssetsResponseDto.md
doc/CreateAlbumDto.md doc/CreateAlbumDto.md
doc/CreateAlbumShareLinkDto.md doc/CreateAlbumShareLinkDto.md
doc/CreateAssetsShareLinkDto.md
doc/CreateProfileImageResponseDto.md doc/CreateProfileImageResponseDto.md
doc/CreateTagDto.md doc/CreateTagDto.md
doc/CreateUserDto.md doc/CreateUserDto.md
@ -86,6 +87,7 @@ doc/ThumbnailFormat.md
doc/TimeGroupEnum.md doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md doc/UpdateAssetDto.md
doc/UpdateAssetsToSharedLinkDto.md
doc/UpdateTagDto.md doc/UpdateTagDto.md
doc/UpdateUserDto.md doc/UpdateUserDto.md
doc/UpsertDeviceInfoDto.md doc/UpsertDeviceInfoDto.md
@ -140,6 +142,7 @@ lib/model/check_existing_assets_dto.dart
lib/model/check_existing_assets_response_dto.dart lib/model/check_existing_assets_response_dto.dart
lib/model/create_album_dto.dart lib/model/create_album_dto.dart
lib/model/create_album_share_link_dto.dart lib/model/create_album_share_link_dto.dart
lib/model/create_assets_share_link_dto.dart
lib/model/create_profile_image_response_dto.dart lib/model/create_profile_image_response_dto.dart
lib/model/create_tag_dto.dart lib/model/create_tag_dto.dart
lib/model/create_user_dto.dart lib/model/create_user_dto.dart
@ -188,6 +191,7 @@ lib/model/thumbnail_format.dart
lib/model/time_group_enum.dart lib/model/time_group_enum.dart
lib/model/update_album_dto.dart lib/model/update_album_dto.dart
lib/model/update_asset_dto.dart lib/model/update_asset_dto.dart
lib/model/update_assets_to_shared_link_dto.dart
lib/model/update_tag_dto.dart lib/model/update_tag_dto.dart
lib/model/update_user_dto.dart lib/model/update_user_dto.dart
lib/model/upsert_device_info_dto.dart lib/model/upsert_device_info_dto.dart
@ -224,6 +228,7 @@ test/check_existing_assets_dto_test.dart
test/check_existing_assets_response_dto_test.dart test/check_existing_assets_response_dto_test.dart
test/create_album_dto_test.dart test/create_album_dto_test.dart
test/create_album_share_link_dto_test.dart test/create_album_share_link_dto_test.dart
test/create_assets_share_link_dto_test.dart
test/create_profile_image_response_dto_test.dart test/create_profile_image_response_dto_test.dart
test/create_tag_dto_test.dart test/create_tag_dto_test.dart
test/create_user_dto_test.dart test/create_user_dto_test.dart
@ -279,6 +284,7 @@ test/thumbnail_format_test.dart
test/time_group_enum_test.dart test/time_group_enum_test.dart
test/update_album_dto_test.dart test/update_album_dto_test.dart
test/update_asset_dto_test.dart test/update_asset_dto_test.dart
test/update_assets_to_shared_link_dto_test.dart
test/update_tag_dto_test.dart test/update_tag_dto_test.dart
test/update_user_dto_test.dart test/update_user_dto_test.dart
test/upsert_device_info_dto_test.dart test/upsert_device_info_dto_test.dart

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -14,6 +14,7 @@ import {
Header, Header,
Put, Put,
UploadedFiles, UploadedFiles,
Patch,
} from '@nestjs/common'; } from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator'; import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service'; import { AssetService } from './asset.service';
@ -50,6 +51,9 @@ import {
IMMICH_CONTENT_LENGTH_HINT, IMMICH_CONTENT_LENGTH_HINT,
} from '../../constants/download.constant'; } from '../../constants/download.constant';
import { DownloadFilesDto } from './dto/download-files.dto'; import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
@ApiBearerAuth() @ApiBearerAuth()
@ApiTags('Asset') @ApiTags('Asset')
@ -321,4 +325,22 @@ export class AssetController {
): Promise<CheckExistingAssetsResponseDto> { ): Promise<CheckExistingAssetsResponseDto> {
return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto); return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
} }
@Authenticated()
@Post('/shared-link')
async createAssetsSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: CreateAssetsShareLinkDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.createAssetsSharedLink(authUser, dto);
}
@Authenticated({ isShared: true })
@Patch('/shared-link')
async updateAssetsInSharedLink(
@GetAuthUser() authUser: AuthUserDto,
@Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
return await this.assetService.updateAssetsInSharedLink(authUser, dto);
}
} }

View file

@ -13,7 +13,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { createHash, randomUUID } from 'node:crypto'; import { createHash, randomUUID } from 'node:crypto';
import { QueryFailedError, Repository } from 'typeorm'; import { QueryFailedError, Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator'; import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { AssetEntity, AssetType } from '@app/infra'; import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
import { constants, createReadStream, ReadStream, stat } from 'fs'; import { constants, createReadStream, ReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto'; import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express'; import { Response as Res } from 'express';
@ -59,6 +59,9 @@ import { StorageService } from '@app/storage';
import { ShareCore } from '../share/share.core'; import { ShareCore } from '../share/share.core';
import { ISharedLinkRepository } from '../share/shared-link.repository'; import { ISharedLinkRepository } from '../share/shared-link.repository';
import { DownloadFilesDto } from './dto/download-files.dto'; import { DownloadFilesDto } from './dto/download-files.dto';
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
const fileInfo = promisify(stat); const fileInfo = promisify(stat);
@ -699,6 +702,42 @@ export class AssetService {
throw new ForbiddenException(); throw new ForbiddenException();
} }
} }
async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> {
const assets = [];
await this.checkAssetsAccess(authUser, dto.assetIds);
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const sharedLink = await this.shareCore.createSharedLink(authUser.id, {
sharedType: SharedLinkType.INDIVIDUAL,
expiredAt: dto.expiredAt,
allowUpload: dto.allowUpload,
assets: assets,
description: dto.description,
});
return mapSharedLinkToResponseDto(sharedLink);
}
async updateAssetsInSharedLink(
authUser: AuthUserDto,
dto: UpdateAssetsToSharedLinkDto,
): Promise<SharedLinkResponseDto> {
if (!authUser.sharedLinkId) throw new ForbiddenException();
const assets = [];
for (const assetId of dto.assetIds) {
const asset = await this._assetRepository.getById(assetId);
assets.push(asset);
}
const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets);
return mapSharedLinkToResponseDto(updatedLink);
}
} }
async function processETag(path: string, res: Res, headers: Record<string, string>): Promise<boolean> { async function processETag(path: string, res: Res, headers: Record<string, string>): Promise<boolean> {

View file

@ -0,0 +1,6 @@
import { IsNotEmpty } from 'class-validator';
export class UpdateAssetsToSharedLinkDto {
@IsNotEmpty()
assetIds!: string[];
}

View file

@ -0,0 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
export class CreateAssetsShareLinkDto {
@IsArray()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@ApiProperty({
isArray: true,
type: String,
title: 'Array asset IDs to be shared',
example: [
'bf973405-3f2a-48d2-a687-2ed4167164be',
'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
],
})
assetIds!: string[];
@IsString()
@IsOptional()
expiredAt?: string;
@IsBoolean()
@IsOptional()
allowUpload?: boolean;
@IsString()
@IsOptional()
description?: string;
}

View file

@ -1258,6 +1258,78 @@
] ]
} }
}, },
"/asset/shared-link": {
"post": {
"operationId": "createAssetsSharedLink",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateAssetsShareLinkDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedLinkResponseDto"
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
}
]
},
"patch": {
"operationId": "updateAssetsInSharedLink",
"description": "",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto"
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SharedLinkResponseDto"
}
}
}
}
},
"tags": [
"Asset"
],
"security": [
{
"bearer": []
}
]
}
},
"/share": { "/share": {
"get": { "get": {
"operationId": "getAllSharedLinks", "operationId": "getAllSharedLinks",
@ -3548,6 +3620,35 @@
"existingIds" "existingIds"
] ]
}, },
"CreateAssetsShareLinkDto": {
"type": "object",
"properties": {
"assetIds": {
"title": "Array asset IDs to be shared",
"example": [
"bf973405-3f2a-48d2-a687-2ed4167164be",
"dd41870b-5d00-46d2-924e-1d8489a0aa0f",
"fad77c3f-deef-4e7e-9608-14c1aa4e559a"
],
"type": "array",
"items": {
"type": "string"
}
},
"expiredAt": {
"type": "string"
},
"allowUpload": {
"type": "boolean"
},
"description": {
"type": "string"
}
},
"required": [
"assetIds"
]
},
"SharedLinkType": { "SharedLinkType": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -3654,6 +3755,20 @@
"allowUpload" "allowUpload"
] ]
}, },
"UpdateAssetsToSharedLinkDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"EditSharedLinkDto": { "EditSharedLinkDto": {
"type": "object", "type": "object",
"properties": { "properties": {

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.40.0 * The version of the OpenAPI document: 1.41.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -702,6 +702,37 @@ export interface CreateAlbumShareLinkDto {
*/ */
'description'?: string; 'description'?: string;
} }
/**
*
* @export
* @interface CreateAssetsShareLinkDto
*/
export interface CreateAssetsShareLinkDto {
/**
*
* @type {Array<string>}
* @memberof CreateAssetsShareLinkDto
*/
'assetIds': Array<string>;
/**
*
* @type {string}
* @memberof CreateAssetsShareLinkDto
*/
'expiredAt'?: string;
/**
*
* @type {boolean}
* @memberof CreateAssetsShareLinkDto
*/
'allowUpload'?: boolean;
/**
*
* @type {string}
* @memberof CreateAssetsShareLinkDto
*/
'description'?: string;
}
/** /**
* *
* @export * @export
@ -2029,6 +2060,19 @@ export interface UpdateAssetDto {
*/ */
'isFavorite'?: boolean; 'isFavorite'?: boolean;
} }
/**
*
* @export
* @interface UpdateAssetsToSharedLinkDto
*/
export interface UpdateAssetsToSharedLinkDto {
/**
*
* @type {Array<string>}
* @memberof UpdateAssetsToSharedLinkDto
*/
'assetIds': Array<string>;
}
/** /**
* *
* @export * @export
@ -3599,6 +3643,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createAssetsSharedLink: async (createAssetsShareLinkDto: CreateAssetsShareLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'createAssetsShareLinkDto' is not null or undefined
assertParamExists('createAssetsSharedLink', 'createAssetsShareLinkDto', createAssetsShareLinkDto)
const localVarPath = `/asset/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(createAssetsShareLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {DeleteAssetDto} deleteAssetDto * @param {DeleteAssetDto} deleteAssetDto
@ -4255,6 +4338,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
options: localVarRequestOptions, options: localVarRequestOptions,
}; };
}, },
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAssetsInSharedLink: async (updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'updateAssetsToSharedLinkDto' is not null or undefined
assertParamExists('updateAssetsInSharedLink', 'updateAssetsToSharedLinkDto', updateAssetsToSharedLinkDto)
const localVarPath = `/asset/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: 'PATCH', ...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(updateAssetsToSharedLinkDto, localVarRequestOptions, configuration)
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/** /**
* *
* @param {any} assetData * @param {any} assetData
@ -4329,6 +4451,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options); const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.createAssetsSharedLink(createAssetsShareLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {DeleteAssetDto} deleteAssetDto * @param {DeleteAssetDto} deleteAssetDto
@ -4501,6 +4633,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options); const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
}, },
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/** /**
* *
* @param {any} assetData * @param {any} assetData
@ -4539,6 +4681,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise<CheckExistingAssetsResponseDto> { checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise<CheckExistingAssetsResponseDto> {
return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath)); return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.createAssetsSharedLink(createAssetsShareLinkDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {DeleteAssetDto} deleteAssetDto * @param {DeleteAssetDto} deleteAssetDto
@ -4694,6 +4845,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> { updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath)); return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath));
}, },
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
return localVarFp.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(axios, basePath));
},
/** /**
* *
* @param {any} assetData * @param {any} assetData
@ -4735,6 +4895,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).createAssetsSharedLink(createAssetsShareLinkDto, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {DeleteAssetDto} deleteAssetDto * @param {DeleteAssetDto} deleteAssetDto
@ -4924,6 +5095,17 @@ export class AssetApi extends BaseAPI {
return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath)); return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath));
} }
/**
*
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof AssetApi
*/
public updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
}
/** /**
* *
* @param {any} assetData * @param {any} assetData
@ -5300,6 +5482,7 @@ export const DeviceInfoApiAxiosParamCreator = function (configuration?: Configur
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
createDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { createDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -5339,6 +5522,7 @@ export const DeviceInfoApiAxiosParamCreator = function (configuration?: Configur
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
updateDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { updateDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -5427,6 +5611,7 @@ export const DeviceInfoApiFp = function(configuration?: Configuration) {
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> { async createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> {
@ -5437,6 +5622,7 @@ export const DeviceInfoApiFp = function(configuration?: Configuration) {
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
async updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> { async updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> {
@ -5467,6 +5653,7 @@ export const DeviceInfoApiFactory = function (configuration?: Configuration, bas
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> { createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> {
@ -5476,6 +5663,7 @@ export const DeviceInfoApiFactory = function (configuration?: Configuration, bas
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
*/ */
updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> { updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> {
@ -5504,6 +5692,7 @@ export class DeviceInfoApi extends BaseAPI {
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
* @memberof DeviceInfoApi * @memberof DeviceInfoApi
*/ */
@ -5515,6 +5704,7 @@ export class DeviceInfoApi extends BaseAPI {
* @deprecated * @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option. * @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError} * @throws {RequiredError}
* @memberof DeviceInfoApi * @memberof DeviceInfoApi
*/ */

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.40.0 * The version of the OpenAPI document: 1.41.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.40.0 * The version of the OpenAPI document: 1.41.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.40.0 * The version of the OpenAPI document: 1.41.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -4,7 +4,7 @@
* Immich * Immich
* Immich API * Immich API
* *
* The version of the OpenAPI document: 1.40.0 * The version of the OpenAPI document: 1.41.1
* *
* *
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View file

@ -1,13 +1,11 @@
<script lang="ts"> <script lang="ts">
import { afterNavigate, goto } from '$app/navigation'; import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import { import {
AlbumResponseDto, AlbumResponseDto,
api, api,
AssetResponseDto, AssetResponseDto,
SharedLinkResponseDto, SharedLinkResponseDto,
SharedLinkType, SharedLinkType,
ThumbnailFormat,
UserResponseDto UserResponseDto
} from '@api'; } from '@api';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -15,9 +13,7 @@
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte'; import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte'; import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
import CircleAvatar from '../shared-components/circle-avatar.svelte'; import CircleAvatar from '../shared-components/circle-avatar.svelte';
import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import AssetSelection from './asset-selection.svelte'; import AssetSelection from './asset-selection.svelte';
import UserSelectionModal from './user-selection-modal.svelte'; import UserSelectionModal from './user-selection-modal.svelte';
import ShareInfoModal from './share-info-modal.svelte'; import ShareInfoModal from './share-info-modal.svelte';
@ -43,14 +39,13 @@
import ThemeButton from '../shared-components/theme-button.svelte'; import ThemeButton from '../shared-components/theme-button.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { bulkDownload } from '$lib/utils/asset-utils'; import { bulkDownload } from '$lib/utils/asset-utils';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
export let album: AlbumResponseDto; export let album: AlbumResponseDto;
export let sharedLink: SharedLinkResponseDto | undefined = undefined; export let sharedLink: SharedLinkResponseDto | undefined = undefined;
const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore; const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
let isShowAssetViewer = false;
let isShowAssetSelection = false; let isShowAssetSelection = false;
let isShowShareLinkModal = false; let isShowShareLinkModal = false;
@ -72,11 +67,6 @@
let isShowAlbumOptions = false; let isShowAlbumOptions = false;
let isShowThumbnailSelection = false; let isShowThumbnailSelection = false;
let selectedAsset: AssetResponseDto;
let currentViewAssetIndex = 0;
let viewWidth: number;
let thumbnailSize = 300;
let backUrl = '/albums'; let backUrl = '/albums';
let currentAlbumName = ''; let currentAlbumName = '';
let currentUser: UserResponseDto; let currentUser: UserResponseDto;
@ -97,18 +87,6 @@
} }
}); });
$: {
if (album.assets?.length < 6) {
thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
} else {
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);
}
}
const locale = navigator.language; const locale = navigator.language;
const albumDateFormat: Intl.DateTimeFormatOptions = { const albumDateFormat: Intl.DateTimeFormatOptions = {
month: 'short', month: 'short',
@ -140,28 +118,6 @@
} }
}); });
const viewAssetHandler = (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;
currentViewAssetIndex = album.assets.findIndex((a) => a.id == asset.id);
selectedAsset = album.assets[currentViewAssetIndex];
isShowAssetViewer = true;
pushState(selectedAsset.id);
};
const selectAssetHandler = (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;
let temp = new Set(multiSelectAsset);
if (multiSelectAsset.has(asset)) {
temp.delete(asset);
} else {
temp.add(asset);
}
multiSelectAsset = temp;
};
const clearMultiSelectAssetAssetHandler = () => { const clearMultiSelectAssetAssetHandler = () => {
multiSelectAsset = new Set(); multiSelectAsset = new Set();
}; };
@ -184,40 +140,6 @@
} }
} }
}; };
const navigateAssetForward = () => {
try {
if (currentViewAssetIndex < album.assetCount - 1) {
currentViewAssetIndex++;
selectedAsset = album.assets[currentViewAssetIndex];
pushState(selectedAsset.id);
}
} catch (e) {
console.error(e);
}
};
const navigateAssetBackward = () => {
try {
if (currentViewAssetIndex > 0) {
currentViewAssetIndex--;
selectedAsset = album.assets[currentViewAssetIndex];
pushState(selectedAsset.id);
}
} catch (e) {
console.error(e);
}
};
const pushState = (assetId: string) => {
// add a URL to the browser's history
// changes the current URL in the address bar but doesn't perform any SvelteKit navigation
history.pushState(null, '', `${$page.url.pathname}/photos/${assetId}`);
};
const closeViewer = () => {
isShowAssetViewer = false;
history.pushState(null, '', `${$page.url.pathname}`);
};
// Update Album Name // Update Album Name
$: { $: {
@ -606,34 +528,11 @@
{/if} {/if}
{#if album.assetCount > 0} {#if album.assetCount > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}> <GalleryViewer
{#each album.assets as asset} assets={album.assets}
{#key asset.id} key={sharedLink?.key ?? ''}
{#if album.assetCount < 7} bind:selectedAssets={multiSelectAsset}
<ImmichThumbnail />
{asset}
{thumbnailSize}
publicSharedKey={sharedLink?.key}
format={ThumbnailFormat.Jpeg}
on:click={(e) =>
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
on:select={selectAssetHandler}
selected={multiSelectAsset.has(asset)}
/>
{:else}
<ImmichThumbnail
{asset}
{thumbnailSize}
publicSharedKey={sharedLink?.key}
on:click={(e) =>
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
on:select={selectAssetHandler}
selected={multiSelectAsset.has(asset)}
/>
{/if}
{/key}
{/each}
</div>
{:else} {:else}
<!-- Album is empty - Show asset selectection buttons --> <!-- Album is empty - Show asset selectection buttons -->
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center"> <section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
@ -654,17 +553,6 @@
</section> </section>
</section> </section>
<!-- Overlay Asset Viewer -->
{#if isShowAssetViewer}
<AssetViewer
asset={selectedAsset}
publicSharedKey={sharedLink?.key}
on:navigate-previous={navigateAssetBackward}
on:navigate-next={navigateAssetForward}
on:close={closeViewer}
/>
{/if}
{#if isShowAssetSelection} {#if isShowAssetSelection}
<AssetSelection <AssetSelection
albumId={album.id} albumId={album.id}

View file

@ -233,7 +233,7 @@
<section <section
id="immich-asset-viewer" id="immich-asset-viewer"
class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4" class="fixed h-screen w-screen left-0 top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4"
> >
<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform"> <div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
<AssetViewerNavBar <AssetViewerNavBar

View file

@ -0,0 +1,150 @@
<script lang="ts">
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import { goto } from '$app/navigation';
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { bulkDownload } from '$lib/utils/asset-utils';
import Close from 'svelte-material-icons/Close.svelte';
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import {
notificationController,
NotificationType
} from '../shared-components/notification/notification';
export let sharedLink: SharedLinkResponseDto;
export let isOwned: boolean;
let assets = sharedLink.assets;
let selectedAssets: Set<AssetResponseDto> = new Set();
$: isMultiSelectionMode = selectedAssets.size > 0;
const clearMultiSelectAssetAssetHandler = () => {
selectedAssets = new Set();
};
const downloadAssets = async (isAll: boolean) => {
await bulkDownload(
'immich-shared',
isAll ? assets : Array.from(selectedAssets),
() => {
isMultiSelectionMode = false;
clearMultiSelectAssetAssetHandler();
},
sharedLink?.key
);
};
const handleUploadAssets = () => {
openFileUploadDialog(undefined, sharedLink?.key, async (assetId) => {
await api.assetApi.updateAssetsInSharedLink(
{
assetIds: [...assets.map((a) => a.id), assetId]
},
{
params: {
key: sharedLink?.key
}
}
);
notificationController.show({
message: 'Add asset to shared link successfully',
type: NotificationType.Info
});
});
};
const handleRemoveAssetsFromSharedLink = async () => {
if (window.confirm('Do you want to remove selected assets from the shared link?')) {
await api.assetApi.updateAssetsInSharedLink(
{
assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id)
},
{
params: {
key: sharedLink?.key
}
}
);
assets = assets.filter((a) => !selectedAssets.has(a));
clearMultiSelectAssetAssetHandler();
}
};
</script>
<section class="bg-immich-bg dark:bg-immich-dark-bg">
{#if isMultiSelectionMode}
<ControlAppBar
on:close-button-click={clearMultiSelectAssetAssetHandler}
backIcon={Close}
tailwindClasses={'bg-white shadow-md'}
>
<svelte:fragment slot="leading">
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
Selected {selectedAssets.size}
</p>
</svelte:fragment>
<svelte:fragment slot="trailing">
<CircleIconButton
title="Download"
on:click={() => downloadAssets(false)}
logo={CloudDownloadOutline}
/>
{#if isOwned}
<CircleIconButton
title="Remove from album"
on:click={handleRemoveAssetsFromSharedLink}
logo={DeleteOutline}
/>
{/if}
</svelte:fragment>
</ControlAppBar>
{:else}
<ControlAppBar
on:close-button-click={() => goto('/photos')}
backIcon={ArrowLeft}
showBackButton={false}
>
<svelte:fragment slot="leading">
<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>
</svelte:fragment>
<svelte:fragment slot="trailing">
{#if sharedLink?.allowUpload}
<CircleIconButton
title="Add Photos"
on:click={handleUploadAssets}
logo={FileImagePlusOutline}
/>
{/if}
<CircleIconButton
title="Download"
on:click={() => downloadAssets(true)}
logo={FolderDownloadOutline}
/>
</svelte:fragment>
</ControlAppBar>
{/if}
<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40">
<GalleryViewer {assets} key={sharedLink.key} bind:selectedAssets />
</section>
</section>

View file

@ -2,7 +2,13 @@
import { createEventDispatcher, onMount } from 'svelte'; import { createEventDispatcher, onMount } from 'svelte';
import BaseModal from '../base-modal.svelte'; import BaseModal from '../base-modal.svelte';
import Link from 'svelte-material-icons/Link.svelte'; import Link from 'svelte-material-icons/Link.svelte';
import { AlbumResponseDto, api, SharedLinkResponseDto, SharedLinkType } from '@api'; import {
AlbumResponseDto,
api,
AssetResponseDto,
SharedLinkResponseDto,
SharedLinkType
} from '@api';
import { notificationController, NotificationType } from '../notification/notification'; import { notificationController, NotificationType } from '../notification/notification';
import { ImmichDropDownOption } from '../dropdown-button.svelte'; import { ImmichDropDownOption } from '../dropdown-button.svelte';
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
@ -10,9 +16,11 @@
import SettingInputField, { import SettingInputField, {
SettingInputFieldType SettingInputFieldType
} from '$lib/components/admin-page/settings/setting-input-field.svelte'; } from '$lib/components/admin-page/settings/setting-input-field.svelte';
import { handleError } from '$lib/utils/handle-error';
export let shareType: SharedLinkType; export let shareType: SharedLinkType;
export let album: AlbumResponseDto | undefined; export let sharedAssets: AssetResponseDto[] = [];
export let album: AlbumResponseDto | undefined = undefined;
export let editingLink: SharedLinkResponseDto | undefined = undefined; export let editingLink: SharedLinkResponseDto | undefined = undefined;
let isShowSharedLink = false; let isShowSharedLink = false;
@ -37,32 +45,36 @@
} }
}); });
const createAlbumSharedLink = async () => { const handleCreateSharedLink = async () => {
if (album) { const expirationTime = getExpirationTimeInMillisecond();
try { const currentTime = new Date().getTime();
const expirationTime = getExpirationTimeInMillisecond(); const expirationDate = expirationTime
const currentTime = new Date().getTime(); ? new Date(currentTime + expirationTime).toISOString()
const expirationDate = expirationTime : undefined;
? new Date(currentTime + expirationTime).toISOString()
: undefined;
try {
if (shareType === SharedLinkType.Album && album) {
const { data } = await api.albumApi.createAlbumSharedLink({ const { data } = await api.albumApi.createAlbumSharedLink({
albumId: album.id, albumId: album.id,
expiredAt: expirationDate, expiredAt: expirationDate,
allowUpload: isAllowUpload, allowUpload: isAllowUpload,
description: description description: description
}); });
buildSharedLink(data); buildSharedLink(data);
isShowSharedLink = true; } else {
} catch (e) { const { data } = await api.assetApi.createAssetsSharedLink({
console.error('[createAlbumSharedLink] Error: ', e); assetIds: sharedAssets.map((a) => a.id),
notificationController.show({ expiredAt: expirationDate,
type: NotificationType.Error, allowUpload: isAllowUpload,
message: 'Failed to create shared link' description: description
}); });
buildSharedLink(data);
} }
} catch (e) {
handleError(e, 'Failed to create shared link');
} }
isShowSharedLink = true;
}; };
const buildSharedLink = (createdLink: SharedLinkResponseDto) => { const buildSharedLink = (createdLink: SharedLinkResponseDto) => {
@ -76,8 +88,11 @@
message: 'Copied to clipboard!', message: 'Copied to clipboard!',
type: NotificationType.Info type: NotificationType.Info
}); });
} catch (error) { } catch (e) {
console.error('Error', error); handleError(
e,
'Cannot copy to clipboard, make sure you are accessing the page through https'
);
} }
}; };
@ -127,11 +142,7 @@
dispatch('close'); dispatch('close');
} catch (e) { } catch (e) {
console.error('[handleEditLink]', e); handleError(e, 'Failed to edit shared link');
notificationController.show({
type: NotificationType.Error,
message: 'Failed to edit shared link'
});
} }
} }
}; };
@ -162,6 +173,18 @@
{/if} {/if}
{/if} {/if}
{#if shareType == SharedLinkType.Individual}
{#if !editingLink}
<div>Let anyone with the link see the selected photo(s)</div>
{:else}
<div class="text-sm">
Individual shared | <span class="text-immich-primary dark:text-immich-dark-primary"
>{editingLink.description}</span
>
</div>
{/if}
{/if}
<div class="mt-6 mb-2"> <div class="mt-6 mb-2">
<p class="text-xs">LINK OPTIONS</p> <p class="text-xs">LINK OPTIONS</p>
</div> </div>
@ -215,7 +238,7 @@
{:else} {:else}
<div class="flex justify-end"> <div class="flex justify-end">
<button <button
on:click={createAlbumSharedLink} on:click={handleCreateSharedLink}
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" 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 Create Link

View file

@ -0,0 +1,118 @@
<script lang="ts">
import { page } from '$app/stores';
import { handleError } from '$lib/utils/handle-error';
import { AssetResponseDto, ThumbnailFormat } from '@api';
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
import ImmichThumbnail from '../../shared-components/immich-thumbnail.svelte';
export let assets: AssetResponseDto[];
export let key: string;
export let selectedAssets: Set<AssetResponseDto> = new Set();
let isShowAssetViewer = false;
let selectedAsset: AssetResponseDto;
let currentViewAssetIndex = 0;
let viewWidth: number;
let thumbnailSize = 300;
$: isMultiSelectionMode = selectedAssets.size > 0;
$: {
if (assets.length < 6) {
thumbnailSize = Math.floor(viewWidth / assets.length - assets.length);
} else {
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);
}
}
const viewAssetHandler = (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
selectedAsset = assets[currentViewAssetIndex];
isShowAssetViewer = true;
pushState(selectedAsset.id);
};
const selectAssetHandler = (event: CustomEvent) => {
const { asset }: { asset: AssetResponseDto } = event.detail;
let temp = new Set(selectedAssets);
if (selectedAssets.has(asset)) {
temp.delete(asset);
} else {
temp.add(asset);
}
selectedAssets = temp;
};
const navigateAssetForward = () => {
try {
if (currentViewAssetIndex < assets.length - 1) {
currentViewAssetIndex++;
selectedAsset = assets[currentViewAssetIndex];
pushState(selectedAsset.id);
}
} catch (e) {
handleError(e, 'Cannot navigate to the next asset');
}
};
const navigateAssetBackward = () => {
try {
if (currentViewAssetIndex > 0) {
currentViewAssetIndex--;
selectedAsset = assets[currentViewAssetIndex];
pushState(selectedAsset.id);
}
} catch (e) {
handleError(e, 'Cannot navigate to previous asset');
}
};
const pushState = (assetId: string) => {
// add a URL to the browser's history
// changes the current URL in the address bar but doesn't perform any SvelteKit navigation
history.pushState(null, '', `${$page.url.pathname}/photos/${assetId}`);
};
const closeViewer = () => {
isShowAssetViewer = false;
history.pushState(null, '', `${$page.url.pathname}`);
};
</script>
{#if assets.length > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
{#each assets as asset (asset.id)}
<ImmichThumbnail
{asset}
{thumbnailSize}
publicSharedKey={key}
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
on:select={selectAssetHandler}
selected={selectedAssets.has(asset)}
/>
{/each}
</div>
{/if}
<!-- Overlay Asset Viewer -->
{#if isShowAssetViewer}
<AssetViewer
asset={selectedAsset}
publicSharedKey={key}
on:navigate-previous={navigateAssetBackward}
on:navigate-next={navigateAssetForward}
on:close={closeViewer}
/>
{/if}

View file

@ -90,7 +90,7 @@
</button> </button>
</div> </div>
<p class="whitespace-pre text-sm pl-[28px] pr-[16px]" data-testid="message"> <p class="whitespace-pre-wrap text-sm pl-[28px] pr-[16px]" data-testid="message">
{@html notificationInfo.message} {@html notificationInfo.message}
</p> </p>
</div> </div>

View file

@ -12,7 +12,7 @@ import { addAssetsToAlbum } from '$lib/utils/asset-utils';
export const openFileUploadDialog = ( export const openFileUploadDialog = (
albumId: string | undefined = undefined, albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined, sharedKey: string | undefined = undefined,
callback?: () => void onDone?: (id: string) => void
) => { ) => {
try { try {
const fileSelector = document.createElement('input'); const fileSelector = document.createElement('input');
@ -28,8 +28,7 @@ export const openFileUploadDialog = (
} }
const files = Array.from<File>(target.files); const files = Array.from<File>(target.files);
await fileUploadHandler(files, albumId, sharedKey); await fileUploadHandler(files, albumId, sharedKey, onDone);
callback && callback();
}; };
fileSelector.click(); fileSelector.click();
@ -41,7 +40,8 @@ export const openFileUploadDialog = (
export const fileUploadHandler = async ( export const fileUploadHandler = async (
files: File[], files: File[],
albumId: string | undefined = undefined, albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined sharedKey: string | undefined = undefined,
onDone?: (id: string) => void
) => { ) => {
if (files.length > 50) { if (files.length > 50) {
notificationController.show({ notificationController.show({
@ -54,13 +54,13 @@ export const fileUploadHandler = async (
return; return;
} }
console.log('fileUploadHandler');
const acceptedFile = files.filter( const acceptedFile = files.filter(
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image' (e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
); );
for (const asset of acceptedFile) { for (const asset of acceptedFile) {
await fileUploader(asset, albumId, sharedKey); await fileUploader(asset, albumId, sharedKey, onDone);
} }
}; };
@ -68,7 +68,8 @@ export const fileUploadHandler = async (
async function fileUploader( async function fileUploader(
asset: File, asset: File,
albumId: string | undefined = undefined, albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined sharedKey: string | undefined = undefined,
onDone?: (id: string) => void
) { ) {
const assetType = asset.type.split('/')[0].toUpperCase(); const assetType = asset.type.split('/')[0].toUpperCase();
const temp = asset.name.split('.'); const temp = asset.name.split('.');
@ -135,6 +136,7 @@ async function fileUploader(
if (albumId && dataId) { if (albumId && dataId) {
addAssetsToAlbum(albumId, [dataId]); addAssetsToAlbum(albumId, [dataId]);
} }
onDone && dataId && onDone(dataId);
return; return;
} }
} }
@ -154,10 +156,9 @@ async function fileUploader(
request.upload.onload = () => { request.upload.onload = () => {
setTimeout(() => { setTimeout(() => {
uploadAssetsStore.removeUploadAsset(deviceAssetId); uploadAssetsStore.removeUploadAsset(deviceAssetId);
const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
if (albumId) { if (albumId) {
try { try {
const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
if (res.id) { if (res.id) {
addAssetsToAlbum(albumId, [res.id], sharedKey); addAssetsToAlbum(albumId, [res.id], sharedKey);
} }
@ -165,6 +166,7 @@ async function fileUploader(
console.error('ERROR parsing data JSON in upload onload'); console.error('ERROR parsing data JSON in upload onload');
} }
} }
onDone && onDone(res.id);
}, 1000); }, 1000);
}; };

View file

@ -6,9 +6,8 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte'; import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import type { PageData } from './$types'; import type { PageData } from './$types';
import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { import {
assetInteractionStore, assetInteractionStore,
@ -21,16 +20,17 @@
import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte'; import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
import Plus from 'svelte-material-icons/Plus.svelte'; import Plus from 'svelte-material-icons/Plus.svelte';
import { AlbumResponseDto, api } from '@api'; import { AlbumResponseDto, api, SharedLinkType } from '@api';
import { import {
notificationController, notificationController,
NotificationType NotificationType
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { assetStore } from '$lib/stores/assets.store'; import { assetStore } from '$lib/stores/assets.store';
import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils'; import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
export let data: PageData; export let data: PageData;
let isShowCreateSharedLinkModal = false;
const deleteSelectedAssetHandler = async () => { const deleteSelectedAssetHandler = async () => {
try { try {
if ( if (
@ -114,6 +114,15 @@
assetInteractionStore.clearMultiselect(); assetInteractionStore.clearMultiselect();
}); });
}; };
const handleCreateSharedLink = async () => {
isShowCreateSharedLinkModal = true;
};
const handleCloseSharedLinkModal = () => {
assetInteractionStore.clearMultiselect();
isShowCreateSharedLinkModal = false;
};
</script> </script>
<section> <section>
@ -129,6 +138,11 @@
</p> </p>
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="trailing"> <svelte:fragment slot="trailing">
<CircleIconButton
title="Share"
logo={ShareVariantOutline}
on:click={handleCreateSharedLink}
/>
<CircleIconButton <CircleIconButton
title="Download" title="Download"
logo={CloudDownloadOutline} logo={CloudDownloadOutline}
@ -164,6 +178,14 @@
on:close={() => (isShowAlbumPicker = false)} on:close={() => (isShowAlbumPicker = false)}
/> />
{/if} {/if}
{#if isShowCreateSharedLinkModal}
<CreateSharedLinkModal
sharedAssets={Array.from($selectedAssets)}
shareType={SharedLinkType.Individual}
on:close={handleCloseSharedLinkModal}
/>
{/if}
</section> </section>
<section <section

View file

@ -5,7 +5,9 @@ import { getThumbnailUrl } from '$lib/utils/asset-utils';
import { serverApi, ThumbnailFormat } from '@api'; import { serverApi, ThumbnailFormat } from '@api';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params }) => { export const load: PageServerLoad = async ({ params, parent }) => {
const { user } = await parent();
const { key } = params; const { key } = params;
try { try {
@ -22,7 +24,8 @@ export const load: PageServerLoad = async ({ params }) => {
imageUrl: assetId imageUrl: assetId
? getThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key) ? getThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
: 'feature-panel.png' : 'feature-panel.png'
} },
user
}; };
} catch (e) { } catch (e) {
throw error(404, { throw error(404, {

View file

@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte'; import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
import { AlbumResponseDto } from '@api'; import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
import { AlbumResponseDto, SharedLinkType } from '@api';
import type { PageData } from './$types'; import type { PageData } from './$types';
export let data: PageData; export let data: PageData;
@ -8,13 +9,20 @@
const { sharedLink } = data; const { sharedLink } = data;
let album: AlbumResponseDto | null = null; let album: AlbumResponseDto | null = null;
let isOwned = data.user ? data.user.id === sharedLink.userId : false;
if (sharedLink.album) { if (sharedLink.album) {
album = { ...sharedLink.album, assets: sharedLink.assets }; album = { ...sharedLink.album, assets: sharedLink.assets };
} }
</script> </script>
{#if album} {#if sharedLink.type == SharedLinkType.Album && album}
<div class="immich-scrollbar"> <div class="immich-scrollbar">
<AlbumViewer {album} {sharedLink} /> <AlbumViewer {album} {sharedLink} />
</div> </div>
{/if} {/if}
{#if sharedLink.type == SharedLinkType.Individual}
<div class="immich-scrollbar">
<IndividualSharedViewer {sharedLink} {isOwned} />
</div>
{/if}