1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

feat(web/server) Add more options to public shared link (#1348)

* Added migration files

* Added logic for shared album level

* Added permission for EXIF

* Update shared link response dto

* Added condition to show download button

* Create and edit link with new parameter:

* Remove deadcode

* PR feedback

* More refactor

* Move logic of allow original file to service

* Simplify

* Wording
This commit is contained in:
Alex 2023-01-21 22:15:16 -06:00 committed by GitHub
parent 4cfac47674
commit b07891089f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 348 additions and 67 deletions

View File

@ -99,7 +99,7 @@ After making any changes in the `server/libs/database/src/entities`, a database
2. Run
```bash
npm run typeorm -- migration:generate ./libs/database/src/<migration-name> -d libs/database/src/config/database.config.ts
npm run typeorm -- migration:generate ./libs/infra/src/db/<migration-name> -d ./libs/infra/src/db/config/database.config.ts
```
3. Check if the migration file makes sense.

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

@ -140,6 +140,8 @@ export class AlbumController {
@Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
@Response({ passthrough: true }) res: Res,
): Promise<any> {
this.albumService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.albumService.downloadArchive(
authUser,
albumId,

View File

@ -15,7 +15,7 @@ import { DownloadService } from '../../modules/download/download.service';
import { DownloadDto } from '../asset/dto/download-library.dto';
import { ShareCore } from '../share/share.core';
import { ISharedLinkRepository } from '../share/shared-link.repository';
import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
import _ from 'lodash';
@ -210,8 +210,14 @@ export class AlbumService {
album: album,
assets: [],
description: dto.description,
allowDownload: dto.allowDownload,
showExif: dto.showExif,
});
return mapSharedLinkToResponseDto(sharedLink);
return mapSharedLink(sharedLink);
}
checkDownloadAccess(authUser: AuthUserDto) {
this.shareCore.checkDownloadAccess(authUser);
}
}

View File

@ -13,6 +13,14 @@ export class CreateAlbumShareLinkDto {
@IsOptional()
allowUpload?: boolean;
@IsBoolean()
@IsOptional()
allowDownload?: boolean;
@IsBoolean()
@IsOptional()
showExif?: boolean;
@IsString()
@IsOptional()
description?: string;

View File

@ -97,6 +97,7 @@ export class AssetController {
@Query(new ValidationPipe({ transform: true })) query: ServeFileDto,
@Param('assetId') assetId: string,
): Promise<any> {
this.assetService.checkDownloadAccess(authUser);
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.downloadFile(query, assetId, res);
}
@ -108,6 +109,7 @@ export class AssetController {
@Response({ passthrough: true }) res: Res,
@Body(new ValidationPipe()) dto: DownloadFilesDto,
): Promise<any> {
this.assetService.checkDownloadAccess(authUser);
await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]);
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto);
res.attachment(fileName);
@ -117,6 +119,9 @@ export class AssetController {
return stream;
}
/**
* Current this is not used in any UI element
*/
@Authenticated({ isShared: true })
@Get('/download-library')
async downloadLibrary(
@ -124,6 +129,7 @@ export class AssetController {
@Query(new ValidationPipe({ transform: true })) dto: DownloadDto,
@Response({ passthrough: true }) res: Res,
): Promise<any> {
this.assetService.checkDownloadAccess(authUser);
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadLibrary(authUser, dto);
res.attachment(fileName);
res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize);
@ -143,7 +149,7 @@ export class AssetController {
@Param('assetId') assetId: string,
): Promise<any> {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return this.assetService.serveFile(assetId, query, res, headers);
return this.assetService.serveFile(authUser, assetId, query, res, headers);
}
@Authenticated({ isShared: true })
@ -246,7 +252,7 @@ export class AssetController {
@Param('assetId') assetId: string,
): Promise<AssetResponseDto> {
await this.assetService.checkAssetsAccess(authUser, [assetId]);
return await this.assetService.getAssetById(assetId);
return await this.assetService.getAssetById(authUser, assetId);
}
/**
@ -274,14 +280,14 @@ export class AssetController {
const deleteAssetList: AssetResponseDto[] = [];
for (const id of assetIds.ids) {
const assets = await this.assetService.getAssetById(id);
const assets = await this.assetService.getAssetById(authUser, id);
if (!assets) {
continue;
}
deleteAssetList.push(assets);
if (assets.livePhotoVideoId) {
const livePhotoVideo = await this.assetService.getAssetById(assets.livePhotoVideoId);
const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId);
if (livePhotoVideo) {
deleteAssetList.push(livePhotoVideo);
assetIds.ids = [...assetIds.ids, livePhotoVideo.id];

View File

@ -23,7 +23,7 @@ import { SearchAssetDto } from './dto/search-asset.dto';
import fs from 'fs/promises';
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto';
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from './response-dto/asset-response.dto';
import { CreateAssetDto } from './dto/create-asset.dto';
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
@ -52,7 +52,7 @@ 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 { mapSharedLink, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
const fileInfo = promisify(stat);
@ -215,10 +215,15 @@ export class AssetService {
return assets.map((asset) => mapAsset(asset));
}
public async getAssetById(assetId: string): Promise<AssetResponseDto> {
public async getAssetById(authUser: AuthUserDto, assetId: string): Promise<AssetResponseDto> {
const allowExif = this.getExifPermission(authUser);
const asset = await this._assetRepository.getById(assetId);
if (allowExif) {
return mapAsset(asset);
} else {
return mapAssetWithoutExif(asset);
}
}
public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
@ -356,7 +361,15 @@ export class AssetService {
}
}
public async serveFile(assetId: string, query: ServeFileDto, res: Res, headers: Record<string, string>) {
public async serveFile(
authUser: AuthUserDto,
assetId: string,
query: ServeFileDto,
res: Res,
headers: Record<string, string>,
) {
const allowOriginalFile = !authUser.isPublicUser || authUser.isAllowDownload;
let fileReadStream: ReadStream;
const asset = await this._assetRepository.getById(assetId);
@ -390,7 +403,7 @@ export class AssetService {
/**
* Serve thumbnail image for both web and mobile app
*/
if (!query.isThumb) {
if (!query.isThumb && allowOriginalFile) {
res.set({
'Content-Type': asset.mimeType,
});
@ -676,6 +689,10 @@ export class AssetService {
}
}
checkDownloadAccess(authUser: AuthUserDto) {
this.shareCore.checkDownloadAccess(authUser);
}
async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> {
const assets = [];
@ -691,9 +708,11 @@ export class AssetService {
allowUpload: dto.allowUpload,
assets: assets,
description: dto.description,
allowDownload: dto.allowDownload,
showExif: dto.showExif,
});
return mapSharedLinkToResponseDto(sharedLink);
return mapSharedLink(sharedLink);
}
async updateAssetsInSharedLink(
@ -709,7 +728,11 @@ export class AssetService {
}
const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets);
return mapSharedLinkToResponseDto(updatedLink);
return mapSharedLink(updatedLink);
}
getExifPermission(authUser: AuthUserDto) {
return !authUser.isPublicUser || authUser.isShowExif;
}
}

View File

@ -25,6 +25,14 @@ export class CreateAssetsShareLinkDto {
@IsOptional()
allowUpload?: boolean;
@IsBoolean()
@IsOptional()
allowDownload?: boolean;
@IsBoolean()
@IsOptional()
showExif?: boolean;
@IsString()
@IsOptional()
description?: string;

View File

@ -49,3 +49,26 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
tags: entity.tags?.map(mapTag),
};
}
export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
return {
id: entity.id,
deviceAssetId: entity.deviceAssetId,
ownerId: entity.userId,
deviceId: entity.deviceId,
type: entity.type,
originalPath: entity.originalPath,
resizePath: entity.resizePath,
createdAt: entity.createdAt,
modifiedAt: entity.modifiedAt,
isFavorite: entity.isFavorite,
mimeType: entity.mimeType,
webpPath: entity.webpPath,
encodedVideoPath: entity.encodedVideoPath,
duration: entity.duration ?? '0:00:00.00000',
exifInfo: undefined,
smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
livePhotoVideoId: entity.livePhotoVideoId,
tags: entity.tags?.map(mapTag),
};
}

View File

@ -8,4 +8,6 @@ export class CreateSharedLinkDto {
assets!: AssetEntity[];
album?: AlbumEntity;
allowUpload?: boolean;
allowDownload?: boolean;
showExif?: boolean;
}

View File

@ -10,6 +10,12 @@ export class EditSharedLinkDto {
@IsOptional()
allowUpload?: boolean;
@IsOptional()
allowDownload?: boolean;
@IsOptional()
showExif?: boolean;
@IsNotEmpty()
isEditExpireTime?: boolean;
}

View File

@ -2,7 +2,7 @@ import { SharedLinkEntity, SharedLinkType } from '@app/infra';
import { ApiProperty } from '@nestjs/swagger';
import _ from 'lodash';
import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album/response-dto/album-response.dto';
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '../../asset/response-dto/asset-response.dto';
export class SharedLinkResponseDto {
id!: string;
@ -17,9 +17,11 @@ export class SharedLinkResponseDto {
assets!: AssetResponseDto[];
album?: AlbumResponseDto;
allowUpload!: boolean;
allowDownload!: boolean;
showExif!: boolean;
}
export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
export function mapSharedLink(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
const linkAssets = sharedLink.assets || [];
const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo);
@ -36,5 +38,29 @@ export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): Shared
assets: assets.map(mapAsset),
album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showExif: sharedLink.showExif,
};
}
export function mapSharedLinkWithNoExif(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
const linkAssets = sharedLink.assets || [];
const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo);
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
return {
id: sharedLink.id,
description: sharedLink.description,
userId: sharedLink.userId,
key: sharedLink.key.toString('hex'),
type: sharedLink.type,
createdAt: sharedLink.createdAt,
expiresAt: sharedLink.expiresAt,
assets: assets.map(mapAssetWithoutExif),
album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined,
allowUpload: sharedLink.allowUpload,
allowDownload: sharedLink.allowDownload,
showExif: sharedLink.showExif,
};
}

View File

@ -25,7 +25,7 @@ export class ShareController {
@Authenticated()
@Get(':id')
getSharedLinkById(@Param('id') id: string): Promise<SharedLinkResponseDto> {
return this.shareService.getById(id);
return this.shareService.getById(id, true);
}
@Authenticated()

View File

@ -2,9 +2,10 @@ import { SharedLinkEntity } from '@app/infra';
import { CreateSharedLinkDto } from './dto/create-shared-link.dto';
import { ISharedLinkRepository } from './shared-link.repository';
import crypto from 'node:crypto';
import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common';
import { BadRequestException, ForbiddenException, InternalServerErrorException, Logger } from '@nestjs/common';
import { AssetEntity } from '@app/infra';
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
export class ShareCore {
readonly logger = new Logger(ShareCore.name);
@ -24,6 +25,8 @@ export class ShareCore {
sharedLink.assets = dto.assets;
sharedLink.album = dto.album;
sharedLink.allowUpload = dto.allowUpload ?? false;
sharedLink.allowDownload = dto.allowDownload ?? true;
sharedLink.showExif = dto.showExif ?? true;
return this.sharedLinkRepository.create(sharedLink);
} catch (error: any) {
@ -74,6 +77,8 @@ export class ShareCore {
link.description = dto.description ?? link.description;
link.allowUpload = dto.allowUpload ?? link.allowUpload;
link.allowDownload = dto.allowDownload ?? link.allowDownload;
link.showExif = dto.showExif ?? link.showExif;
if (dto.isEditExpireTime && dto.expiredAt) {
link.expiresAt = dto.expiredAt;
@ -87,4 +92,10 @@ export class ShareCore {
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
return this.sharedLinkRepository.hasAssetAccess(id, assetId);
}
checkDownloadAccess(user: AuthUserDto) {
if (user.isPublicUser && !user.isAllowDownload) {
throw new ForbiddenException();
}
}
}

View File

@ -9,7 +9,7 @@ import {
import { UserService } from '@app/domain';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from './response-dto/shared-link-response.dto';
import { mapSharedLink, mapSharedLinkWithNoExif, SharedLinkResponseDto } from './response-dto/shared-link-response.dto';
import { ShareCore } from './share.core';
import { ISharedLinkRepository } from './shared-link.repository';
@ -39,6 +39,8 @@ export class ShareService {
isPublicUser: true,
sharedLinkId: link.id,
isAllowUpload: link.allowUpload,
isAllowDownload: link.allowDownload,
isShowExif: link.showExif,
};
}
}
@ -48,7 +50,7 @@ export class ShareService {
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
const links = await this.shareCore.getSharedLinks(authUser.id);
return links.map(mapSharedLinkToResponseDto);
return links.map(mapSharedLink);
}
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
@ -56,15 +58,25 @@ export class ShareService {
throw new ForbiddenException();
}
return this.getById(authUser.sharedLinkId);
let allowExif = true;
if (authUser.isShowExif != undefined) {
allowExif = authUser.isShowExif;
}
async getById(id: string): Promise<SharedLinkResponseDto> {
return this.getById(authUser.sharedLinkId, allowExif);
}
async getById(id: string, allowExif: boolean): Promise<SharedLinkResponseDto> {
const link = await this.shareCore.getSharedLinkById(id);
if (!link) {
throw new BadRequestException('Shared link not found');
}
return mapSharedLinkToResponseDto(link);
if (allowExif) {
return mapSharedLink(link);
} else {
return mapSharedLinkWithNoExif(link);
}
}
async remove(id: string, userId: string): Promise<string> {
@ -77,11 +89,11 @@ export class ShareService {
if (!link) {
throw new BadRequestException('Shared link not found');
}
return mapSharedLinkToResponseDto(link);
return mapSharedLink(link);
}
async edit(id: string, authUser: AuthUserDto, dto: EditSharedLinkDto) {
const link = await this.shareCore.updateSharedLink(id, authUser.id, dto);
return mapSharedLinkToResponseDto(link);
return mapSharedLink(link);
}
}

View File

@ -139,7 +139,6 @@ export class MetadataExtractionProcessor {
async extractExifInfo(job: Job<IExifExtractionProcessor>) {
try {
const { asset, fileName }: { asset: AssetEntity; fileName: string } = job.data;
const exifData = await exiftool.read(asset.originalPath).catch((e) => {
this.logger.warn(`The exifData parsing failed due to: ${e} on file ${asset.originalPath}`);
return null;

View File

@ -736,7 +736,7 @@
"/asset/download-library": {
"get": {
"operationId": "downloadLibrary",
"description": "",
"description": "Current this is not used in any UI element",
"parameters": [
{
"name": "skip",
@ -3786,6 +3786,12 @@
"allowUpload": {
"type": "boolean"
},
"allowDownload": {
"type": "boolean"
},
"showExif": {
"type": "boolean"
},
"description": {
"type": "string"
}
@ -3887,6 +3893,12 @@
},
"allowUpload": {
"type": "boolean"
},
"allowDownload": {
"type": "boolean"
},
"showExif": {
"type": "boolean"
}
},
"required": [
@ -3897,7 +3909,9 @@
"createdAt",
"expiresAt",
"assets",
"allowUpload"
"allowUpload",
"allowDownload",
"showExif"
]
},
"UpdateAssetsToSharedLinkDto": {
@ -3926,6 +3940,12 @@
"allowUpload": {
"type": "boolean"
},
"allowDownload": {
"type": "boolean"
},
"showExif": {
"type": "boolean"
},
"isEditExpireTime": {
"type": "boolean"
}
@ -4085,6 +4105,12 @@
"allowUpload": {
"type": "boolean"
},
"allowDownload": {
"type": "boolean"
},
"showExif": {
"type": "boolean"
},
"description": {
"type": "string"
}

View File

@ -5,4 +5,6 @@ export class AuthUserDto {
isPublicUser?: boolean;
sharedLinkId?: string;
isAllowUpload?: boolean;
isAllowDownload?: boolean;
isShowExif?: boolean;
}

View File

@ -30,6 +30,12 @@ export class SharedLinkEntity {
@Column({ type: 'boolean', default: false })
allowUpload!: boolean;
@Column({ type: 'boolean', default: true })
allowDownload!: boolean;
@Column({ type: 'boolean', default: true })
showExif!: boolean;
@ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks)
assets!: AssetEntity[];
@ -47,4 +53,4 @@ export enum SharedLinkType {
INDIVIDUAL = 'INDIVIDUAL',
}
// npm run typeorm -- migration:generate ./libs/database/src/AddSharedLinkTable -d libs/database/src/config/database.config.ts
// npm run typeorm -- migration:generate ./libs/infra/src/db/AddMorePermissionToSharedLink -d ./libs/infra/src/db/config/database.config.ts

View File

@ -0,0 +1,15 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddMorePermissionToSharedLink1673907194740 implements MigrationInterface {
name = 'AddMorePermissionToSharedLink1673907194740';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "shared_links" ADD "allowDownload" boolean NOT NULL DEFAULT true`);
await queryRunner.query(`ALTER TABLE "shared_links" ADD "showExif" boolean NOT NULL DEFAULT true`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "showExif"`);
await queryRunner.query(`ALTER TABLE "shared_links" DROP COLUMN "allowDownload"`);
}
}

View File

@ -665,6 +665,18 @@ export interface CreateAlbumShareLinkDto {
* @memberof CreateAlbumShareLinkDto
*/
'allowUpload'?: boolean;
/**
*
* @type {boolean}
* @memberof CreateAlbumShareLinkDto
*/
'allowDownload'?: boolean;
/**
*
* @type {boolean}
* @memberof CreateAlbumShareLinkDto
*/
'showExif'?: boolean;
/**
*
* @type {string}
@ -696,6 +708,18 @@ export interface CreateAssetsShareLinkDto {
* @memberof CreateAssetsShareLinkDto
*/
'allowUpload'?: boolean;
/**
*
* @type {boolean}
* @memberof CreateAssetsShareLinkDto
*/
'allowDownload'?: boolean;
/**
*
* @type {boolean}
* @memberof CreateAssetsShareLinkDto
*/
'showExif'?: boolean;
/**
*
* @type {string}
@ -987,6 +1011,18 @@ export interface EditSharedLinkDto {
* @memberof EditSharedLinkDto
*/
'allowUpload'?: boolean;
/**
*
* @type {boolean}
* @memberof EditSharedLinkDto
*/
'allowDownload'?: boolean;
/**
*
* @type {boolean}
* @memberof EditSharedLinkDto
*/
'showExif'?: boolean;
/**
*
* @type {boolean}
@ -1612,6 +1648,18 @@ export interface SharedLinkResponseDto {
* @memberof SharedLinkResponseDto
*/
'allowUpload': boolean;
/**
*
* @type {boolean}
* @memberof SharedLinkResponseDto
*/
'allowDownload': boolean;
/**
*
* @type {boolean}
* @memberof SharedLinkResponseDto
*/
'showExif': boolean;
}
/**
*

View File

@ -320,6 +320,7 @@
}
}
} catch (e) {
$downloadAssets = {};
console.error('Error downloading file ', e);
notificationController.show({
type: NotificationType.Error,
@ -460,11 +461,13 @@
<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} />
{/if}
{#if !isPublicShared || (isPublicShared && sharedLink?.allowDownload)}
<CircleIconButton
title="Download"
on:click={() => downloadAlbum()}
logo={FolderDownloadOutline}
/>
{/if}
{#if !isPublicShared}
<CircleIconButton
@ -534,11 +537,7 @@
{/if}
{#if album.assetCount > 0}
<GalleryViewer
assets={album.assets}
key={sharedLink?.key ?? ''}
bind:selectedAssets={multiSelectAsset}
/>
<GalleryViewer assets={album.assets} {sharedLink} 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">

View File

@ -22,6 +22,7 @@
export let showCopyButton: boolean;
export let showMotionPlayButton: boolean;
export let isMotionPhotoPlaying = false;
export let showDownloadButton: boolean;
const isOwner = asset.ownerId === $page.data.user?.id;
@ -77,11 +78,14 @@
}}
/>
{/if}
{#if showDownloadButton}
<CircleIconButton
logo={CloudDownloadOutline}
on:click={() => dispatch('download')}
title="Download"
/>
{/if}
<CircleIconButton
logo={InformationOutline}
on:click={() => dispatch('showDetail')}

View File

@ -10,7 +10,13 @@
import { downloadAssets } from '$lib/stores/download';
import VideoViewer from './video-viewer.svelte';
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api';
import {
api,
AssetResponseDto,
AssetTypeEnum,
AlbumResponseDto,
SharedLinkResponseDto
} from '@api';
import {
notificationController,
NotificationType
@ -22,6 +28,7 @@
export let asset: AssetResponseDto;
export let publicSharedKey = '';
export let showNavigation = true;
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
const dispatch = createEventDispatcher();
let halfLeftHover = false;
@ -31,6 +38,7 @@
let isShowAlbumPicker = false;
let addToSharedAlbum = true;
let shouldPlayMotionPhoto = false;
let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : true;
const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo.key);
onMount(async () => {
@ -166,6 +174,7 @@
}, 2000);
}
} catch (e) {
$downloadAssets = {};
console.error('Error downloading file ', e);
notificationController.show({
type: NotificationType.Error,
@ -247,6 +256,7 @@
isMotionPhotoPlaying={shouldPlayMotionPhoto}
showCopyButton={asset.type === AssetTypeEnum.Image}
showMotionPlayButton={!!asset.livePhotoVideoId}
showDownloadButton={shouldShowDownloadButton}
on:goBack={closeViewer}
on:showDetail={showDetailInfoHandler}
on:download={handleDownload}

View File

@ -136,15 +136,17 @@
/>
{/if}
{#if sharedLink?.allowDownload}
<CircleIconButton
title="Download"
on:click={() => downloadAssets(true)}
logo={FolderDownloadOutline}
/>
{/if}
</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 />
<GalleryViewer {assets} {sharedLink} bind:selectedAssets />
</section>
</section>

View File

@ -36,7 +36,7 @@
<div
use:clickOutside
on:outclick={() => dispatch('close')}
class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[500px] rounded-lg shadow-md"
class="bg-immich-bg dark:bg-immich-dark-gray dark:text-immich-dark-fg w-[450px] min-h-[200px] max-h-[600px] rounded-lg shadow-md"
>
<div class="flex justify-between place-items-center px-5 py-3">
<div>

View File

@ -29,6 +29,8 @@
let sharedLink = '';
let description = '';
let shouldChangeExpirationTime = false;
let isAllowDownload = true;
let shouldShowExif = true;
const dispatch = createEventDispatcher();
const expiredDateOption: ImmichDropDownOption = {
@ -42,6 +44,8 @@
description = editingLink.description;
}
isAllowUpload = editingLink.allowUpload;
isAllowDownload = editingLink.allowDownload;
shouldShowExif = editingLink.showExif;
}
});
@ -58,7 +62,9 @@
albumId: album.id,
expiredAt: expirationDate,
allowUpload: isAllowUpload,
description: description
description: description,
allowDownload: isAllowDownload,
showExif: shouldShowExif
});
buildSharedLink(data);
} else {
@ -66,7 +72,9 @@
assetIds: sharedAssets.map((a) => a.id),
expiredAt: expirationDate,
allowUpload: isAllowUpload,
description: description
description: description,
allowDownload: isAllowDownload,
showExif: shouldShowExif
});
buildSharedLink(data);
}
@ -132,7 +140,9 @@
description: description,
expiredAt: expirationDate,
allowUpload: isAllowUpload,
isEditExpireTime: shouldChangeExpirationTime
isEditExpireTime: shouldChangeExpirationTime,
allowDownload: isAllowDownload,
showExif: shouldShowExif
});
notificationController.show({
@ -185,12 +195,12 @@
{/if}
{/if}
<div class="mt-6 mb-2">
<div class="mt-4 mb-2">
<p class="text-xs">LINK OPTIONS</p>
</div>
<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg">
<div class="flex flex-col">
<div class="mb-4">
<div class="mb-2">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="Description"
@ -198,9 +208,19 @@
/>
</div>
<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} />
<div class="my-3">
<SettingSwitch bind:checked={shouldShowExif} title={'Show metadata'} />
</div>
<div class="text-sm mt-4">
<div class="my-3">
<SettingSwitch bind:checked={isAllowDownload} title={'Allow public user to download'} />
</div>
<div class="my-3">
<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} />
</div>
<div class="text-sm">
{#if editingLink}
<p class="my-2 immich-form-label">
<SettingSwitch

View File

@ -1,13 +1,13 @@
<script lang="ts">
import { page } from '$app/stores';
import { handleError } from '$lib/utils/handle-error';
import { AssetResponseDto, ThumbnailFormat } from '@api';
import { AssetResponseDto, SharedLinkResponseDto, 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 sharedLink: SharedLinkResponseDto | undefined = undefined;
export let selectedAssets: Set<AssetResponseDto> = new Set();
let isShowAssetViewer = false;
@ -96,7 +96,7 @@
<ImmichThumbnail
{asset}
{thumbnailSize}
publicSharedKey={key}
publicSharedKey={sharedLink?.key}
format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
on:select={selectAssetHandler}
@ -110,7 +110,8 @@
{#if isShowAssetViewer}
<AssetViewer
asset={selectedAsset}
publicSharedKey={key}
publicSharedKey={sharedLink?.key}
{sharedLink}
on:navigate-previous={navigateAssetBackward}
on:navigate-next={navigateAssetForward}
on:close={closeViewer}

View File

@ -122,12 +122,28 @@
</div>
</div>
<div class="info-bottom">
<div class="info-bottom flex gap-4">
{#if link.allowUpload}
<div
class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[80px]"
>
Upload
</div>
{/if}
{#if link.allowDownload}
<div
class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[100px]"
>
Allow upload
Download
</div>
{/if}
{#if link.showExif}
<div
class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[60px]"
>
EXIF
</div>
{/if}
</div>