mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	Show all albums an asset appears in on the asset viewer page (#575)
* Add route to query albums for a specific asset * Update API and add to detail-panel * Fix tests * Refactor API endpoint * Added alt attribute to img tag Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		@@ -259,7 +259,7 @@ Name | Type | Description  | Notes
 | 
			
		||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 | 
			
		||||
 | 
			
		||||
# **getAllAlbums**
 | 
			
		||||
> List<AlbumResponseDto> getAllAlbums(shared)
 | 
			
		||||
> List<AlbumResponseDto> getAllAlbums(shared, assetId)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -275,9 +275,10 @@ import 'package:openapi/api.dart';
 | 
			
		||||
 | 
			
		||||
final api_instance = AlbumApi();
 | 
			
		||||
final shared = true; // bool | 
 | 
			
		||||
final assetId = assetId_example; // String | Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
 | 
			
		||||
try {
 | 
			
		||||
    final result = api_instance.getAllAlbums(shared);
 | 
			
		||||
    final result = api_instance.getAllAlbums(shared, assetId);
 | 
			
		||||
    print(result);
 | 
			
		||||
} catch (e) {
 | 
			
		||||
    print('Exception when calling AlbumApi->getAllAlbums: $e\n');
 | 
			
		||||
@@ -289,6 +290,7 @@ try {
 | 
			
		||||
Name | Type | Description  | Notes
 | 
			
		||||
------------- | ------------- | ------------- | -------------
 | 
			
		||||
 **shared** | **bool**|  | [optional] 
 | 
			
		||||
 **assetId** | **String**| Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums | [optional] 
 | 
			
		||||
 | 
			
		||||
### Return type
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -259,7 +259,10 @@ class AlbumApi {
 | 
			
		||||
  /// Parameters:
 | 
			
		||||
  ///
 | 
			
		||||
  /// * [bool] shared:
 | 
			
		||||
  Future<Response> getAllAlbumsWithHttpInfo({ bool? shared, }) async {
 | 
			
		||||
  ///
 | 
			
		||||
  /// * [String] assetId:
 | 
			
		||||
  ///   Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
  Future<Response> getAllAlbumsWithHttpInfo({ bool? shared, String? assetId, }) async {
 | 
			
		||||
    // ignore: prefer_const_declarations
 | 
			
		||||
    final path = r'/album';
 | 
			
		||||
 | 
			
		||||
@@ -273,6 +276,9 @@ class AlbumApi {
 | 
			
		||||
    if (shared != null) {
 | 
			
		||||
      queryParams.addAll(_queryParams('', 'shared', shared));
 | 
			
		||||
    }
 | 
			
		||||
    if (assetId != null) {
 | 
			
		||||
      queryParams.addAll(_queryParams('', 'assetId', assetId));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const contentTypes = <String>[];
 | 
			
		||||
 | 
			
		||||
@@ -291,8 +297,11 @@ class AlbumApi {
 | 
			
		||||
  /// Parameters:
 | 
			
		||||
  ///
 | 
			
		||||
  /// * [bool] shared:
 | 
			
		||||
  Future<List<AlbumResponseDto>?> getAllAlbums({ bool? shared, }) async {
 | 
			
		||||
    final response = await getAllAlbumsWithHttpInfo( shared: shared, );
 | 
			
		||||
  ///
 | 
			
		||||
  /// * [String] assetId:
 | 
			
		||||
  ///   Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
  Future<List<AlbumResponseDto>?> getAllAlbums({ bool? shared, String? assetId, }) async {
 | 
			
		||||
    final response = await getAllAlbumsWithHttpInfo( shared: shared, assetId: assetId, );
 | 
			
		||||
    if (response.statusCode >= HttpStatus.badRequest) {
 | 
			
		||||
      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ export interface IAlbumRepository {
 | 
			
		||||
  removeAssets(album: AlbumEntity, removeAssets: RemoveAssetsDto): Promise<AlbumEntity>;
 | 
			
		||||
  addAssets(album: AlbumEntity, addAssetsDto: AddAssetsDto): Promise<AlbumEntity>;
 | 
			
		||||
  updateAlbum(album: AlbumEntity, updateAlbumDto: UpdateAlbumDto): Promise<AlbumEntity>;
 | 
			
		||||
  getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ALBUM_REPOSITORY = 'ALBUM_REPOSITORY';
 | 
			
		||||
@@ -149,6 +150,31 @@ export class AlbumRepository implements IAlbumRepository {
 | 
			
		||||
    return albums;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async getListByAssetId(userId: string, assetId: string): Promise<AlbumEntity[]> {
 | 
			
		||||
    let query = this.albumRepository.createQueryBuilder('album');
 | 
			
		||||
 | 
			
		||||
    const albums = await query
 | 
			
		||||
        .where('album.ownerId = :ownerId', { ownerId: userId })
 | 
			
		||||
        .andWhere((qb) => {
 | 
			
		||||
          // shared with userId
 | 
			
		||||
          const subQuery = qb
 | 
			
		||||
              .subQuery()
 | 
			
		||||
              .select('assetAlbum.albumId')
 | 
			
		||||
              .from(AssetAlbumEntity, 'assetAlbum')
 | 
			
		||||
              .where('assetAlbum.assetId = :assetId', {assetId: assetId})
 | 
			
		||||
              .getQuery();
 | 
			
		||||
          return `album.id IN ${subQuery}`;
 | 
			
		||||
        })
 | 
			
		||||
        .leftJoinAndSelect('album.assets', 'assets')
 | 
			
		||||
        .leftJoinAndSelect('assets.assetInfo', 'assetInfo')
 | 
			
		||||
        .leftJoinAndSelect('album.sharedUsers', 'sharedUser')
 | 
			
		||||
        .leftJoinAndSelect('sharedUser.userInfo', 'userInfo')
 | 
			
		||||
        .orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
 | 
			
		||||
        .getMany();
 | 
			
		||||
 | 
			
		||||
    return albums;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async get(albumId: string): Promise<AlbumEntity | undefined> {
 | 
			
		||||
    let query = this.albumRepository.createQueryBuilder('album');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -116,6 +116,7 @@ describe('Album service', () => {
 | 
			
		||||
      removeAssets: jest.fn(),
 | 
			
		||||
      removeUser: jest.fn(),
 | 
			
		||||
      updateAlbum: jest.fn(),
 | 
			
		||||
      getListByAssetId: jest.fn()
 | 
			
		||||
    };
 | 
			
		||||
    sut = new AlbumService(albumRepositoryMock);
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -48,8 +48,11 @@ export class AlbumService {
 | 
			
		||||
   * @returns All Shared Album And Its Members
 | 
			
		||||
   */
 | 
			
		||||
  async getAllAlbums(authUser: AuthUserDto, getAlbumsDto: GetAlbumsDto): Promise<AlbumResponseDto[]> {
 | 
			
		||||
    if (typeof getAlbumsDto.assetId === 'string') {
 | 
			
		||||
      const albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
 | 
			
		||||
      return albums.map(mapAlbumExcludeAssetInfo);
 | 
			
		||||
    }
 | 
			
		||||
    const albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
 | 
			
		||||
 | 
			
		||||
    return albums.map((album) => mapAlbumExcludeAssetInfo(album));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,4 +18,11 @@ export class GetAlbumsDto {
 | 
			
		||||
   * undefined: shared and owned albums
 | 
			
		||||
   */
 | 
			
		||||
  shared?: boolean;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Only returns albums that contain the asset
 | 
			
		||||
   * Ignores the shared parameter
 | 
			
		||||
   * undefined: get all albums
 | 
			
		||||
   */
 | 
			
		||||
  assetId?: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -1457,10 +1457,11 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {boolean} [shared] 
 | 
			
		||||
         * @param {string} [assetId] Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getAllAlbums: async (shared?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
        getAllAlbums: async (shared?: boolean, assetId?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            const localVarPath = `/album`;
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
@@ -1481,6 +1482,10 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
 | 
			
		||||
                localVarQueryParameter['shared'] = shared;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (assetId !== undefined) {
 | 
			
		||||
                localVarQueryParameter['assetId'] = assetId;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
@@ -1684,11 +1689,12 @@ export const AlbumApiFp = function(configuration?: Configuration) {
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {boolean} [shared] 
 | 
			
		||||
         * @param {string} [assetId] Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getAllAlbums(shared?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AlbumResponseDto>>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAlbums(shared, options);
 | 
			
		||||
        async getAllAlbums(shared?: boolean, assetId?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AlbumResponseDto>>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAlbums(shared, assetId, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
@@ -1784,11 +1790,12 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {boolean} [shared] 
 | 
			
		||||
         * @param {string} [assetId] Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getAllAlbums(shared?: boolean, options?: any): AxiosPromise<Array<AlbumResponseDto>> {
 | 
			
		||||
            return localVarFp.getAllAlbums(shared, options).then((request) => request(axios, basePath));
 | 
			
		||||
        getAllAlbums(shared?: boolean, assetId?: string, options?: any): AxiosPromise<Array<AlbumResponseDto>> {
 | 
			
		||||
            return localVarFp.getAllAlbums(shared, assetId, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
@@ -1890,12 +1897,13 @@ export class AlbumApi extends BaseAPI {
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {boolean} [shared] 
 | 
			
		||||
     * @param {string} [assetId] Only returns albums that contain the asset Ignores the shared parameter undefined: get all albums
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof AlbumApi
 | 
			
		||||
     */
 | 
			
		||||
    public getAllAlbums(shared?: boolean, options?: AxiosRequestConfig) {
 | 
			
		||||
        return AlbumApiFp(this.configuration).getAllAlbums(shared, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    public getAllAlbums(shared?: boolean, assetId?: string, options?: AxiosRequestConfig) {
 | 
			
		||||
        return AlbumApiFp(this.configuration).getAllAlbums(shared, assetId, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -8,18 +8,26 @@
 | 
			
		||||
	import DetailPanel from './detail-panel.svelte';
 | 
			
		||||
	import { downloadAssets } from '$lib/stores/download';
 | 
			
		||||
	import VideoViewer from './video-viewer.svelte';
 | 
			
		||||
	import { api, AssetResponseDto, AssetTypeEnum } from '@api';
 | 
			
		||||
	import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api';
 | 
			
		||||
	import {
 | 
			
		||||
		notificationController,
 | 
			
		||||
		NotificationType
 | 
			
		||||
	} from '../shared-components/notification/notification';
 | 
			
		||||
 | 
			
		||||
	export let asset: AssetResponseDto;
 | 
			
		||||
	$: {
 | 
			
		||||
		appearsInAlbums = [];
 | 
			
		||||
 | 
			
		||||
		api.albumApi.getAllAlbums(undefined, asset.id).then(result => {
 | 
			
		||||
			appearsInAlbums = result.data;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
	let halfLeftHover = false;
 | 
			
		||||
	let halfRightHover = false;
 | 
			
		||||
	let isShowDetail = false;
 | 
			
		||||
	let appearsInAlbums: AlbumResponseDto[] = [];
 | 
			
		||||
 | 
			
		||||
	onMount(() => {
 | 
			
		||||
		document.addEventListener('keydown', (keyInfo) => handleKeyboardPress(keyInfo.key));
 | 
			
		||||
@@ -200,7 +208,7 @@
 | 
			
		||||
			class="bg-immich-bg w-[360px] row-span-full transition-all "
 | 
			
		||||
			translate="yes"
 | 
			
		||||
		>
 | 
			
		||||
			<DetailPanel {asset} on:close={() => (isShowDetail = false)} />
 | 
			
		||||
			<DetailPanel {asset} albums={appearsInAlbums} on:close={() => (isShowDetail = false)} />
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
</section>
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
	import moment from 'moment';
 | 
			
		||||
	import { createEventDispatcher, onMount } from 'svelte';
 | 
			
		||||
	import { browser } from '$app/env';
 | 
			
		||||
	import { AssetResponseDto } from '@api';
 | 
			
		||||
	import { AssetResponseDto, AlbumResponseDto } from '@api';
 | 
			
		||||
 | 
			
		||||
	// Map Property
 | 
			
		||||
	let map: any;
 | 
			
		||||
@@ -19,6 +19,8 @@
 | 
			
		||||
		drawMap(asset.exifInfo.latitude, asset.exifInfo.longitude);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	export let albums: AlbumResponseDto[];
 | 
			
		||||
 | 
			
		||||
	onMount(async () => {
 | 
			
		||||
		if (browser) {
 | 
			
		||||
			if (asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null) {
 | 
			
		||||
@@ -201,6 +203,37 @@
 | 
			
		||||
	<div class="h-[360px] w-full" id="map" />
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<section class="p-2">
 | 
			
		||||
	<div class="px-4 py-4">
 | 
			
		||||
		{#if albums.length > 0}
 | 
			
		||||
			<p class="text-sm pb-4">APPEARS IN</p>
 | 
			
		||||
		{/if}
 | 
			
		||||
		{#each albums as album}
 | 
			
		||||
			<a sveltekit:prefetch href={`/albums/${album.id}`}>
 | 
			
		||||
				<div class="flex gap-4 py-2 hover:cursor-pointer" on:click={() => dispatch('click', album)}>
 | 
			
		||||
					<div>
 | 
			
		||||
						<img
 | 
			
		||||
							alt={album.albumName}
 | 
			
		||||
							class="w-[50px] h-[50px] object-cover rounded"
 | 
			
		||||
							src={`/api/asset/thumbnail/${album.albumThumbnailAssetId}?format=JPEG`}
 | 
			
		||||
						/>
 | 
			
		||||
					</div>
 | 
			
		||||
 | 
			
		||||
					<div class="mt-auto mb-auto">
 | 
			
		||||
						<p>{album.albumName}</p>
 | 
			
		||||
						<div class="flex gap-2 text-sm">
 | 
			
		||||
							<p>{album.assetCount} items</p>
 | 
			
		||||
							{#if album.shared}
 | 
			
		||||
								<p>· Shared</p>
 | 
			
		||||
							{/if}
 | 
			
		||||
						</div>
 | 
			
		||||
					</div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</a>
 | 
			
		||||
		{/each}
 | 
			
		||||
	</div>
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
	@import 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.css';
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user