mirror of
https://github.com/immich-app/immich.git
synced 2025-01-01 08:31:59 +00:00
feat(server,web): improve performances in person page (1) (#4387)
* feat: improve performances in people page * feat: add loadingspinner when searching * fix: reset people on error * fix: case insensitive * feat: better sql query * fix: reset people list before api request * fix: format
This commit is contained in:
parent
f36c40bc6b
commit
b8d6cc1e09
17 changed files with 361 additions and 44 deletions
89
cli/src/api/open-api/api.ts
generated
89
cli/src/api/open-api/api.ts
generated
|
@ -12139,6 +12139,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
searchPerson: async (name: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'name' is not null or undefined
|
||||||
|
assertParamExists('searchPerson', 'name', name)
|
||||||
|
const localVarPath = `/search/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)
|
||||||
|
|
||||||
|
if (name !== undefined) {
|
||||||
|
localVarQueryParameter['name'] = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
@ -12192,6 +12237,16 @@ export const SearchApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async searchPerson(name: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.searchPerson(name, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12219,6 +12274,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
|
||||||
search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
|
search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
|
||||||
return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
|
return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SearchApiSearchPersonRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||||
|
return localVarFp.searchPerson(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12341,6 +12405,20 @@ export interface SearchApiSearchRequest {
|
||||||
readonly motion?: boolean
|
readonly motion?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for searchPerson operation in SearchApi.
|
||||||
|
* @export
|
||||||
|
* @interface SearchApiSearchPersonRequest
|
||||||
|
*/
|
||||||
|
export interface SearchApiSearchPersonRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SearchApiSearchPerson
|
||||||
|
*/
|
||||||
|
readonly name: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchApi - object-oriented interface
|
* SearchApi - object-oriented interface
|
||||||
* @export
|
* @export
|
||||||
|
@ -12368,6 +12446,17 @@ export class SearchApi extends BaseAPI {
|
||||||
public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
|
public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
|
||||||
return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
|
return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SearchApiSearchPersonRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof SearchApi
|
||||||
|
*/
|
||||||
|
public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig) {
|
||||||
|
return SearchApiFp(this.configuration).searchPerson(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/SearchApi.md
generated
BIN
mobile/openapi/doc/SearchApi.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api/search_api.dart
generated
BIN
mobile/openapi/lib/api/search_api.dart
generated
Binary file not shown.
BIN
mobile/openapi/test/search_api_test.dart
generated
BIN
mobile/openapi/test/search_api_test.dart
generated
Binary file not shown.
|
@ -3789,6 +3789,50 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/search/person": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "searchPerson",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "name",
|
||||||
|
"required": true,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/PersonResponseDto"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Search"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/server-info": {
|
"/server-info": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getServerInfo",
|
"operationId": "getServerInfo",
|
||||||
|
|
|
@ -22,6 +22,7 @@ export interface IPersonRepository {
|
||||||
getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
|
getAllForUser(userId: string, options: PersonSearchOptions): Promise<PersonEntity[]>;
|
||||||
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
||||||
getById(personId: string): Promise<PersonEntity | null>;
|
getById(personId: string): Promise<PersonEntity | null>;
|
||||||
|
getByName(userId: string, personName: string): Promise<PersonEntity[]>;
|
||||||
|
|
||||||
getAssets(personId: string): Promise<AssetEntity[]>;
|
getAssets(personId: string): Promise<AssetEntity[]>;
|
||||||
prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
|
prepareReassignFaces(data: UpdateFacesData): Promise<string[]>;
|
||||||
|
|
|
@ -85,3 +85,9 @@ export class SearchDto {
|
||||||
@Transform(toBoolean)
|
@Transform(toBoolean)
|
||||||
motion?: boolean;
|
motion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class SearchPeopleDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
name!: string;
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { AssetResponseDto, mapAsset } from '../asset';
|
||||||
import { AuthUserDto } from '../auth';
|
import { AuthUserDto } from '../auth';
|
||||||
import { usePagination } from '../domain.util';
|
import { usePagination } from '../domain.util';
|
||||||
import { IAssetFaceJob, IBulkEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
import { IAssetFaceJob, IBulkEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
||||||
|
import { PersonResponseDto } from '../person/person.dto';
|
||||||
import {
|
import {
|
||||||
AssetFaceId,
|
AssetFaceId,
|
||||||
IAlbumRepository,
|
IAlbumRepository,
|
||||||
|
@ -21,7 +22,7 @@ import {
|
||||||
SearchStrategy,
|
SearchStrategy,
|
||||||
} from '../repositories';
|
} from '../repositories';
|
||||||
import { FeatureFlag, SystemConfigCore } from '../system-config';
|
import { FeatureFlag, SystemConfigCore } from '../system-config';
|
||||||
import { SearchDto } from './dto';
|
import { SearchDto, SearchPeopleDto } from './dto';
|
||||||
import { SearchResponseDto } from './response-dto';
|
import { SearchResponseDto } from './response-dto';
|
||||||
|
|
||||||
interface SyncQueue {
|
interface SyncQueue {
|
||||||
|
@ -158,6 +159,10 @@ export class SearchService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async searchPerson(authUser: AuthUserDto, dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
||||||
|
return await this.personRepository.getByName(authUser.id, dto.name);
|
||||||
|
}
|
||||||
|
|
||||||
async handleIndexAlbums() {
|
async handleIndexAlbums() {
|
||||||
if (!this.enabled) {
|
if (!this.enabled) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
import { AuthUserDto, SearchDto, SearchExploreResponseDto, SearchResponseDto, SearchService } from '@app/domain';
|
import {
|
||||||
|
AuthUserDto,
|
||||||
|
PersonResponseDto,
|
||||||
|
SearchDto,
|
||||||
|
SearchExploreResponseDto,
|
||||||
|
SearchPeopleDto,
|
||||||
|
SearchResponseDto,
|
||||||
|
SearchService,
|
||||||
|
} from '@app/domain';
|
||||||
import { Controller, Get, Query } from '@nestjs/common';
|
import { Controller, Get, Query } from '@nestjs/common';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from '@nestjs/swagger';
|
||||||
import { AuthUser, Authenticated } from '../app.guard';
|
import { AuthUser, Authenticated } from '../app.guard';
|
||||||
|
@ -11,6 +19,11 @@ import { UseValidation } from '../app.utils';
|
||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private service: SearchService) {}
|
constructor(private service: SearchService) {}
|
||||||
|
|
||||||
|
@Get('person')
|
||||||
|
searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise<PersonResponseDto[]> {
|
||||||
|
return this.service.searchPerson(authUser, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise<SearchResponseDto> {
|
||||||
return this.service.search(authUser, dto);
|
return this.service.search(authUser, dto);
|
||||||
|
|
|
@ -95,6 +95,16 @@ export class PersonRepository implements IPersonRepository {
|
||||||
return this.personRepository.findOne({ where: { id: personId } });
|
return this.personRepository.findOne({ where: { id: personId } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getByName(userId: string, personName: string): Promise<PersonEntity[]> {
|
||||||
|
return this.personRepository
|
||||||
|
.createQueryBuilder('person')
|
||||||
|
.leftJoin('person.faces', 'face')
|
||||||
|
.where('person.ownerId = :userId', { userId })
|
||||||
|
.andWhere('LOWER(person.name) LIKE :name', { name: `${personName.toLowerCase()}%` })
|
||||||
|
.limit(20)
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
getAssets(personId: string): Promise<AssetEntity[]> {
|
getAssets(personId: string): Promise<AssetEntity[]> {
|
||||||
return this.assetRepository.find({
|
return this.assetRepository.find({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -9,6 +9,8 @@ export const newPersonRepositoryMock = (): jest.Mocked<IPersonRepository> => {
|
||||||
getAssets: jest.fn(),
|
getAssets: jest.fn(),
|
||||||
getAllWithoutFaces: jest.fn(),
|
getAllWithoutFaces: jest.fn(),
|
||||||
|
|
||||||
|
getByName: jest.fn(),
|
||||||
|
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
deleteAll: jest.fn(),
|
deleteAll: jest.fn(),
|
||||||
|
|
89
web/src/api/open-api/api.ts
generated
89
web/src/api/open-api/api.ts
generated
|
@ -12139,6 +12139,51 @@ export const SearchApiAxiosParamCreator = function (configuration?: Configuratio
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: toPathString(localVarUrlObj),
|
||||||
|
options: localVarRequestOptions,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
searchPerson: async (name: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||||
|
// verify required parameter 'name' is not null or undefined
|
||||||
|
assertParamExists('searchPerson', 'name', name)
|
||||||
|
const localVarPath = `/search/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)
|
||||||
|
|
||||||
|
if (name !== undefined) {
|
||||||
|
localVarQueryParameter['name'] = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||||
|
@ -12192,6 +12237,16 @@ export const SearchApiFp = function(configuration?: Configuration) {
|
||||||
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
|
const localVarAxiosArgs = await localVarAxiosParamCreator.search(q, query, clip, type, isFavorite, isArchived, exifInfoCity, exifInfoState, exifInfoCountry, exifInfoMake, exifInfoModel, exifInfoProjectionType, smartInfoObjects, smartInfoTags, recent, motion, options);
|
||||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} name
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
async searchPerson(name: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> {
|
||||||
|
const localVarAxiosArgs = await localVarAxiosParamCreator.searchPerson(name, options);
|
||||||
|
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12219,6 +12274,15 @@ export const SearchApiFactory = function (configuration?: Configuration, basePat
|
||||||
search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
|
search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig): AxiosPromise<SearchResponseDto> {
|
||||||
return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
|
return localVarFp.search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(axios, basePath));
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SearchApiSearchPersonRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
*/
|
||||||
|
searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> {
|
||||||
|
return localVarFp.searchPerson(requestParameters.name, options).then((request) => request(axios, basePath));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12341,6 +12405,20 @@ export interface SearchApiSearchRequest {
|
||||||
readonly motion?: boolean
|
readonly motion?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request parameters for searchPerson operation in SearchApi.
|
||||||
|
* @export
|
||||||
|
* @interface SearchApiSearchPersonRequest
|
||||||
|
*/
|
||||||
|
export interface SearchApiSearchPersonRequest {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {string}
|
||||||
|
* @memberof SearchApiSearchPerson
|
||||||
|
*/
|
||||||
|
readonly name: string
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchApi - object-oriented interface
|
* SearchApi - object-oriented interface
|
||||||
* @export
|
* @export
|
||||||
|
@ -12368,6 +12446,17 @@ export class SearchApi extends BaseAPI {
|
||||||
public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
|
public search(requestParameters: SearchApiSearchRequest = {}, options?: AxiosRequestConfig) {
|
||||||
return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
|
return SearchApiFp(this.configuration).search(requestParameters.q, requestParameters.query, requestParameters.clip, requestParameters.type, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.exifInfoCity, requestParameters.exifInfoState, requestParameters.exifInfoCountry, requestParameters.exifInfoMake, requestParameters.exifInfoModel, requestParameters.exifInfoProjectionType, requestParameters.smartInfoObjects, requestParameters.smartInfoTags, requestParameters.recent, requestParameters.motion, options).then((request) => request(this.axios, this.basePath));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {SearchApiSearchPersonRequest} requestParameters Request parameters.
|
||||||
|
* @param {*} [options] Override http request option.
|
||||||
|
* @throws {RequiredError}
|
||||||
|
* @memberof SearchApi
|
||||||
|
*/
|
||||||
|
public searchPerson(requestParameters: SearchApiSearchPersonRequest, options?: AxiosRequestConfig) {
|
||||||
|
return SearchApiFp(this.configuration).searchPerson(requestParameters.name, options).then((request) => request(this.axios, this.basePath));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher, onMount } from 'svelte';
|
||||||
import { api, type PersonResponseDto } from '@api';
|
import { api, type PersonResponseDto } from '@api';
|
||||||
import FaceThumbnail from './face-thumbnail.svelte';
|
import FaceThumbnail from './face-thumbnail.svelte';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
import SwapHorizontal from 'svelte-material-icons/SwapHorizontal.svelte';
|
import SwapHorizontal from 'svelte-material-icons/SwapHorizontal.svelte';
|
||||||
|
|
||||||
export let person: PersonResponseDto;
|
export let person: PersonResponseDto;
|
||||||
export let people: PersonResponseDto[];
|
let people: PersonResponseDto[];
|
||||||
let selectedPeople: PersonResponseDto[] = [];
|
let selectedPeople: PersonResponseDto[] = [];
|
||||||
let screenHeight: number;
|
let screenHeight: number;
|
||||||
let isShowConfirmation = false;
|
let isShowConfirmation = false;
|
||||||
|
@ -28,6 +28,11 @@
|
||||||
(source) => !selectedPeople.some((selected) => selected.id === source.id) && source.id !== person.id,
|
(source) => !selectedPeople.some((selected) => selected.id === source.id) && source.id !== person.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
const { data } = await api.personApi.getAllPeople({ withHidden: false });
|
||||||
|
people = data.people;
|
||||||
|
});
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
dispatch('go-back');
|
dispatch('go-back');
|
||||||
};
|
};
|
||||||
|
|
|
@ -254,9 +254,15 @@
|
||||||
if (!edittingPerson || personName === edittingPerson.name) {
|
if (!edittingPerson || personName === edittingPerson.name) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (personName === '') {
|
||||||
|
changeName();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { data } = await api.searchApi.searchPerson({ name: personName });
|
||||||
|
|
||||||
// We check if another person has the same name as the name entered by the user
|
// We check if another person has the same name as the name entered by the user
|
||||||
|
|
||||||
const existingPerson = people.find(
|
const existingPerson = data.find(
|
||||||
(person: PersonResponseDto) =>
|
(person: PersonResponseDto) =>
|
||||||
person.name.toLowerCase() === personName.toLowerCase() &&
|
person.name.toLowerCase() === personName.toLowerCase() &&
|
||||||
edittingPerson &&
|
edittingPerson &&
|
||||||
|
|
|
@ -9,12 +9,10 @@ export const load = (async ({ locals, parent, params }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: person } = await locals.api.personApi.getPerson({ id: params.personId });
|
const { data: person } = await locals.api.personApi.getPerson({ id: params.personId });
|
||||||
const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: false });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
person,
|
person,
|
||||||
people,
|
|
||||||
meta: {
|
meta: {
|
||||||
title: person.name || 'Person',
|
title: person.name || 'Person',
|
||||||
},
|
},
|
||||||
|
|
|
@ -35,6 +35,8 @@
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { clickOutside } from '$lib/utils/click-outside';
|
import { clickOutside } from '$lib/utils/click-outside';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
|
@ -61,7 +63,7 @@
|
||||||
let isEditingName = false;
|
let isEditingName = false;
|
||||||
let previousRoute: string = AppRoute.EXPLORE;
|
let previousRoute: string = AppRoute.EXPLORE;
|
||||||
let previousPersonId: string = data.person.id;
|
let previousPersonId: string = data.person.id;
|
||||||
let people = data.people.people;
|
let people: PersonResponseDto[];
|
||||||
let personMerge1: PersonResponseDto;
|
let personMerge1: PersonResponseDto;
|
||||||
let personMerge2: PersonResponseDto;
|
let personMerge2: PersonResponseDto;
|
||||||
let potentialMergePeople: PersonResponseDto[] = [];
|
let potentialMergePeople: PersonResponseDto[] = [];
|
||||||
|
@ -74,20 +76,58 @@
|
||||||
let name: string = data.person.name;
|
let name: string = data.person.name;
|
||||||
let suggestedPeople: PersonResponseDto[] = [];
|
let suggestedPeople: PersonResponseDto[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the word used to search people name: for example,
|
||||||
|
* if searching 'r' and the server returns 15 people with names starting with 'r',
|
||||||
|
* there's no need to search again people with name starting with 'ri'.
|
||||||
|
* However, it needs to make a new api request if searching 'r' returns 20 names (arbitrary value, the limit sent back by the server).
|
||||||
|
* or if the new search word starts with another word / letter
|
||||||
|
**/
|
||||||
|
let searchWord: string;
|
||||||
|
let maxPeople = false;
|
||||||
|
let isSearchingPeople = false;
|
||||||
|
|
||||||
|
const searchPeople = async () => {
|
||||||
|
isSearchingPeople = true;
|
||||||
|
people = [];
|
||||||
|
try {
|
||||||
|
const { data } = await api.searchApi.searchPerson({ name });
|
||||||
|
people = data;
|
||||||
|
searchWord = name;
|
||||||
|
if (data.length < 20) {
|
||||||
|
maxPeople = false;
|
||||||
|
} else {
|
||||||
|
maxPeople = true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, "Can't search people");
|
||||||
|
}
|
||||||
|
|
||||||
|
isSearchingPeople = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (name !== '' && browser) {
|
||||||
|
if (maxPeople === true || (!name.startsWith(searchWord) && maxPeople === false)) searchPeople();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
|
$: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
|
||||||
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
|
$: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
|
||||||
$: $onPersonThumbnail === data.person.id &&
|
$: $onPersonThumbnail === data.person.id &&
|
||||||
(thumbnailData = api.getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`);
|
(thumbnailData = api.getPeopleThumbnailUrl(data.person.id) + `?now=${Date.now()}`);
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
suggestedPeople = !name
|
if (people) {
|
||||||
? []
|
suggestedPeople = !name
|
||||||
: people
|
? []
|
||||||
.filter(
|
: people
|
||||||
(person: PersonResponseDto) =>
|
.filter(
|
||||||
person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== data.person.id,
|
(person: PersonResponseDto) =>
|
||||||
)
|
person.name.toLowerCase().startsWith(name.toLowerCase()) && person.id !== data.person.id,
|
||||||
.slice(0, 5);
|
)
|
||||||
|
.slice(0, 5);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -199,18 +239,11 @@
|
||||||
try {
|
try {
|
||||||
isEditingName = false;
|
isEditingName = false;
|
||||||
|
|
||||||
const { data: updatedPerson } = await api.personApi.updatePerson({
|
await api.personApi.updatePerson({
|
||||||
id: data.person.id,
|
id: data.person.id,
|
||||||
personUpdateDto: { name: personName },
|
personUpdateDto: { name: personName },
|
||||||
});
|
});
|
||||||
|
|
||||||
people = people.map((person: PersonResponseDto) => {
|
|
||||||
if (person.id === updatedPerson.id) {
|
|
||||||
return updatedPerson;
|
|
||||||
}
|
|
||||||
return person;
|
|
||||||
});
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Change name succesfully',
|
message: 'Change name succesfully',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
|
@ -235,15 +268,21 @@
|
||||||
if (data.person.name === personName) {
|
if (data.person.name === personName) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (name === '') {
|
||||||
|
changeName();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const existingPerson = people.find(
|
const result = await api.searchApi.searchPerson({ name: personName });
|
||||||
|
|
||||||
|
const existingPerson = result.data.find(
|
||||||
(person: PersonResponseDto) =>
|
(person: PersonResponseDto) =>
|
||||||
person.name.toLowerCase() === personName.toLowerCase() && person.id !== data.person.id && person.name,
|
person.name.toLowerCase() === personName.toLowerCase() && person.id !== data.person.id && person.name,
|
||||||
);
|
);
|
||||||
if (existingPerson) {
|
if (existingPerson) {
|
||||||
personMerge2 = existingPerson;
|
personMerge2 = existingPerson;
|
||||||
personMerge1 = data.person;
|
personMerge1 = data.person;
|
||||||
potentialMergePeople = people
|
potentialMergePeople = result.data
|
||||||
.filter(
|
.filter(
|
||||||
(person: PersonResponseDto) =>
|
(person: PersonResponseDto) =>
|
||||||
personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
|
personMerge2.name.toLowerCase() === person.name.toLowerCase() &&
|
||||||
|
@ -310,7 +349,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if viewMode === ViewMode.MERGE_FACES}
|
{#if viewMode === ViewMode.MERGE_FACES}
|
||||||
<MergeFaceSelector person={data.person} bind:people on:go-back={handleGoBack} on:merge={handleMerge} />
|
<MergeFaceSelector person={data.person} on:go-back={handleGoBack} on:merge={handleMerge} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
|
@ -374,7 +413,7 @@
|
||||||
{#if isEditingName}
|
{#if isEditingName}
|
||||||
<EditNameInput
|
<EditNameInput
|
||||||
person={data.person}
|
person={data.person}
|
||||||
suggestedPeople={suggestedPeople.length > 0}
|
suggestedPeople={suggestedPeople.length > 0 || isSearchingPeople}
|
||||||
bind:name
|
bind:name
|
||||||
on:change={(event) => handleNameChange(event.detail)}
|
on:change={(event) => handleNameChange(event.detail)}
|
||||||
/>
|
/>
|
||||||
|
@ -406,25 +445,35 @@
|
||||||
</section>
|
</section>
|
||||||
{#if isEditingName}
|
{#if isEditingName}
|
||||||
<div class="absolute z-[999] w-96">
|
<div class="absolute z-[999] w-96">
|
||||||
{#each suggestedPeople as person, index (person.id)}
|
{#if isSearchingPeople}
|
||||||
<div
|
<div
|
||||||
class="flex {index === suggestedPeople.length - 1
|
class="flex rounded-b-lg dark:border-immich-dark-gray place-items-center bg-gray-100 p-2 dark:bg-gray-700"
|
||||||
? 'rounded-b-lg'
|
|
||||||
: 'border-b dark:border-immich-dark-gray'} place-items-center bg-gray-100 p-2 dark:bg-gray-700"
|
|
||||||
>
|
>
|
||||||
<button class="flex w-full place-items-center" on:click={() => handleSuggestPeople(person)}>
|
<div class="flex w-full place-items-center">
|
||||||
<ImageThumbnail
|
<LoadingSpinner />
|
||||||
circle
|
</div>
|
||||||
shadow
|
|
||||||
url={api.getPeopleThumbnailUrl(person.id)}
|
|
||||||
altText={person.name}
|
|
||||||
widthStyle="2rem"
|
|
||||||
heightStyle="2rem"
|
|
||||||
/>
|
|
||||||
<p class="ml-4 text-gray-700 dark:text-gray-100">{person.name}</p>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{:else}
|
||||||
|
{#each suggestedPeople as person, index (person.id)}
|
||||||
|
<div
|
||||||
|
class="flex {index === suggestedPeople.length - 1
|
||||||
|
? 'rounded-b-lg'
|
||||||
|
: 'border-b dark:border-immich-dark-gray'} place-items-center bg-gray-100 p-2 dark:bg-gray-700"
|
||||||
|
>
|
||||||
|
<button class="flex w-full place-items-center" on:click={() => handleSuggestPeople(person)}>
|
||||||
|
<ImageThumbnail
|
||||||
|
circle
|
||||||
|
shadow
|
||||||
|
url={api.getPeopleThumbnailUrl(person.id)}
|
||||||
|
altText={person.name}
|
||||||
|
widthStyle="2rem"
|
||||||
|
heightStyle="2rem"
|
||||||
|
/>
|
||||||
|
<p class="ml-4 text-gray-700 dark:text-gray-100">{person.name}</p>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue