mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	[WEB] Select album thumbnail (#383)
* Added context menu for album opionts * choose asset for album thumbnail * Refactor UpdateAlbumDto to accept albumThumbnailAssetId * implemented changing album cover on web * Fixed api change on mobile app
This commit is contained in:
		
							parent
							
								
									6dbca8d478
								
							
						
					
					
						commit
						ef4136d327
					
				@ -134,7 +134,6 @@ class SharedAlbumService {
 | 
				
			|||||||
      await _apiService.albumApi.updateAlbumInfo(
 | 
					      await _apiService.albumApi.updateAlbumInfo(
 | 
				
			||||||
        albumId,
 | 
					        albumId,
 | 
				
			||||||
        UpdateAlbumDto(
 | 
					        UpdateAlbumDto(
 | 
				
			||||||
          ownerId: ownerId,
 | 
					 | 
				
			||||||
          albumName: newAlbumTitle,
 | 
					          albumName: newAlbumTitle,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,8 @@ import 'package:openapi/api.dart';
 | 
				
			|||||||
## Properties
 | 
					## Properties
 | 
				
			||||||
Name | Type | Description | Notes
 | 
					Name | Type | Description | Notes
 | 
				
			||||||
------------ | ------------- | ------------- | -------------
 | 
					------------ | ------------- | ------------- | -------------
 | 
				
			||||||
**albumName** | **String** |  | 
 | 
					**albumName** | **String** |  | [optional] 
 | 
				
			||||||
**ownerId** | **String** |  | 
 | 
					**albumThumbnailAssetId** | **String** |  | [optional] 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
					[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -13,32 +13,52 @@ part of openapi.api;
 | 
				
			|||||||
class UpdateAlbumDto {
 | 
					class UpdateAlbumDto {
 | 
				
			||||||
  /// Returns a new [UpdateAlbumDto] instance.
 | 
					  /// Returns a new [UpdateAlbumDto] instance.
 | 
				
			||||||
  UpdateAlbumDto({
 | 
					  UpdateAlbumDto({
 | 
				
			||||||
    required this.albumName,
 | 
					    this.albumName,
 | 
				
			||||||
    required this.ownerId,
 | 
					    this.albumThumbnailAssetId,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String albumName;
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Please note: This property should have been non-nullable! Since the specification file
 | 
				
			||||||
 | 
					  /// does not include a default value (using the "default:" property), however, the generated
 | 
				
			||||||
 | 
					  /// source code must fall back to having a nullable type.
 | 
				
			||||||
 | 
					  /// Consider adding a "default:" property in the specification file to hide this note.
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  String? albumName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String ownerId;
 | 
					  ///
 | 
				
			||||||
 | 
					  /// Please note: This property should have been non-nullable! Since the specification file
 | 
				
			||||||
 | 
					  /// does not include a default value (using the "default:" property), however, the generated
 | 
				
			||||||
 | 
					  /// source code must fall back to having a nullable type.
 | 
				
			||||||
 | 
					  /// Consider adding a "default:" property in the specification file to hide this note.
 | 
				
			||||||
 | 
					  ///
 | 
				
			||||||
 | 
					  String? albumThumbnailAssetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
 | 
					  bool operator ==(Object other) => identical(this, other) || other is UpdateAlbumDto &&
 | 
				
			||||||
     other.albumName == albumName &&
 | 
					     other.albumName == albumName &&
 | 
				
			||||||
     other.ownerId == ownerId;
 | 
					     other.albumThumbnailAssetId == albumThumbnailAssetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  int get hashCode =>
 | 
					  int get hashCode =>
 | 
				
			||||||
    // ignore: unnecessary_parenthesis
 | 
					    // ignore: unnecessary_parenthesis
 | 
				
			||||||
    (albumName.hashCode) +
 | 
					    (albumName == null ? 0 : albumName!.hashCode) +
 | 
				
			||||||
    (ownerId.hashCode);
 | 
					    (albumThumbnailAssetId == null ? 0 : albumThumbnailAssetId!.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'UpdateAlbumDto[albumName=$albumName, ownerId=$ownerId]';
 | 
					  String toString() => 'UpdateAlbumDto[albumName=$albumName, albumThumbnailAssetId=$albumThumbnailAssetId]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final _json = <String, dynamic>{};
 | 
					    final _json = <String, dynamic>{};
 | 
				
			||||||
 | 
					    if (albumName != null) {
 | 
				
			||||||
      _json[r'albumName'] = albumName;
 | 
					      _json[r'albumName'] = albumName;
 | 
				
			||||||
      _json[r'ownerId'] = ownerId;
 | 
					    } else {
 | 
				
			||||||
 | 
					      _json[r'albumName'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (albumThumbnailAssetId != null) {
 | 
				
			||||||
 | 
					      _json[r'albumThumbnailAssetId'] = albumThumbnailAssetId;
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      _json[r'albumThumbnailAssetId'] = null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return _json;
 | 
					    return _json;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -61,8 +81,8 @@ class UpdateAlbumDto {
 | 
				
			|||||||
      }());
 | 
					      }());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return UpdateAlbumDto(
 | 
					      return UpdateAlbumDto(
 | 
				
			||||||
        albumName: mapValueOfType<String>(json, r'albumName')!,
 | 
					        albumName: mapValueOfType<String>(json, r'albumName'),
 | 
				
			||||||
        ownerId: mapValueOfType<String>(json, r'ownerId')!,
 | 
					        albumThumbnailAssetId: mapValueOfType<String>(json, r'albumThumbnailAssetId'),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
@ -112,8 +132,6 @@ class UpdateAlbumDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  /// The list of required keys that must be present in a JSON.
 | 
					  /// The list of required keys that must be present in a JSON.
 | 
				
			||||||
  static const requiredKeys = <String>{
 | 
					  static const requiredKeys = <String>{
 | 
				
			||||||
    'albumName',
 | 
					 | 
				
			||||||
    'ownerId',
 | 
					 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -237,7 +237,8 @@ export class AlbumRepository implements IAlbumRepository {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> {
 | 
					  updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity> {
 | 
				
			||||||
    album.albumName = updateAlbumDto.albumName;
 | 
					    album.albumName = updateAlbumDto.albumName || album.albumName;
 | 
				
			||||||
 | 
					    album.albumThumbnailAssetId = updateAlbumDto.albumThumbnailAssetId || album.albumThumbnailAssetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this.albumRepository.save(album);
 | 
					    return this.albumRepository.save(album);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -104,6 +104,6 @@ export class AlbumController {
 | 
				
			|||||||
    @Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto,
 | 
					    @Body(ValidationPipe) updateAlbumInfoDto: UpdateAlbumDto,
 | 
				
			||||||
    @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
 | 
					    @Param('albumId', new ParseUUIDPipe({ version: '4' })) albumId: string,
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    return this.albumService.updateAlbumTitle(authUser, updateAlbumInfoDto, albumId);
 | 
					    return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -260,17 +260,16 @@ describe('Album service', () => {
 | 
				
			|||||||
    const albumEntity = _getOwnedAlbum();
 | 
					    const albumEntity = _getOwnedAlbum();
 | 
				
			||||||
    const albumId = albumEntity.id;
 | 
					    const albumId = albumEntity.id;
 | 
				
			||||||
    const updatedAlbumName = 'new album name';
 | 
					    const updatedAlbumName = 'new album name';
 | 
				
			||||||
 | 
					    const updatedAlbumThumbnailAssetId = '69d2f917-0b31-48d8-9d7d-673b523f1aac';
 | 
				
			||||||
    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
					    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
    albumRepositoryMock.updateAlbum.mockImplementation(() =>
 | 
					    albumRepositoryMock.updateAlbum.mockImplementation(() =>
 | 
				
			||||||
      Promise.resolve<AlbumEntity>({ ...albumEntity, albumName: updatedAlbumName }),
 | 
					      Promise.resolve<AlbumEntity>({ ...albumEntity, albumName: updatedAlbumName }),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const result = await sut.updateAlbumTitle(
 | 
					    const result = await sut.updateAlbumInfo(
 | 
				
			||||||
      authUser,
 | 
					      authUser,
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        albumName: updatedAlbumName,
 | 
					        albumName: updatedAlbumName,
 | 
				
			||||||
        ownerId: 'this is not used and will be removed',
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      albumId,
 | 
					      albumId,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
@ -280,7 +279,7 @@ describe('Album service', () => {
 | 
				
			|||||||
    expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
 | 
					    expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledTimes(1);
 | 
				
			||||||
    expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
 | 
					    expect(albumRepositoryMock.updateAlbum).toHaveBeenCalledWith(albumEntity, {
 | 
				
			||||||
      albumName: updatedAlbumName,
 | 
					      albumName: updatedAlbumName,
 | 
				
			||||||
      ownerId: 'this is not used and will be removed',
 | 
					      thumbnailAssetId: updatedAlbumThumbnailAssetId,
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -291,11 +290,11 @@ describe('Album service', () => {
 | 
				
			|||||||
    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
					    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await expect(
 | 
					    await expect(
 | 
				
			||||||
      sut.updateAlbumTitle(
 | 
					      sut.updateAlbumInfo(
 | 
				
			||||||
        authUser,
 | 
					        authUser,
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          albumName: 'new album name',
 | 
					          albumName: 'new album name',
 | 
				
			||||||
          ownerId: 'this is not used and will be removed',
 | 
					          albumThumbnailAssetId: '69d2f917-0b31-48d8-9d7d-673b523f1aac',
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        albumId,
 | 
					        albumId,
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
@ -361,7 +360,7 @@ describe('Album service', () => {
 | 
				
			|||||||
  it('removes assets from owned album', async () => {
 | 
					  it('removes assets from owned album', async () => {
 | 
				
			||||||
    const albumEntity = _getOwnedAlbum();
 | 
					    const albumEntity = _getOwnedAlbum();
 | 
				
			||||||
    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
					    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
    albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve(true));
 | 
					    albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await expect(
 | 
					    await expect(
 | 
				
			||||||
      sut.removeAssetsFromAlbum(
 | 
					      sut.removeAssetsFromAlbum(
 | 
				
			||||||
@ -381,7 +380,7 @@ describe('Album service', () => {
 | 
				
			|||||||
  it('removes assets from shared album (shared with auth user)', async () => {
 | 
					  it('removes assets from shared album (shared with auth user)', async () => {
 | 
				
			||||||
    const albumEntity = _getOwnedSharedAlbum();
 | 
					    const albumEntity = _getOwnedSharedAlbum();
 | 
				
			||||||
    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
					    albumRepositoryMock.get.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
    albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve(true));
 | 
					    albumRepositoryMock.removeAssets.mockImplementation(() => Promise.resolve<AlbumEntity>(albumEntity));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await expect(
 | 
					    await expect(
 | 
				
			||||||
      sut.removeAssetsFromAlbum(
 | 
					      sut.removeAssetsFromAlbum(
 | 
				
			||||||
 | 
				
			|||||||
@ -103,16 +103,17 @@ export class AlbumService {
 | 
				
			|||||||
    return mapAlbum(updatedAlbum);
 | 
					    return mapAlbum(updatedAlbum);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async updateAlbumTitle(
 | 
					  async updateAlbumInfo(
 | 
				
			||||||
    authUser: AuthUserDto,
 | 
					    authUser: AuthUserDto,
 | 
				
			||||||
    updateAlbumDto: UpdateAlbumDto,
 | 
					    updateAlbumDto: UpdateAlbumDto,
 | 
				
			||||||
    albumId: string,
 | 
					    albumId: string,
 | 
				
			||||||
  ): Promise<AlbumResponseDto> {
 | 
					  ): Promise<AlbumResponseDto> {
 | 
				
			||||||
    // TODO: this should not come from request DTO. To be removed from here and DTO
 | 
					 | 
				
			||||||
    // if (authUser.id != updateAlbumDto.ownerId) {
 | 
					 | 
				
			||||||
    //   throw new BadRequestException('Unauthorized to change album info');
 | 
					 | 
				
			||||||
    // }
 | 
					 | 
				
			||||||
    const album = await this._getAlbum({ authUser, albumId });
 | 
					    const album = await this._getAlbum({ authUser, albumId });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (authUser.id != album.ownerId) {
 | 
				
			||||||
 | 
					      throw new BadRequestException('Unauthorized to change album info');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
 | 
					    const updatedAlbum = await this._albumRepository.updateAlbum(album, updateAlbumDto);
 | 
				
			||||||
    return mapAlbum(updatedAlbum);
 | 
					    return mapAlbum(updatedAlbum);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,9 @@
 | 
				
			|||||||
import { IsNotEmpty } from 'class-validator';
 | 
					import { IsNotEmpty, IsOptional } from 'class-validator';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class UpdateAlbumDto {
 | 
					export class UpdateAlbumDto {
 | 
				
			||||||
  @IsNotEmpty()
 | 
					  @IsOptional()
 | 
				
			||||||
  albumName!: string;
 | 
					  albumName?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @IsNotEmpty()
 | 
					  @IsOptional()
 | 
				
			||||||
  ownerId!: string;
 | 
					  albumThumbnailAssetId?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@ -1001,13 +1001,13 @@ export interface UpdateAlbumDto {
 | 
				
			|||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     * @memberof UpdateAlbumDto
 | 
					     * @memberof UpdateAlbumDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'albumName': string;
 | 
					    'albumName'?: string;
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
     * @memberof UpdateAlbumDto
 | 
					     * @memberof UpdateAlbumDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'ownerId': string;
 | 
					    'albumThumbnailAssetId'?: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * 
 | 
					 * 
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,11 @@
 | 
				
			|||||||
	import CircleIconButton from '../shared-components/circle-icon-button.svelte';
 | 
						import CircleIconButton from '../shared-components/circle-icon-button.svelte';
 | 
				
			||||||
	import Close from 'svelte-material-icons/Close.svelte';
 | 
						import Close from 'svelte-material-icons/Close.svelte';
 | 
				
			||||||
	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 | 
						import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 | 
				
			||||||
 | 
						import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
				
			||||||
 | 
						import ContextMenu from '../shared-components/context-menu/context-menu.svelte';
 | 
				
			||||||
 | 
						import MenuOption from '../shared-components/context-menu/menu-option.svelte';
 | 
				
			||||||
 | 
						import ThumbnailSelection from './thumbnail-selection.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	export let album: AlbumResponseDto;
 | 
						export let album: AlbumResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let isShowAssetViewer = false;
 | 
						let isShowAssetViewer = false;
 | 
				
			||||||
@ -26,6 +31,8 @@
 | 
				
			|||||||
	let isEditingTitle = false;
 | 
						let isEditingTitle = false;
 | 
				
			||||||
	let isCreatingSharedAlbum = false;
 | 
						let isCreatingSharedAlbum = false;
 | 
				
			||||||
	let isShowShareInfoModal = false;
 | 
						let isShowShareInfoModal = false;
 | 
				
			||||||
 | 
						let isShowAlbumOptions = false;
 | 
				
			||||||
 | 
						let isShowThumbnailSelection = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let selectedAsset: AssetResponseDto;
 | 
						let selectedAsset: AssetResponseDto;
 | 
				
			||||||
	let currentViewAssetIndex = 0;
 | 
						let currentViewAssetIndex = 0;
 | 
				
			||||||
@ -37,6 +44,7 @@
 | 
				
			|||||||
	let currentAlbumName = '';
 | 
						let currentAlbumName = '';
 | 
				
			||||||
	let currentUser: UserResponseDto;
 | 
						let currentUser: UserResponseDto;
 | 
				
			||||||
	let titleInput: HTMLInputElement;
 | 
						let titleInput: HTMLInputElement;
 | 
				
			||||||
 | 
						let contextMenuPosition = { x: 0, y: 0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	$: isOwned = currentUser?.id == album.ownerId;
 | 
						$: isOwned = currentUser?.id == album.ownerId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -165,7 +173,6 @@
 | 
				
			|||||||
		if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) {
 | 
							if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) {
 | 
				
			||||||
			api.albumApi
 | 
								api.albumApi
 | 
				
			||||||
				.updateAlbumInfo(album.id, {
 | 
									.updateAlbumInfo(album.id, {
 | 
				
			||||||
					ownerId: album.ownerId,
 | 
					 | 
				
			||||||
					albumName: album.albumName
 | 
										albumName: album.albumName
 | 
				
			||||||
				})
 | 
									})
 | 
				
			||||||
				.then(() => {
 | 
									.then(() => {
 | 
				
			||||||
@ -238,6 +245,28 @@
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const showAlbumOptionsMenu = (event: CustomEvent) => {
 | 
				
			||||||
 | 
							contextMenuPosition = {
 | 
				
			||||||
 | 
								x: event.detail.mouseEvent.x,
 | 
				
			||||||
 | 
								y: event.detail.mouseEvent.y
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isShowAlbumOptions = !isShowAlbumOptions;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const setAlbumThumbnailHandler = (event: CustomEvent) => {
 | 
				
			||||||
 | 
							const { asset }: { asset: AssetResponseDto } = event.detail;
 | 
				
			||||||
 | 
							try {
 | 
				
			||||||
 | 
								api.albumApi.updateAlbumInfo(album.id, {
 | 
				
			||||||
 | 
									albumThumbnailAssetId: asset.id
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							} catch (e) {
 | 
				
			||||||
 | 
								console.log('Error [setAlbumThumbnailHandler] ', e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							isShowThumbnailSelection = false;
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<section class="bg-immich-bg">
 | 
					<section class="bg-immich-bg">
 | 
				
			||||||
@ -274,7 +303,7 @@
 | 
				
			|||||||
						logo={FileImagePlusOutline}
 | 
											logo={FileImagePlusOutline}
 | 
				
			||||||
					/>
 | 
										/>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					<!-- Sharing only for owner -->
 | 
										<!-- Share and remove album -->
 | 
				
			||||||
					{#if isOwned}
 | 
										{#if isOwned}
 | 
				
			||||||
						<CircleIconButton
 | 
											<CircleIconButton
 | 
				
			||||||
							title="Share"
 | 
												title="Share"
 | 
				
			||||||
@ -283,6 +312,12 @@
 | 
				
			|||||||
						/>
 | 
											/>
 | 
				
			||||||
						<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} />
 | 
											<CircleIconButton title="Remove album" on:click={removeAlbum} logo={DeleteOutline} />
 | 
				
			||||||
					{/if}
 | 
										{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
										<CircleIconButton
 | 
				
			||||||
 | 
											title="Album options"
 | 
				
			||||||
 | 
											on:click={(event) => showAlbumOptionsMenu(event)}
 | 
				
			||||||
 | 
											logo={DotsVertical}
 | 
				
			||||||
 | 
										/>
 | 
				
			||||||
				{/if}
 | 
									{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
 | 
									{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
 | 
				
			||||||
@ -418,3 +453,25 @@
 | 
				
			|||||||
		on:user-deleted={sharedUserDeletedHandler}
 | 
							on:user-deleted={sharedUserDeletedHandler}
 | 
				
			||||||
	/>
 | 
						/>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if isShowAlbumOptions}
 | 
				
			||||||
 | 
						<ContextMenu {...contextMenuPosition} on:clickoutside={() => (isShowAlbumOptions = false)}>
 | 
				
			||||||
 | 
							{#if isOwned}
 | 
				
			||||||
 | 
								<MenuOption
 | 
				
			||||||
 | 
									on:click={() => {
 | 
				
			||||||
 | 
										isShowThumbnailSelection = true;
 | 
				
			||||||
 | 
										isShowAlbumOptions = false;
 | 
				
			||||||
 | 
									}}
 | 
				
			||||||
 | 
									text="Set album cover"
 | 
				
			||||||
 | 
								/>
 | 
				
			||||||
 | 
							{/if}
 | 
				
			||||||
 | 
						</ContextMenu>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#if isShowThumbnailSelection}
 | 
				
			||||||
 | 
						<ThumbnailSelection
 | 
				
			||||||
 | 
							{album}
 | 
				
			||||||
 | 
							on:close={() => (isShowThumbnailSelection = false)}
 | 
				
			||||||
 | 
							on:thumbnail-selected={setAlbumThumbnailHandler}
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					{/if}
 | 
				
			||||||
 | 
				
			|||||||
@ -170,7 +170,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<section
 | 
					<section
 | 
				
			||||||
	transition:fly={{ y: 500, duration: 100, easing: quintOut }}
 | 
						transition:fly={{ y: 500, duration: 100, easing: quintOut }}
 | 
				
			||||||
	class="absolute top-0 left-0 w-full h-full  bg-immich-bg z-[9999]"
 | 
						class="absolute top-0 left-0 w-full h-full py-[160px]  bg-immich-bg z-[9999]"
 | 
				
			||||||
>
 | 
					>
 | 
				
			||||||
	<AlbumAppBar on:close-button-click={() => dispatch('go-back')}>
 | 
						<AlbumAppBar on:close-button-click={() => dispatch('go-back')}>
 | 
				
			||||||
		<svelte:fragment slot="leading">
 | 
							<svelte:fragment slot="leading">
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										54
									
								
								web/src/lib/components/album-page/thumbnail-selection.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								web/src/lib/components/album-page/thumbnail-selection.svelte
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import { AlbumResponseDto, AssetResponseDto } from '@api';
 | 
				
			||||||
 | 
						import { createEventDispatcher } from 'svelte';
 | 
				
			||||||
 | 
						import { quintOut } from 'svelte/easing';
 | 
				
			||||||
 | 
						import { fly } from 'svelte/transition';
 | 
				
			||||||
 | 
						import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
 | 
				
			||||||
 | 
						import AlbumAppBar from './album-app-bar.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						export let album: AlbumResponseDto;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let selectedThumbnail: AssetResponseDto | undefined;
 | 
				
			||||||
 | 
						const dispatch = createEventDispatcher();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						$: isSelected = (id: string): boolean | undefined => {
 | 
				
			||||||
 | 
							if (!selectedThumbnail && album.albumThumbnailAssetId == id) {
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								return selectedThumbnail?.id == id;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section
 | 
				
			||||||
 | 
						transition:fly={{ y: 500, duration: 100, easing: quintOut }}
 | 
				
			||||||
 | 
						class="absolute top-0 left-0 w-full h-full py-[160px]  bg-immich-bg z-[9999]"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<AlbumAppBar on:close-button-click={() => dispatch('close')}>
 | 
				
			||||||
 | 
							<svelte:fragment slot="leading">
 | 
				
			||||||
 | 
								<p class="text-lg">Select album cover</p>
 | 
				
			||||||
 | 
							</svelte:fragment>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<svelte:fragment slot="trailing">
 | 
				
			||||||
 | 
								<button
 | 
				
			||||||
 | 
									disabled={selectedThumbnail == undefined}
 | 
				
			||||||
 | 
									on:click={() => dispatch('thumbnail-selected', { asset: selectedThumbnail })}
 | 
				
			||||||
 | 
									class="immich-text-button border bg-immich-primary text-gray-50 hover:bg-immich-primary/75 px-6 text-sm disabled:opacity-25 disabled:bg-gray-500 disabled:cursor-not-allowed"
 | 
				
			||||||
 | 
									><span class="px-2">Done</span></button
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
							</svelte:fragment>
 | 
				
			||||||
 | 
						</AlbumAppBar>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<section class="flex flex-wrap gap-14  px-20 overflow-y-auto">
 | 
				
			||||||
 | 
							<!-- Image grid -->
 | 
				
			||||||
 | 
							<div class="flex flex-wrap gap-[2px]">
 | 
				
			||||||
 | 
								{#each album.assets as asset}
 | 
				
			||||||
 | 
									<ImmichThumbnail
 | 
				
			||||||
 | 
										{asset}
 | 
				
			||||||
 | 
										on:click={() => (selectedThumbnail = asset)}
 | 
				
			||||||
 | 
										selected={isSelected(asset.id)}
 | 
				
			||||||
 | 
									/>
 | 
				
			||||||
 | 
								{/each}
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</section>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
@ -45,6 +45,6 @@
 | 
				
			|||||||
	<title>{album.albumName} - Immich</title>
 | 
						<title>{album.albumName} - Immich</title>
 | 
				
			||||||
</svelte:head>
 | 
					</svelte:head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<div class="relative immich-scrollbar">
 | 
					<div class="immich-scrollbar">
 | 
				
			||||||
	<AlbumViewer {album} />
 | 
						<AlbumViewer {album} />
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user