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:
martin 2023-10-10 16:34:25 +02:00 committed by GitHub
parent f36c40bc6b
commit b8d6cc1e09
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 475 additions and 44 deletions

View file

@ -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));
}
} }

View file

@ -154,6 +154,7 @@ Class | Method | HTTP request | Description
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search | *SearchApi* | [**search**](doc//SearchApi.md#search) | **GET** /search |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config | *ServerInfoApi* | [**getServerConfig**](doc//ServerInfoApi.md#getserverconfig) | **GET** /server-info/config |
*ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features | *ServerInfoApi* | [**getServerFeatures**](doc//ServerInfoApi.md#getserverfeatures) | **GET** /server-info/features |
*ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info | *ServerInfoApi* | [**getServerInfo**](doc//ServerInfoApi.md#getserverinfo) | **GET** /server-info |

View file

@ -11,6 +11,7 @@ Method | HTTP request | Description
------------- | ------------- | ------------- ------------- | ------------- | -------------
[**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore | [**getExploreData**](SearchApi.md#getexploredata) | **GET** /search/explore |
[**search**](SearchApi.md#search) | **GET** /search | [**search**](SearchApi.md#search) | **GET** /search |
[**searchPerson**](SearchApi.md#searchperson) | **GET** /search/person |
# **getExploreData** # **getExploreData**
@ -149,3 +150,58 @@ Name | Type | Description | Notes
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
# **searchPerson**
> List<PersonResponseDto> searchPerson(name)
### Example
```dart
import 'package:openapi/api.dart';
// TODO Configure API key authorization: cookie
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
// TODO Configure API key authorization: api_key
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
// TODO Configure HTTP Bearer authorization: bearer
// Case 1. Use String Token
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
// Case 2. Use Function which generate token.
// String yourTokenGeneratorFunction() { ... }
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
final api_instance = SearchApi();
final name = name_example; // String |
try {
final result = api_instance.searchPerson(name);
print(result);
} catch (e) {
print('Exception when calling SearchApi->searchPerson: $e\n');
}
```
### Parameters
Name | Type | Description | Notes
------------- | ------------- | ------------- | -------------
**name** | **String**| |
### Return type
[**List<PersonResponseDto>**](PersonResponseDto.md)
### Authorization
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
### HTTP request headers
- **Content-Type**: Not defined
- **Accept**: application/json
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)

View file

@ -215,4 +215,56 @@ class SearchApi {
} }
return null; return null;
} }
/// Performs an HTTP 'GET /search/person' operation and returns the [Response].
/// Parameters:
///
/// * [String] name (required):
Future<Response> searchPersonWithHttpInfo(String name,) async {
// ignore: prefer_const_declarations
final path = r'/search/person';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
queryParams.addAll(_queryParams('', 'name', name));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [String] name (required):
Future<List<PersonResponseDto>?> searchPerson(String name,) async {
final response = await searchPersonWithHttpInfo(name,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<PersonResponseDto>') as List)
.cast<PersonResponseDto>()
.toList();
}
return null;
}
} }

View file

@ -27,5 +27,10 @@ void main() {
// TODO // TODO
}); });
//Future<List<PersonResponseDto>> searchPerson(String name) async
test('test searchPerson', () async {
// TODO
});
}); });
} }

View file

@ -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",

View file

@ -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[]>;

View file

@ -85,3 +85,9 @@ export class SearchDto {
@Transform(toBoolean) @Transform(toBoolean)
motion?: boolean; motion?: boolean;
} }
export class SearchPeopleDto {
@IsString()
@IsNotEmpty()
name!: string;
}

View file

@ -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;

View file

@ -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);

View file

@ -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: {

View file

@ -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(),

View file

@ -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));
}
} }

View file

@ -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');
}; };

View file

@ -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 &&

View file

@ -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',
}, },

View file

@ -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>