mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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;
 | 
			
		||||
}
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
		Reference in New Issue
	
	Block a user