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 475 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
1
mobile/openapi/README.md
generated
1
mobile/openapi/README.md
generated
|
@ -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 |
|
||||||
|
|
56
mobile/openapi/doc/SearchApi.md
generated
56
mobile/openapi/doc/SearchApi.md
generated
|
@ -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)
|
||||||
|
|
||||||
|
|
52
mobile/openapi/lib/api/search_api.dart
generated
52
mobile/openapi/lib/api/search_api.dart
generated
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
5
mobile/openapi/test/search_api_test.dart
generated
5
mobile/openapi/test/search_api_test.dart
generated
|
@ -27,5 +27,10 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Future<List<PersonResponseDto>> searchPerson(String name) async
|
||||||
|
test('test searchPerson', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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