mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat: facial recognition (#2180)
This commit is contained in:
		@@ -8,6 +8,7 @@ import {
 | 
			
		||||
	ConfigurationParameters,
 | 
			
		||||
	JobApi,
 | 
			
		||||
	OAuthApi,
 | 
			
		||||
	PersonApi,
 | 
			
		||||
	PartnerApi,
 | 
			
		||||
	SearchApi,
 | 
			
		||||
	ServerInfoApi,
 | 
			
		||||
@@ -31,6 +32,7 @@ export class ImmichApi {
 | 
			
		||||
	public searchApi: SearchApi;
 | 
			
		||||
	public serverInfoApi: ServerInfoApi;
 | 
			
		||||
	public shareApi: ShareApi;
 | 
			
		||||
	public personApi: PersonApi;
 | 
			
		||||
	public systemConfigApi: SystemConfigApi;
 | 
			
		||||
	public userApi: UserApi;
 | 
			
		||||
 | 
			
		||||
@@ -49,6 +51,7 @@ export class ImmichApi {
 | 
			
		||||
		this.searchApi = new SearchApi(this.config);
 | 
			
		||||
		this.serverInfoApi = new ServerInfoApi(this.config);
 | 
			
		||||
		this.shareApi = new ShareApi(this.config);
 | 
			
		||||
		this.personApi = new PersonApi(this.config);
 | 
			
		||||
		this.systemConfigApi = new SystemConfigApi(this.config);
 | 
			
		||||
		this.userApi = new UserApi(this.config);
 | 
			
		||||
	}
 | 
			
		||||
@@ -98,6 +101,11 @@ export class ImmichApi {
 | 
			
		||||
		const path = `/user/profile-image/${userId}`;
 | 
			
		||||
		return this.createUrl(path);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public getPeopleThumbnailUrl(personId: string) {
 | 
			
		||||
		const path = `/person/${personId}/thumbnail`;
 | 
			
		||||
		return this.createUrl(path);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const api = new ImmichApi({ basePath: '/api' });
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										451
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										451
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@@ -339,6 +339,12 @@ export interface AllJobStatusResponseDto {
 | 
			
		||||
     * @memberof AllJobStatusResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'search-queue': JobStatusDto;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {JobStatusDto}
 | 
			
		||||
     * @memberof AllJobStatusResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'recognize-faces-queue': JobStatusDto;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
@@ -566,6 +572,12 @@ export interface AssetResponseDto {
 | 
			
		||||
     * @memberof AssetResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'tags'?: Array<TagResponseDto>;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {Array<PersonResponseDto>}
 | 
			
		||||
     * @memberof AssetResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'people'?: Array<PersonResponseDto>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -1329,6 +1341,7 @@ export const JobName = {
 | 
			
		||||
    MetadataExtractionQueue: 'metadata-extraction-queue',
 | 
			
		||||
    VideoConversionQueue: 'video-conversion-queue',
 | 
			
		||||
    ObjectTaggingQueue: 'object-tagging-queue',
 | 
			
		||||
    RecognizeFacesQueue: 'recognize-faces-queue',
 | 
			
		||||
    ClipEncodingQueue: 'clip-encoding-queue',
 | 
			
		||||
    BackgroundTaskQueue: 'background-task-queue',
 | 
			
		||||
    StorageTemplateMigrationQueue: 'storage-template-migration-queue',
 | 
			
		||||
@@ -1546,6 +1559,44 @@ export interface OAuthConfigResponseDto {
 | 
			
		||||
     */
 | 
			
		||||
    'autoLaunch'?: boolean;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
 * @interface PersonResponseDto
 | 
			
		||||
 */
 | 
			
		||||
export interface PersonResponseDto {
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof PersonResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'id': string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof PersonResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'name': string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof PersonResponseDto
 | 
			
		||||
     */
 | 
			
		||||
    'thumbnailPath': string;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
 * @interface PersonUpdateDto
 | 
			
		||||
 */
 | 
			
		||||
export interface PersonUpdateDto {
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @type {string}
 | 
			
		||||
     * @memberof PersonUpdateDto
 | 
			
		||||
     */
 | 
			
		||||
    'name': string;
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * 
 | 
			
		||||
 * @export
 | 
			
		||||
@@ -7460,6 +7511,406 @@ export class PartnerApi extends BaseAPI {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * PersonApi - axios parameter creator
 | 
			
		||||
 * @export
 | 
			
		||||
 */
 | 
			
		||||
export const PersonApiAxiosParamCreator = function (configuration?: Configuration) {
 | 
			
		||||
    return {
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getAllPeople: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            const localVarPath = `/person`;
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            // authentication cookie required
 | 
			
		||||
 | 
			
		||||
            // authentication api_key required
 | 
			
		||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
			
		||||
 | 
			
		||||
            // authentication bearer required
 | 
			
		||||
            // http bearer authentication required
 | 
			
		||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getPerson: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            // verify required parameter 'id' is not null or undefined
 | 
			
		||||
            assertParamExists('getPerson', 'id', id)
 | 
			
		||||
            const localVarPath = `/person/{id}`
 | 
			
		||||
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            // authentication cookie required
 | 
			
		||||
 | 
			
		||||
            // authentication api_key required
 | 
			
		||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
			
		||||
 | 
			
		||||
            // authentication bearer required
 | 
			
		||||
            // http bearer authentication required
 | 
			
		||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getPersonAssets: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            // verify required parameter 'id' is not null or undefined
 | 
			
		||||
            assertParamExists('getPersonAssets', 'id', id)
 | 
			
		||||
            const localVarPath = `/person/{id}/assets`
 | 
			
		||||
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            // authentication cookie required
 | 
			
		||||
 | 
			
		||||
            // authentication api_key required
 | 
			
		||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
			
		||||
 | 
			
		||||
            // authentication bearer required
 | 
			
		||||
            // http bearer authentication required
 | 
			
		||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getPersonThumbnail: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            // verify required parameter 'id' is not null or undefined
 | 
			
		||||
            assertParamExists('getPersonThumbnail', 'id', id)
 | 
			
		||||
            const localVarPath = `/person/{id}/thumbnail`
 | 
			
		||||
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            // authentication cookie required
 | 
			
		||||
 | 
			
		||||
            // authentication api_key required
 | 
			
		||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
			
		||||
 | 
			
		||||
            // authentication bearer required
 | 
			
		||||
            // http bearer authentication required
 | 
			
		||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {PersonUpdateDto} personUpdateDto 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        updatePerson: async (id: string, personUpdateDto: PersonUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
 | 
			
		||||
            // verify required parameter 'id' is not null or undefined
 | 
			
		||||
            assertParamExists('updatePerson', 'id', id)
 | 
			
		||||
            // verify required parameter 'personUpdateDto' is not null or undefined
 | 
			
		||||
            assertParamExists('updatePerson', 'personUpdateDto', personUpdateDto)
 | 
			
		||||
            const localVarPath = `/person/{id}`
 | 
			
		||||
                .replace(`{${"id"}}`, encodeURIComponent(String(id)));
 | 
			
		||||
            // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | 
			
		||||
            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
 | 
			
		||||
            let baseOptions;
 | 
			
		||||
            if (configuration) {
 | 
			
		||||
                baseOptions = configuration.baseOptions;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
 | 
			
		||||
            const localVarHeaderParameter = {} as any;
 | 
			
		||||
            const localVarQueryParameter = {} as any;
 | 
			
		||||
 | 
			
		||||
            // authentication cookie required
 | 
			
		||||
 | 
			
		||||
            // authentication api_key required
 | 
			
		||||
            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
 | 
			
		||||
 | 
			
		||||
            // authentication bearer required
 | 
			
		||||
            // http bearer authentication required
 | 
			
		||||
            await setBearerAuthToObject(localVarHeaderParameter, configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
            localVarHeaderParameter['Content-Type'] = 'application/json';
 | 
			
		||||
 | 
			
		||||
            setSearchParams(localVarUrlObj, localVarQueryParameter);
 | 
			
		||||
            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
 | 
			
		||||
            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
 | 
			
		||||
            localVarRequestOptions.data = serializeDataIfNeeded(personUpdateDto, localVarRequestOptions, configuration)
 | 
			
		||||
 | 
			
		||||
            return {
 | 
			
		||||
                url: toPathString(localVarUrlObj),
 | 
			
		||||
                options: localVarRequestOptions,
 | 
			
		||||
            };
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * PersonApi - functional programming interface
 | 
			
		||||
 * @export
 | 
			
		||||
 */
 | 
			
		||||
export const PersonApiFp = function(configuration?: Configuration) {
 | 
			
		||||
    const localVarAxiosParamCreator = PersonApiAxiosParamCreator(configuration)
 | 
			
		||||
    return {
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getAllPeople(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getPerson(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getPerson(id, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getPersonAssets(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getPersonAssets(id, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async getPersonThumbnail(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<File>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.getPersonThumbnail(id, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {PersonUpdateDto} personUpdateDto 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        async updatePerson(id: string, personUpdateDto: PersonUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PersonResponseDto>> {
 | 
			
		||||
            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePerson(id, personUpdateDto, options);
 | 
			
		||||
            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
 | 
			
		||||
        },
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * PersonApi - factory interface
 | 
			
		||||
 * @export
 | 
			
		||||
 */
 | 
			
		||||
export const PersonApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
 | 
			
		||||
    const localVarFp = PersonApiFp(configuration)
 | 
			
		||||
    return {
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getAllPeople(options?: any): AxiosPromise<Array<PersonResponseDto>> {
 | 
			
		||||
            return localVarFp.getAllPeople(options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getPerson(id: string, options?: any): AxiosPromise<PersonResponseDto> {
 | 
			
		||||
            return localVarFp.getPerson(id, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getPersonAssets(id: string, options?: any): AxiosPromise<Array<AssetResponseDto>> {
 | 
			
		||||
            return localVarFp.getPersonAssets(id, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        getPersonThumbnail(id: string, options?: any): AxiosPromise<File> {
 | 
			
		||||
            return localVarFp.getPersonThumbnail(id, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
        /**
 | 
			
		||||
         * 
 | 
			
		||||
         * @param {string} id 
 | 
			
		||||
         * @param {PersonUpdateDto} personUpdateDto 
 | 
			
		||||
         * @param {*} [options] Override http request option.
 | 
			
		||||
         * @throws {RequiredError}
 | 
			
		||||
         */
 | 
			
		||||
        updatePerson(id: string, personUpdateDto: PersonUpdateDto, options?: any): AxiosPromise<PersonResponseDto> {
 | 
			
		||||
            return localVarFp.updatePerson(id, personUpdateDto, options).then((request) => request(axios, basePath));
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * PersonApi - object-oriented interface
 | 
			
		||||
 * @export
 | 
			
		||||
 * @class PersonApi
 | 
			
		||||
 * @extends {BaseAPI}
 | 
			
		||||
 */
 | 
			
		||||
export class PersonApi extends BaseAPI {
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof PersonApi
 | 
			
		||||
     */
 | 
			
		||||
    public getAllPeople(options?: AxiosRequestConfig) {
 | 
			
		||||
        return PersonApiFp(this.configuration).getAllPeople(options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {string} id 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof PersonApi
 | 
			
		||||
     */
 | 
			
		||||
    public getPerson(id: string, options?: AxiosRequestConfig) {
 | 
			
		||||
        return PersonApiFp(this.configuration).getPerson(id, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {string} id 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof PersonApi
 | 
			
		||||
     */
 | 
			
		||||
    public getPersonAssets(id: string, options?: AxiosRequestConfig) {
 | 
			
		||||
        return PersonApiFp(this.configuration).getPersonAssets(id, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {string} id 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof PersonApi
 | 
			
		||||
     */
 | 
			
		||||
    public getPersonThumbnail(id: string, options?: AxiosRequestConfig) {
 | 
			
		||||
        return PersonApiFp(this.configuration).getPersonThumbnail(id, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {string} id 
 | 
			
		||||
     * @param {PersonUpdateDto} personUpdateDto 
 | 
			
		||||
     * @param {*} [options] Override http request option.
 | 
			
		||||
     * @throws {RequiredError}
 | 
			
		||||
     * @memberof PersonApi
 | 
			
		||||
     */
 | 
			
		||||
    public updatePerson(id: string, personUpdateDto: PersonUpdateDto, options?: AxiosRequestConfig) {
 | 
			
		||||
        return PersonApiFp(this.configuration).updatePerson(id, personUpdateDto, options).then((request) => request(this.axios, this.basePath));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * SearchApi - axios parameter creator
 | 
			
		||||
 * @export
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,10 @@
 | 
			
		||||
			title: 'Encode Clip',
 | 
			
		||||
			subtitle: 'Run machine learning to generate clip embeddings'
 | 
			
		||||
		},
 | 
			
		||||
		[JobName.RecognizeFacesQueue]: {
 | 
			
		||||
			title: 'Recognize Faces',
 | 
			
		||||
			subtitle: 'Run machine learning to recognize faces'
 | 
			
		||||
		},
 | 
			
		||||
		[JobName.VideoConversionQueue]: {
 | 
			
		||||
			title: 'Transcode Videos',
 | 
			
		||||
			subtitle: 'Transcode videos not in the desired format'
 | 
			
		||||
 
 | 
			
		||||
@@ -120,10 +120,6 @@
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const clearMultiSelectAssetAssetHandler = () => {
 | 
			
		||||
		multiSelectAsset = new Set();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// Update Album Name
 | 
			
		||||
	$: {
 | 
			
		||||
		if (!isEditingTitle && currentAlbumName != album.albumName && isOwned) {
 | 
			
		||||
@@ -340,7 +336,7 @@
 | 
			
		||||
	{#if isMultiSelectionMode}
 | 
			
		||||
		<AssetSelectControlBar
 | 
			
		||||
			assets={multiSelectAsset}
 | 
			
		||||
			clearSelect={clearMultiSelectAssetAssetHandler}
 | 
			
		||||
			clearSelect={() => (multiSelectAsset = new Set())}
 | 
			
		||||
		>
 | 
			
		||||
			<DownloadFiles filename={album.albumName} sharedLinkKey={sharedLink?.key} />
 | 
			
		||||
			{#if isOwned}
 | 
			
		||||
 
 | 
			
		||||
@@ -91,6 +91,11 @@
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleCloseViewer = () => {
 | 
			
		||||
		isShowDetail = false;
 | 
			
		||||
		closeViewer();
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const closeViewer = () => {
 | 
			
		||||
		dispatch('close');
 | 
			
		||||
	};
 | 
			
		||||
@@ -398,6 +403,7 @@
 | 
			
		||||
				{asset}
 | 
			
		||||
				albums={appearsInAlbums}
 | 
			
		||||
				on:close={() => (isShowDetail = false)}
 | 
			
		||||
				on:close-viewer={handleCloseViewer}
 | 
			
		||||
				on:description-focus-in={disableKeyDownEvent}
 | 
			
		||||
				on:description-focus-out={enableKeyDownEvent}
 | 
			
		||||
			/>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,17 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import Close from 'svelte-material-icons/Close.svelte';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import { locale } from '$lib/stores/preferences.store';
 | 
			
		||||
	import type { LatLngTuple } from 'leaflet';
 | 
			
		||||
	import { DateTime } from 'luxon';
 | 
			
		||||
	import Calendar from 'svelte-material-icons/Calendar.svelte';
 | 
			
		||||
	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 | 
			
		||||
	import CameraIris from 'svelte-material-icons/CameraIris.svelte';
 | 
			
		||||
	import Close from 'svelte-material-icons/Close.svelte';
 | 
			
		||||
	import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
 | 
			
		||||
	import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte';
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	import { AssetResponseDto, AlbumResponseDto, api, ThumbnailFormat } from '@api';
 | 
			
		||||
	import { asByteUnitString } from '../../utils/byte-units';
 | 
			
		||||
	import { locale } from '$lib/stores/preferences.store';
 | 
			
		||||
	import { DateTime } from 'luxon';
 | 
			
		||||
	import type { LatLngTuple } from 'leaflet';
 | 
			
		||||
	import { page } from '$app/stores';
 | 
			
		||||
	import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
 | 
			
		||||
 | 
			
		||||
	export let asset: AssetResponseDto;
 | 
			
		||||
	export let albums: AlbumResponseDto[] = [];
 | 
			
		||||
@@ -20,9 +21,10 @@
 | 
			
		||||
	$: {
 | 
			
		||||
		// Get latest description from server
 | 
			
		||||
		if (asset.id) {
 | 
			
		||||
			api.assetApi
 | 
			
		||||
				.getAssetById(asset.id)
 | 
			
		||||
				.then((res) => (textarea.value = res.data?.exifInfo?.description || ''));
 | 
			
		||||
			api.assetApi.getAssetById(asset.id).then((res) => {
 | 
			
		||||
				people = res.data?.people || [];
 | 
			
		||||
				textarea.value = res.data?.exifInfo?.description || '';
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -35,6 +37,8 @@
 | 
			
		||||
		}
 | 
			
		||||
	})();
 | 
			
		||||
 | 
			
		||||
	$: people = asset.people || [];
 | 
			
		||||
 | 
			
		||||
	const dispatch = createEventDispatcher();
 | 
			
		||||
 | 
			
		||||
	const getMegapixel = (width: number, height: number): number | undefined => {
 | 
			
		||||
@@ -81,7 +85,7 @@
 | 
			
		||||
		<p class="text-immich-fg dark:text-immich-dark-fg text-lg">Info</p>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="mx-4 mt-10">
 | 
			
		||||
	<section class="mx-4 mt-10">
 | 
			
		||||
		<textarea
 | 
			
		||||
			bind:this={textarea}
 | 
			
		||||
			class="max-h-[500px]
 | 
			
		||||
@@ -96,13 +100,35 @@
 | 
			
		||||
			bind:value={description}
 | 
			
		||||
			disabled={$page?.data?.user?.id !== asset.ownerId}
 | 
			
		||||
		/>
 | 
			
		||||
	</div>
 | 
			
		||||
	</section>
 | 
			
		||||
 | 
			
		||||
	{#if people.length > 0}
 | 
			
		||||
		<section class="px-4 py-4 text-sm">
 | 
			
		||||
			<h2>PEOPLE</h2>
 | 
			
		||||
 | 
			
		||||
			<div class="flex flex-wrap gap-2 mt-4">
 | 
			
		||||
				{#each people as person (person.id)}
 | 
			
		||||
					<a href="/people/{person.id}" class="w-[90px]" on:click={() => dispatch('close-viewer')}>
 | 
			
		||||
						<ImageThumbnail
 | 
			
		||||
							curve
 | 
			
		||||
							shadow
 | 
			
		||||
							url={api.getPeopleThumbnailUrl(person.id)}
 | 
			
		||||
							altText={person.name}
 | 
			
		||||
							widthStyle="90px"
 | 
			
		||||
							heightStyle="90px"
 | 
			
		||||
						/>
 | 
			
		||||
						<p class="font-medium mt-1 truncate">{person.name}</p>
 | 
			
		||||
					</a>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</div>
 | 
			
		||||
		</section>
 | 
			
		||||
	{/if}
 | 
			
		||||
 | 
			
		||||
	<div class="px-4 py-4">
 | 
			
		||||
		{#if !asset.exifInfo}
 | 
			
		||||
			<p class="text-sm pb-4">NO EXIF INFO AVAILABLE</p>
 | 
			
		||||
			<p class="text-sm">NO EXIF INFO AVAILABLE</p>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<p class="text-sm pb-4">DETAILS</p>
 | 
			
		||||
			<p class="text-sm">DETAILS</p>
 | 
			
		||||
		{/if}
 | 
			
		||||
 | 
			
		||||
		{#if asset.exifInfo?.dateTimeOriginal}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,13 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { imageLoad } from '$lib/utils/image-load';
 | 
			
		||||
 | 
			
		||||
	export let url: string;
 | 
			
		||||
	export let altText: string;
 | 
			
		||||
	export let heightStyle: string;
 | 
			
		||||
	export let heightStyle: string | undefined = undefined;
 | 
			
		||||
	export let widthStyle: string;
 | 
			
		||||
 | 
			
		||||
	export let curve = false;
 | 
			
		||||
	export let shadow = false;
 | 
			
		||||
	export let circle = false;
 | 
			
		||||
	let loading = true;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -13,7 +17,11 @@
 | 
			
		||||
	src={url}
 | 
			
		||||
	alt={altText}
 | 
			
		||||
	class="object-cover transition-opacity duration-300"
 | 
			
		||||
	class:rounded-lg={curve}
 | 
			
		||||
	class:shadow-lg={shadow}
 | 
			
		||||
	class:rounded-full={circle}
 | 
			
		||||
	class:opacity-0={loading}
 | 
			
		||||
	draggable="false"
 | 
			
		||||
	on:load|once={() => (loading = false)}
 | 
			
		||||
	use:imageLoad
 | 
			
		||||
	on:image-load|once={() => (loading = false)}
 | 
			
		||||
/>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										42
									
								
								web/src/lib/components/faces-page/edit-name-input.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								web/src/lib/components/faces-page/edit-name-input.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,42 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { PersonResponseDto, api } from '@api';
 | 
			
		||||
	import { createEventDispatcher } from 'svelte';
 | 
			
		||||
	import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
 | 
			
		||||
	import Button from '../elements/buttons/button.svelte';
 | 
			
		||||
 | 
			
		||||
	export let person: PersonResponseDto;
 | 
			
		||||
	let name = person.name;
 | 
			
		||||
 | 
			
		||||
	const dispatch = createEventDispatcher<{ change: string }>();
 | 
			
		||||
	const handleNameChange = () => dispatch('change', name);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	class="flex place-items-center max-w-lg rounded-lg border dark:border-transparent p-2 bg-gray-100 dark:bg-gray-700"
 | 
			
		||||
>
 | 
			
		||||
	<ImageThumbnail
 | 
			
		||||
		circle
 | 
			
		||||
		shadow
 | 
			
		||||
		url={api.getPeopleThumbnailUrl(person.id)}
 | 
			
		||||
		altText={person.name}
 | 
			
		||||
		widthStyle="2rem"
 | 
			
		||||
		heightStyle="2rem"
 | 
			
		||||
	/>
 | 
			
		||||
	<form
 | 
			
		||||
		class="ml-4 flex justify-between w-full gap-16"
 | 
			
		||||
		autocomplete="off"
 | 
			
		||||
		on:submit|preventDefault={handleNameChange}
 | 
			
		||||
	>
 | 
			
		||||
		<!-- svelte-ignore a11y-autofocus -->
 | 
			
		||||
		<input
 | 
			
		||||
			autofocus
 | 
			
		||||
			class="gap-2 w-full bg-gray-100 dark:bg-gray-700 dark:text-white"
 | 
			
		||||
			type="text"
 | 
			
		||||
			placeholder="New name or nickname"
 | 
			
		||||
			required
 | 
			
		||||
			bind:value={name}
 | 
			
		||||
			on:blur
 | 
			
		||||
		/>
 | 
			
		||||
		<Button size="sm" type="submit">Done</Button>
 | 
			
		||||
	</form>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -10,6 +10,7 @@ export enum AppRoute {
 | 
			
		||||
	ALBUMS = '/albums',
 | 
			
		||||
	ARCHIVE = '/archive',
 | 
			
		||||
	FAVORITES = '/favorites',
 | 
			
		||||
	PEOPLE = '/people',
 | 
			
		||||
	PHOTOS = '/photos',
 | 
			
		||||
	EXPLORE = '/explore',
 | 
			
		||||
	SHARING = '/sharing',
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,11 @@ export const load = (async ({ locals, parent }) => {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { data: items } = await locals.api.searchApi.getExploreData();
 | 
			
		||||
 | 
			
		||||
	const { data: people } = await locals.api.personApi.getAllPeople();
 | 
			
		||||
	return {
 | 
			
		||||
		user,
 | 
			
		||||
		items,
 | 
			
		||||
		people,
 | 
			
		||||
		meta: {
 | 
			
		||||
			title: 'Explore'
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,13 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
 | 
			
		||||
	import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
 | 
			
		||||
	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
 | 
			
		||||
	import { AppRoute } from '$lib/constants';
 | 
			
		||||
	import { AssetTypeEnum, SearchExploreItem } from '@api';
 | 
			
		||||
	import { AssetTypeEnum, SearchExploreResponseDto, api } from '@api';
 | 
			
		||||
	import ClockOutline from 'svelte-material-icons/ClockOutline.svelte';
 | 
			
		||||
	import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
 | 
			
		||||
	import MotionPlayOutline from 'svelte-material-icons/MotionPlayOutline.svelte';
 | 
			
		||||
	import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
 | 
			
		||||
	import HeartMultipleOutline from 'svelte-material-icons/HeartMultipleOutline.svelte';
 | 
			
		||||
	import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
	export let data: PageData;
 | 
			
		||||
@@ -19,27 +20,47 @@
 | 
			
		||||
 | 
			
		||||
	const MAX_ITEMS = 12;
 | 
			
		||||
 | 
			
		||||
	let things: SearchExploreItem[] = [];
 | 
			
		||||
	let places: SearchExploreItem[] = [];
 | 
			
		||||
	const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => {
 | 
			
		||||
		const targetField = items.find((item) => item.fieldName === field);
 | 
			
		||||
		return targetField?.items || [];
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	for (const item of data.items) {
 | 
			
		||||
		switch (item.fieldName) {
 | 
			
		||||
			case Field.OBJECTS:
 | 
			
		||||
				things = item.items;
 | 
			
		||||
				break;
 | 
			
		||||
 | 
			
		||||
			case Field.CITY:
 | 
			
		||||
				places = item.items;
 | 
			
		||||
				break;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	things = things.slice(0, MAX_ITEMS);
 | 
			
		||||
	places = places.slice(0, MAX_ITEMS);
 | 
			
		||||
	$: things = getFieldItems(data.items, Field.OBJECTS);
 | 
			
		||||
	$: places = getFieldItems(data.items, Field.CITY);
 | 
			
		||||
	$: people = data.people.slice(0, MAX_ITEMS);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<UserPageLayout user={data.user} title={data.meta.title}>
 | 
			
		||||
	<div class="mx-4 flex flex-col">
 | 
			
		||||
	<div class="mx-4">
 | 
			
		||||
		{#if people.length > 0}
 | 
			
		||||
			<div class="mb-6 mt-2">
 | 
			
		||||
				<div class="flex justify-between">
 | 
			
		||||
					<p class="mb-4 dark:text-immich-dark-fg font-medium">People</p>
 | 
			
		||||
					{#if data.people.length > MAX_ITEMS}
 | 
			
		||||
						<a
 | 
			
		||||
							href={AppRoute.PEOPLE}
 | 
			
		||||
							class="font-medium hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg"
 | 
			
		||||
							draggable="false">View All</a
 | 
			
		||||
						>
 | 
			
		||||
					{/if}
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="flex flex-row flex-wrap gap-4">
 | 
			
		||||
					{#each people as person (person.id)}
 | 
			
		||||
						<a href="/people/{person.id}" class="w-24 text-center">
 | 
			
		||||
							<ImageThumbnail
 | 
			
		||||
								circle
 | 
			
		||||
								shadow
 | 
			
		||||
								url={api.getPeopleThumbnailUrl(person.id)}
 | 
			
		||||
								altText={person.name}
 | 
			
		||||
								widthStyle="100%"
 | 
			
		||||
							/>
 | 
			
		||||
							<p class="font-medium mt-2 text-ellipsis text-sm dark:text-white">{person.name}</p>
 | 
			
		||||
						</a>
 | 
			
		||||
					{/each}
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
		{/if}
 | 
			
		||||
 | 
			
		||||
		{#if places.length > 0}
 | 
			
		||||
			<div class="mb-6 mt-2">
 | 
			
		||||
				<div>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								web/src/routes/(user)/people/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								web/src/routes/(user)/people/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageServerLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async ({ locals, parent }) => {
 | 
			
		||||
	const { user } = await parent();
 | 
			
		||||
	if (!user) {
 | 
			
		||||
		throw redirect(302, '/auth/login');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { data: people } = await locals.api.personApi.getAllPeople();
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		user,
 | 
			
		||||
		people,
 | 
			
		||||
		meta: {
 | 
			
		||||
			title: 'People'
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
}) satisfies PageServerLoad;
 | 
			
		||||
							
								
								
									
										48
									
								
								web/src/routes/(user)/people/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								web/src/routes/(user)/people/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
 | 
			
		||||
	import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
 | 
			
		||||
	import { api } from '@api';
 | 
			
		||||
	import AccountOff from 'svelte-material-icons/AccountOff.svelte';
 | 
			
		||||
	import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
	export let data: PageData;
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<UserPageLayout user={data.user} showUploadButton title="People">
 | 
			
		||||
	{#if data.people.length > 0}
 | 
			
		||||
		<div class="pl-4">
 | 
			
		||||
			<div class="flex flex-row flex-wrap gap-1">
 | 
			
		||||
				{#each data.people as person (person.id)}
 | 
			
		||||
					<div class="relative">
 | 
			
		||||
						<a href="/people/{person.id}" draggable="false">
 | 
			
		||||
							<div class="filter brightness-75 rounded-xl w-48">
 | 
			
		||||
								<ImageThumbnail
 | 
			
		||||
									shadow
 | 
			
		||||
									url={api.getPeopleThumbnailUrl(person.id)}
 | 
			
		||||
									altText={person.name}
 | 
			
		||||
									widthStyle="100%"
 | 
			
		||||
								/>
 | 
			
		||||
							</div>
 | 
			
		||||
							{#if person.name}
 | 
			
		||||
								<span
 | 
			
		||||
									class="absolute bottom-2 w-full text-center font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]"
 | 
			
		||||
								>
 | 
			
		||||
									{person.name}
 | 
			
		||||
								</span>
 | 
			
		||||
							{/if}
 | 
			
		||||
						</a>
 | 
			
		||||
					</div>
 | 
			
		||||
				{/each}
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<div
 | 
			
		||||
			class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"
 | 
			
		||||
		>
 | 
			
		||||
			<div class="flex flex-col content-center items-center text-center">
 | 
			
		||||
				<AccountOff size="3.5em" />
 | 
			
		||||
				<p class="font-medium text-3xl mt-5">No people</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	{/if}
 | 
			
		||||
</UserPageLayout>
 | 
			
		||||
							
								
								
									
										21
									
								
								web/src/routes/(user)/people/[personId]/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								web/src/routes/(user)/people/[personId]/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
import { redirect } from '@sveltejs/kit';
 | 
			
		||||
import type { PageServerLoad } from './$types';
 | 
			
		||||
 | 
			
		||||
export const load = (async ({ locals, parent, params }) => {
 | 
			
		||||
	const { user } = await parent();
 | 
			
		||||
	if (!user) {
 | 
			
		||||
		throw redirect(302, '/auth/login');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const { data: person } = await locals.api.personApi.getPerson(params.personId);
 | 
			
		||||
	const { data: assets } = await locals.api.personApi.getPersonAssets(params.personId);
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		user,
 | 
			
		||||
		assets,
 | 
			
		||||
		person,
 | 
			
		||||
		meta: {
 | 
			
		||||
			title: person.name || 'Person'
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
}) satisfies PageServerLoad;
 | 
			
		||||
							
								
								
									
										113
									
								
								web/src/routes/(user)/people/[personId]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								web/src/routes/(user)/people/[personId]/+page.svelte
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,113 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { goto } from '$app/navigation';
 | 
			
		||||
	import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
 | 
			
		||||
	import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte';
 | 
			
		||||
	import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
 | 
			
		||||
	import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
 | 
			
		||||
	import DownloadFiles from '$lib/components/photos-page/actions/download-files.svelte';
 | 
			
		||||
	import MoveToArchive from '$lib/components/photos-page/actions/move-to-archive.svelte';
 | 
			
		||||
	import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte';
 | 
			
		||||
	import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
 | 
			
		||||
	import OptionAddToAlbum from '$lib/components/photos-page/menu-options/option-add-to-album.svelte';
 | 
			
		||||
	import OptionAddToFavorites from '$lib/components/photos-page/menu-options/option-add-to-favorites.svelte';
 | 
			
		||||
	import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
 | 
			
		||||
	import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
 | 
			
		||||
	import { AppRoute } from '$lib/constants';
 | 
			
		||||
	import { handleError } from '$lib/utils/handle-error';
 | 
			
		||||
	import { AssetResponseDto, api } from '@api';
 | 
			
		||||
	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
 | 
			
		||||
	import Plus from 'svelte-material-icons/Plus.svelte';
 | 
			
		||||
	import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
	export let data: PageData;
 | 
			
		||||
 | 
			
		||||
	let isEditName = false;
 | 
			
		||||
 | 
			
		||||
	let multiSelectAsset: Set<AssetResponseDto> = new Set();
 | 
			
		||||
	$: isMultiSelectionMode = multiSelectAsset.size > 0;
 | 
			
		||||
 | 
			
		||||
	const handleNameChange = async (name: string) => {
 | 
			
		||||
		try {
 | 
			
		||||
			isEditName = false;
 | 
			
		||||
			data.person.name = name;
 | 
			
		||||
			await api.personApi.updatePerson(data.person.id, { name });
 | 
			
		||||
		} catch (error) {
 | 
			
		||||
			handleError(error, 'Unable to save name');
 | 
			
		||||
		}
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const handleAssetDelete = (assetId: string) => {
 | 
			
		||||
		data.assets = data.assets.filter((asset: AssetResponseDto) => asset.id !== assetId);
 | 
			
		||||
	};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if isMultiSelectionMode}
 | 
			
		||||
	<AssetSelectControlBar
 | 
			
		||||
		assets={multiSelectAsset}
 | 
			
		||||
		clearSelect={() => (multiSelectAsset = new Set())}
 | 
			
		||||
	>
 | 
			
		||||
		<CreateSharedLink />
 | 
			
		||||
		<MoveToArchive />
 | 
			
		||||
		<DownloadFiles filename={data.person.name} />
 | 
			
		||||
		<AssetSelectContextMenu icon={Plus} title="Add">
 | 
			
		||||
			<OptionAddToFavorites />
 | 
			
		||||
			<OptionAddToAlbum />
 | 
			
		||||
			<OptionAddToAlbum shared />
 | 
			
		||||
		</AssetSelectContextMenu>
 | 
			
		||||
		<DeleteAssets onAssetDelete={handleAssetDelete} />
 | 
			
		||||
	</AssetSelectControlBar>
 | 
			
		||||
{:else}
 | 
			
		||||
	<ControlAppBar
 | 
			
		||||
		showBackButton
 | 
			
		||||
		backIcon={ArrowLeft}
 | 
			
		||||
		on:close-button-click={() => goto(AppRoute.EXPLORE)}
 | 
			
		||||
	/>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<!-- Face information block -->
 | 
			
		||||
<section class="pt-24 px-4 sm:px-6 flex place-items-center">
 | 
			
		||||
	{#if isEditName}
 | 
			
		||||
		<EditNameInput
 | 
			
		||||
			person={data.person}
 | 
			
		||||
			on:change={(event) => handleNameChange(event.detail)}
 | 
			
		||||
			on:blur={() => (isEditName = false)}
 | 
			
		||||
		/>
 | 
			
		||||
	{:else}
 | 
			
		||||
		<ImageThumbnail
 | 
			
		||||
			circle
 | 
			
		||||
			shadow
 | 
			
		||||
			url={api.getPeopleThumbnailUrl(data.person.id)}
 | 
			
		||||
			altText={data.person.name}
 | 
			
		||||
			widthStyle="3.375rem"
 | 
			
		||||
			heightStyle="3.375rem"
 | 
			
		||||
		/>
 | 
			
		||||
		<button
 | 
			
		||||
			title="Edit name"
 | 
			
		||||
			class="px-4 text-immich-primary dark:text-immich-dark-primary"
 | 
			
		||||
			on:click={() => (isEditName = true)}
 | 
			
		||||
		>
 | 
			
		||||
			{#if data.person.name}
 | 
			
		||||
				<p class="font-medium py-2">{data.person.name}</p>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<p class="font-medium w-fit">Add a name</p>
 | 
			
		||||
				<p class="text-sm text-gray-500 dark:text-immich-gray">
 | 
			
		||||
					Find them fast by name with search
 | 
			
		||||
				</p>
 | 
			
		||||
			{/if}
 | 
			
		||||
		</button>
 | 
			
		||||
	{/if}
 | 
			
		||||
</section>
 | 
			
		||||
 | 
			
		||||
<!-- Gallery Block -->
 | 
			
		||||
<section class="relative pt-8 sm:px-4 mb-12 bg-immich-bg dark:bg-immich-dark-bg">
 | 
			
		||||
	<section class="overflow-y-auto relative immich-scrollbar">
 | 
			
		||||
		<section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg">
 | 
			
		||||
			<GalleryViewer
 | 
			
		||||
				assets={data.assets}
 | 
			
		||||
				viewFrom="search-page"
 | 
			
		||||
				showArchiveIcon={true}
 | 
			
		||||
				bind:selectedAssets={multiSelectAsset}
 | 
			
		||||
			/>
 | 
			
		||||
		</section>
 | 
			
		||||
	</section>
 | 
			
		||||
</section>
 | 
			
		||||
@@ -91,7 +91,7 @@
 | 
			
		||||
				</div>
 | 
			
		||||
			{:else}
 | 
			
		||||
				<div
 | 
			
		||||
					class="flex items-center place-content-center w-full min-h-[calc(100vh_-_11rem)] dark:text-white"
 | 
			
		||||
					class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"
 | 
			
		||||
				>
 | 
			
		||||
					<div class="flex flex-col content-center items-center text-center">
 | 
			
		||||
						<ImageOffOutline size="3.5em" />
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user