You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-09 23:17:29 +02:00
Add ablum feature to web (#352)
* Added album page * Refactor sidebar * Added album assets count info * Added album viewer page * Refactor album sorting * Fixed incorrectly showing selected asset in album selection * Improve fetching speed with prefetch * Refactor to use ImmichThubmnail component for all * Update to the latest version of Svelte * Implement fixed app bar in album viewer * Added shared user avatar * Correctly get all owned albums, including shared
This commit is contained in:
@@ -84,7 +84,7 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
});
|
||||
}
|
||||
|
||||
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
||||
async getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]> {
|
||||
const filteringByShared = typeof getAlbumsDto.shared == 'boolean';
|
||||
const userId = ownerId;
|
||||
let query = this.albumRepository.createQueryBuilder('album');
|
||||
@@ -132,35 +132,44 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
query = query
|
||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||
.where('album.ownerId = :ownerId', { ownerId: userId })
|
||||
.orWhere((qb) => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('userAlbum.albumId')
|
||||
.from(UserAlbumEntity, 'userAlbum')
|
||||
.where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
|
||||
.getQuery();
|
||||
return `album.id IN ${subQuery}`;
|
||||
});
|
||||
.where('album.ownerId = :ownerId', { ownerId: userId });
|
||||
// .orWhere((qb) => {
|
||||
// const subQuery = qb
|
||||
// .subQuery()
|
||||
// .select('userAlbum.albumId')
|
||||
// .from(UserAlbumEntity, 'userAlbum')
|
||||
// .where('userAlbum.sharedUserId = :sharedUserId', { sharedUserId: userId })
|
||||
// .getQuery();
|
||||
// return `album.id IN ${subQuery}`;
|
||||
// });
|
||||
}
|
||||
return query.orderBy('album.createdAt', 'DESC').getMany();
|
||||
// Get information of assets in albums
|
||||
query = query
|
||||
.leftJoinAndSelect('album.assets', 'assets')
|
||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
|
||||
const albums = await query.getMany();
|
||||
|
||||
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
||||
|
||||
return albums;
|
||||
}
|
||||
|
||||
async get(albumId: string): Promise<AlbumEntity | undefined> {
|
||||
const album = await this.albumRepository.findOne({
|
||||
where: { id: albumId },
|
||||
relations: ['sharedUsers', 'sharedUsers.userInfo', 'assets', 'assets.assetInfo'],
|
||||
});
|
||||
let query = this.albumRepository.createQueryBuilder('album');
|
||||
|
||||
const album = await query
|
||||
.where('album.id = :albumId', { albumId })
|
||||
.leftJoinAndSelect('album.sharedUsers', 'sharedUser')
|
||||
.leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
|
||||
.leftJoinAndSelect('album.assets', 'assets')
|
||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
|
||||
.getOne();
|
||||
|
||||
if (!album) {
|
||||
return;
|
||||
}
|
||||
// TODO: sort in query
|
||||
const sortedSharedAsset = album.assets?.sort(
|
||||
(a, b) => new Date(a.assetInfo.createdAt).valueOf() - new Date(b.assetInfo.createdAt).valueOf(),
|
||||
);
|
||||
|
||||
album.assets = sortedSharedAsset;
|
||||
|
||||
return album;
|
||||
}
|
||||
|
@@ -43,6 +43,7 @@ import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@@ -109,8 +110,11 @@ export class AssetController {
|
||||
}
|
||||
|
||||
@Get('/thumbnail/:assetId')
|
||||
async getAssetThumbnail(@Param('assetId') assetId: string): Promise<any> {
|
||||
return this.assetService.getAssetThumbnail(assetId);
|
||||
async getAssetThumbnail(
|
||||
@Param('assetId') assetId: string,
|
||||
@Query(new ValidationPipe({ transform: true })) query: GetAssetThumbnailDto,
|
||||
): Promise<any> {
|
||||
return this.assetService.getAssetThumbnail(assetId, query);
|
||||
}
|
||||
|
||||
@Get('/allObjects')
|
||||
|
@@ -23,6 +23,7 @@ import { AssetResponseDto, mapAsset } from './response-dto/asset-response.dto';
|
||||
import { AssetFileUploadDto } from './dto/asset-file-upload.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';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@@ -187,7 +188,7 @@ export class AssetService {
|
||||
}
|
||||
}
|
||||
|
||||
public async getAssetThumbnail(assetId: string) {
|
||||
public async getAssetThumbnail(assetId: string, query: GetAssetThumbnailDto) {
|
||||
let fileReadStream: ReadStream;
|
||||
|
||||
const asset = await this.assetRepository.findOne({ where: { id: assetId } });
|
||||
@@ -197,16 +198,25 @@ export class AssetService {
|
||||
}
|
||||
|
||||
try {
|
||||
if (asset.webpPath && asset.webpPath.length > 0) {
|
||||
await fs.access(asset.webpPath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.webpPath);
|
||||
} else {
|
||||
if (query.format == GetAssetThumbnailFormatEnum.JPEG) {
|
||||
if (!asset.resizePath) {
|
||||
throw new NotFoundException('resizePath not set');
|
||||
}
|
||||
|
||||
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.resizePath);
|
||||
} else {
|
||||
if (asset.webpPath && asset.webpPath.length > 0) {
|
||||
await fs.access(asset.webpPath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.webpPath);
|
||||
} else {
|
||||
if (!asset.resizePath) {
|
||||
throw new NotFoundException('resizePath not set');
|
||||
}
|
||||
|
||||
await fs.access(asset.resizePath, constants.R_OK | constants.W_OK);
|
||||
fileReadStream = createReadStream(asset.resizePath);
|
||||
}
|
||||
}
|
||||
|
||||
return new StreamableFile(fileReadStream);
|
||||
|
@@ -0,0 +1,19 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
|
||||
|
||||
export enum GetAssetThumbnailFormatEnum {
|
||||
JPEG = 'JPEG',
|
||||
WEBP = 'WEBP',
|
||||
}
|
||||
|
||||
export class GetAssetThumbnailDto {
|
||||
@IsOptional()
|
||||
@ApiProperty({
|
||||
enum: GetAssetThumbnailFormatEnum,
|
||||
default: GetAssetThumbnailFormatEnum.WEBP,
|
||||
required: false,
|
||||
enumName: 'ThumbnailFormat',
|
||||
})
|
||||
format = GetAssetThumbnailFormatEnum.WEBP;
|
||||
}
|
Reference in New Issue
Block a user