1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +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/CreateAlbumDto.md
doc/CreateAlbumShareLinkDto.md
doc/CreateAssetsShareLinkDto.md
doc/CreateProfileImageResponseDto.md
doc/CreateTagDto.md
doc/CreateUserDto.md
@ -86,6 +87,7 @@ doc/ThumbnailFormat.md
doc/TimeGroupEnum.md
doc/UpdateAlbumDto.md
doc/UpdateAssetDto.md
doc/UpdateAssetsToSharedLinkDto.md
doc/UpdateTagDto.md
doc/UpdateUserDto.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/create_album_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_tag_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/update_album_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_user_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/create_album_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_tag_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/update_album_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_user_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,
Put,
UploadedFiles,
Patch,
} from '@nestjs/common';
import { Authenticated } from '../../decorators/authenticated.decorator';
import { AssetService } from './asset.service';
@ -50,6 +51,9 @@ import {
IMMICH_CONTENT_LENGTH_HINT,
} from '../../constants/download.constant';
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()
@ApiTags('Asset')
@ -321,4 +325,22 @@ export class AssetController {
): Promise<CheckExistingAssetsResponseDto> {
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 { QueryFailedError, Repository } from 'typeorm';
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 { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express';
@ -59,6 +59,9 @@ import { StorageService } from '@app/storage';
import { ShareCore } from '../share/share.core';
import { ISharedLinkRepository } from '../share/shared-link.repository';
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);
@ -699,6 +702,42 @@ export class AssetService {
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> {

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": {
"get": {
"operationId": "getAllSharedLinks",
@ -3548,6 +3620,35 @@
"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": {
"type": "string",
"enum": [
@ -3654,6 +3755,20 @@
"allowUpload"
]
},
"UpdateAssetsToSharedLinkDto": {
"type": "object",
"properties": {
"assetIds": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"assetIds"
]
},
"EditSharedLinkDto": {
"type": "object",
"properties": {

View file

@ -4,7 +4,7 @@
* Immich
* 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).
@ -702,6 +702,37 @@ export interface CreateAlbumShareLinkDto {
*/
'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
@ -2029,6 +2060,19 @@ export interface UpdateAssetDto {
*/
'isFavorite'?: boolean;
}
/**
*
* @export
* @interface UpdateAssetsToSharedLinkDto
*/
export interface UpdateAssetsToSharedLinkDto {
/**
*
* @type {Array<string>}
* @memberof UpdateAssetsToSharedLinkDto
*/
'assetIds': Array<string>;
}
/**
*
* @export
@ -3599,6 +3643,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
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
@ -4255,6 +4338,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
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
@ -4329,6 +4451,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options);
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
@ -4501,6 +4633,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options);
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
@ -4539,6 +4681,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise<CheckExistingAssetsResponseDto> {
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
@ -4694,6 +4845,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
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
@ -4735,6 +4895,17 @@ export class AssetApi extends BaseAPI {
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
@ -4924,6 +5095,17 @@ export class AssetApi extends BaseAPI {
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
@ -5300,6 +5482,7 @@ export const DeviceInfoApiAxiosParamCreator = function (configuration?: Configur
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
createDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -5339,6 +5522,7 @@ export const DeviceInfoApiAxiosParamCreator = function (configuration?: Configur
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
updateDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@ -5427,6 +5611,7 @@ export const DeviceInfoApiFp = function(configuration?: Configuration) {
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
async createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> {
@ -5437,6 +5622,7 @@ export const DeviceInfoApiFp = function(configuration?: Configuration) {
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
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
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> {
@ -5476,6 +5663,7 @@ export const DeviceInfoApiFactory = function (configuration?: Configuration, bas
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
*/
updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> {
@ -5504,6 +5692,7 @@ export class DeviceInfoApi extends BaseAPI {
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
* @memberof DeviceInfoApi
*/
@ -5515,6 +5704,7 @@ export class DeviceInfoApi extends BaseAPI {
* @deprecated
* @param {UpsertDeviceInfoDto} upsertDeviceInfoDto
* @param {*} [options] Override http request option.
* @deprecated
* @throws {RequiredError}
* @memberof DeviceInfoApi
*/

View file

@ -4,7 +4,7 @@
* Immich
* 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).

View file

@ -4,7 +4,7 @@
* Immich
* 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).

View file

@ -4,7 +4,7 @@
* Immich
* 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).

View file

@ -4,7 +4,7 @@
* Immich
* 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).

View file

@ -1,13 +1,11 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/stores';
import {
AlbumResponseDto,
api,
AssetResponseDto,
SharedLinkResponseDto,
SharedLinkType,
ThumbnailFormat,
UserResponseDto
} from '@api';
import { onMount } from 'svelte';
@ -15,9 +13,7 @@
import Plus from 'svelte-material-icons/Plus.svelte';
import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.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 ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
import AssetSelection from './asset-selection.svelte';
import UserSelectionModal from './user-selection-modal.svelte';
import ShareInfoModal from './share-info-modal.svelte';
@ -43,14 +39,13 @@
import ThemeButton from '../shared-components/theme-button.svelte';
import { openFileUploadDialog } from '$lib/utils/file-uploader';
import { bulkDownload } from '$lib/utils/asset-utils';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
export let album: AlbumResponseDto;
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
let isShowAssetViewer = false;
let isShowAssetSelection = false;
let isShowShareLinkModal = false;
@ -72,11 +67,6 @@
let isShowAlbumOptions = false;
let isShowThumbnailSelection = false;
let selectedAsset: AssetResponseDto;
let currentViewAssetIndex = 0;
let viewWidth: number;
let thumbnailSize = 300;
let backUrl = '/albums';
let currentAlbumName = '';
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 albumDateFormat: Intl.DateTimeFormatOptions = {
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 = () => {
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
$: {
@ -606,34 +528,11 @@
{/if}
{#if album.assetCount > 0}
<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
{#each album.assets as asset}
{#key asset.id}
{#if album.assetCount < 7}
<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>
<GalleryViewer
assets={album.assets}
key={sharedLink?.key ?? ''}
bind:selectedAssets={multiSelectAsset}
/>
{:else}
<!-- Album is empty - Show asset selectection buttons -->
<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
@ -654,17 +553,6 @@
</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}
<AssetSelection
albumId={album.id}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
<script lang="ts">
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';
export let data: PageData;
@ -8,13 +9,20 @@
const { sharedLink } = data;
let album: AlbumResponseDto | null = null;
let isOwned = data.user ? data.user.id === sharedLink.userId : false;
if (sharedLink.album) {
album = { ...sharedLink.album, assets: sharedLink.assets };
}
</script>
{#if album}
{#if sharedLink.type == SharedLinkType.Album && album}
<div class="immich-scrollbar">
<AlbumViewer {album} {sharedLink} />
</div>
{/if}
{#if sharedLink.type == SharedLinkType.Individual}
<div class="immich-scrollbar">
<IndividualSharedViewer {sharedLink} {isOwned} />
</div>
{/if}