You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(server,web): hide faces (#3262)
* feat: hide faces * fix: types * pr feedback * fix: svelte checks * feat: new server endpoint * refactor: rename person count dto * fix(server): linter * fix: remove duplicate button * docs: add comments * pr feedback * fix: get unhidden faces * fix: do not use PersonCountResponseDto * fix: transition * pr feedback * pr feedback * fix: remove unused check * add server tests * rename persons to people * feat: add exit button * pr feedback * add server tests * pr feedback * pr feedback * fix: show & hide faces * simplify * fix: close button * pr feeback * pr feeback --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
		
							
								
								
									
										73
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										73
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1777,6 +1777,31 @@ export interface OAuthConfigResponseDto { | ||||
|      */ | ||||
|     'autoLaunch'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface PeopleResponseDto | ||||
|  */ | ||||
| export interface PeopleResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof PeopleResponseDto | ||||
|      */ | ||||
|     'total': number; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof PeopleResponseDto | ||||
|      */ | ||||
|     'visible': number; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<PersonResponseDto>} | ||||
|      * @memberof PeopleResponseDto | ||||
|      */ | ||||
|     'people': Array<PersonResponseDto>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -1801,6 +1826,12 @@ export interface PersonResponseDto { | ||||
|      * @memberof PersonResponseDto | ||||
|      */ | ||||
|     'thumbnailPath': string; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof PersonResponseDto | ||||
|      */ | ||||
|     'isHidden': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -1820,6 +1851,12 @@ export interface PersonUpdateDto { | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|     'featureFaceAssetId'?: string; | ||||
|     /** | ||||
|      * Person visibility | ||||
|      * @type {boolean} | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|     'isHidden'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -8644,10 +8681,11 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {boolean} [withHidden]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllPeople: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getAllPeople: async (withHidden?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             const localVarPath = `/person`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
| @@ -8669,6 +8707,10 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
|             if (withHidden !== undefined) { | ||||
|                 localVarQueryParameter['withHidden'] = withHidden; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
| @@ -8914,11 +8956,12 @@ export const PersonApiFp = function(configuration?: Configuration) { | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {boolean} [withHidden]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getAllPeople(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(options); | ||||
|         async getAllPeople(withHidden?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PeopleResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(withHidden, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -8985,11 +9028,12 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllPeople(options?: AxiosRequestConfig): AxiosPromise<Array<PersonResponseDto>> { | ||||
|             return localVarFp.getAllPeople(options).then((request) => request(axios, basePath)); | ||||
|         getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: AxiosRequestConfig): AxiosPromise<PeopleResponseDto> { | ||||
|             return localVarFp.getAllPeople(requestParameters.withHidden, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -9039,6 +9083,20 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for getAllPeople operation in PersonApi. | ||||
|  * @export | ||||
|  * @interface PersonApiGetAllPeopleRequest | ||||
|  */ | ||||
| export interface PersonApiGetAllPeopleRequest { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof PersonApiGetAllPeople | ||||
|      */ | ||||
|     readonly withHidden?: boolean | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for getPerson operation in PersonApi. | ||||
|  * @export | ||||
| @@ -9132,12 +9190,13 @@ export interface PersonApiUpdatePersonRequest { | ||||
| export class PersonApi extends BaseAPI { | ||||
|     /** | ||||
|      *  | ||||
|      * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof PersonApi | ||||
|      */ | ||||
|     public getAllPeople(options?: AxiosRequestConfig) { | ||||
|         return PersonApiFp(this.configuration).getAllPeople(options).then((request) => request(this.axios, this.basePath)); | ||||
|     public getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: AxiosRequestConfig) { | ||||
|         return PersonApiFp(this.configuration).getAllPeople(requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
| @@ -18,7 +18,8 @@ class PersonService { | ||||
|  | ||||
|   Future<List<PersonResponseDto>?> getCuratedPeople() async { | ||||
|     try { | ||||
|       return await _apiService.personApi.getAllPeople(); | ||||
|       final peopleResponseDto = await _apiService.personApi.getAllPeople(); | ||||
|       return peopleResponseDto?.people; | ||||
|     } catch (e) { | ||||
|       debugPrint("Error [getCuratedPeople] ${e.toString()}"); | ||||
|       return null; | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -71,6 +71,7 @@ doc/OAuthCallbackDto.md | ||||
| doc/OAuthConfigDto.md | ||||
| doc/OAuthConfigResponseDto.md | ||||
| doc/PartnerApi.md | ||||
| doc/PeopleResponseDto.md | ||||
| doc/PersonApi.md | ||||
| doc/PersonResponseDto.md | ||||
| doc/PersonUpdateDto.md | ||||
| @@ -208,6 +209,7 @@ lib/model/merge_person_dto.dart | ||||
| lib/model/o_auth_callback_dto.dart | ||||
| lib/model/o_auth_config_dto.dart | ||||
| lib/model/o_auth_config_response_dto.dart | ||||
| lib/model/people_response_dto.dart | ||||
| lib/model/person_response_dto.dart | ||||
| lib/model/person_update_dto.dart | ||||
| lib/model/queue_status_dto.dart | ||||
| @@ -322,6 +324,7 @@ test/o_auth_callback_dto_test.dart | ||||
| test/o_auth_config_dto_test.dart | ||||
| test/o_auth_config_response_dto_test.dart | ||||
| test/partner_api_test.dart | ||||
| test/people_response_dto_test.dart | ||||
| test/person_api_test.dart | ||||
| test/person_response_dto_test.dart | ||||
| test/person_update_dto_test.dart | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -238,6 +238,7 @@ Class | Method | HTTP request | Description | ||||
|  - [OAuthCallbackDto](doc//OAuthCallbackDto.md) | ||||
|  - [OAuthConfigDto](doc//OAuthConfigDto.md) | ||||
|  - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md) | ||||
|  - [PeopleResponseDto](doc//PeopleResponseDto.md) | ||||
|  - [PersonResponseDto](doc//PersonResponseDto.md) | ||||
|  - [PersonUpdateDto](doc//PersonUpdateDto.md) | ||||
|  - [QueueStatusDto](doc//QueueStatusDto.md) | ||||
|   | ||||
							
								
								
									
										17
									
								
								mobile/openapi/doc/PeopleResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								mobile/openapi/doc/PeopleResponseDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| # openapi.model.PeopleResponseDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **total** | **num** |  |  | ||||
| **visible** | **num** |  |  | ||||
| **people** | [**List<PersonResponseDto>**](PersonResponseDto.md) |  | [default to const []] | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										12
									
								
								mobile/openapi/doc/PersonApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/doc/PersonApi.md
									
									
									
										generated
									
									
									
								
							| @@ -18,7 +18,7 @@ Method | HTTP request | Description | ||||
| 
 | ||||
| 
 | ||||
| # **getAllPeople** | ||||
| > List<PersonResponseDto> getAllPeople() | ||||
| > PeopleResponseDto getAllPeople(withHidden) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -41,9 +41,10 @@ import 'package:openapi/api.dart'; | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = PersonApi(); | ||||
| final withHidden = true; // bool |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.getAllPeople(); | ||||
|     final result = api_instance.getAllPeople(withHidden); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling PersonApi->getAllPeople: $e\n'); | ||||
| @@ -51,11 +52,14 @@ try { | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| This endpoint does not need any parameter. | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **withHidden** | **bool**|  | [optional] [default to false] | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| [**List<PersonResponseDto>**](PersonResponseDto.md) | ||||
| [**PeopleResponseDto**](PeopleResponseDto.md) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/PersonResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/PersonResponseDto.md
									
									
									
										generated
									
									
									
								
							| @@ -11,6 +11,7 @@ Name | Type | Description | Notes | ||||
| **id** | **String** |  |  | ||||
| **name** | **String** |  |  | ||||
| **thumbnailPath** | **String** |  |  | ||||
| **isHidden** | **bool** |  |  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/PersonUpdateDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/PersonUpdateDto.md
									
									
									
										generated
									
									
									
								
							| @@ -10,6 +10,7 @@ Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **name** | **String** | Person name. | [optional]  | ||||
| **featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]  | ||||
| **isHidden** | **bool** | Person visibility | [optional]  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -104,6 +104,7 @@ part 'model/merge_person_dto.dart'; | ||||
| part 'model/o_auth_callback_dto.dart'; | ||||
| part 'model/o_auth_config_dto.dart'; | ||||
| part 'model/o_auth_config_response_dto.dart'; | ||||
| part 'model/people_response_dto.dart'; | ||||
| part 'model/person_response_dto.dart'; | ||||
| part 'model/person_update_dto.dart'; | ||||
| part 'model/queue_status_dto.dart'; | ||||
|   | ||||
							
								
								
									
										23
									
								
								mobile/openapi/lib/api/person_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								mobile/openapi/lib/api/person_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -17,7 +17,10 @@ class PersonApi { | ||||
|   final ApiClient apiClient; | ||||
| 
 | ||||
|   /// Performs an HTTP 'GET /person' operation and returns the [Response]. | ||||
|   Future<Response> getAllPeopleWithHttpInfo() async { | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [bool] withHidden: | ||||
|   Future<Response> getAllPeopleWithHttpInfo({ bool? withHidden, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/person'; | ||||
| 
 | ||||
| @@ -28,6 +31,10 @@ class PersonApi { | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     if (withHidden != null) { | ||||
|       queryParams.addAll(_queryParams('', 'withHidden', withHidden)); | ||||
|     } | ||||
| 
 | ||||
|     const contentTypes = <String>[]; | ||||
| 
 | ||||
| 
 | ||||
| @@ -42,8 +49,11 @@ class PersonApi { | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   Future<List<PersonResponseDto>?> getAllPeople() async { | ||||
|     final response = await getAllPeopleWithHttpInfo(); | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [bool] withHidden: | ||||
|   Future<PeopleResponseDto?> getAllPeople({ bool? withHidden, }) async { | ||||
|     final response = await getAllPeopleWithHttpInfo( withHidden: withHidden, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
| @@ -51,11 +61,8 @@ class PersonApi { | ||||
|     // 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 await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PeopleResponseDto',) as PeopleResponseDto; | ||||
|      | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -303,6 +303,8 @@ class ApiClient { | ||||
|           return OAuthConfigDto.fromJson(value); | ||||
|         case 'OAuthConfigResponseDto': | ||||
|           return OAuthConfigResponseDto.fromJson(value); | ||||
|         case 'PeopleResponseDto': | ||||
|           return PeopleResponseDto.fromJson(value); | ||||
|         case 'PersonResponseDto': | ||||
|           return PersonResponseDto.fromJson(value); | ||||
|         case 'PersonUpdateDto': | ||||
|   | ||||
							
								
								
									
										114
									
								
								mobile/openapi/lib/model/people_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								mobile/openapi/lib/model/people_response_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class PeopleResponseDto { | ||||
|   /// Returns a new [PeopleResponseDto] instance. | ||||
|   PeopleResponseDto({ | ||||
|     required this.total, | ||||
|     required this.visible, | ||||
|     this.people = const [], | ||||
|   }); | ||||
| 
 | ||||
|   num total; | ||||
| 
 | ||||
|   num visible; | ||||
| 
 | ||||
|   List<PersonResponseDto> people; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is PeopleResponseDto && | ||||
|      other.total == total && | ||||
|      other.visible == visible && | ||||
|      other.people == people; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (total.hashCode) + | ||||
|     (visible.hashCode) + | ||||
|     (people.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'PeopleResponseDto[total=$total, visible=$visible, people=$people]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'total'] = this.total; | ||||
|       json[r'visible'] = this.visible; | ||||
|       json[r'people'] = this.people; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [PeopleResponseDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static PeopleResponseDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return PeopleResponseDto( | ||||
|         total: num.parse('${json[r'total']}'), | ||||
|         visible: num.parse('${json[r'visible']}'), | ||||
|         people: PersonResponseDto.listFromJson(json[r'people']), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<PeopleResponseDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <PeopleResponseDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = PeopleResponseDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, PeopleResponseDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, PeopleResponseDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = PeopleResponseDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of PeopleResponseDto-objects as value to a dart map | ||||
|   static Map<String, List<PeopleResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<PeopleResponseDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = PeopleResponseDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'total', | ||||
|     'visible', | ||||
|     'people', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/person_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/person_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,7 @@ class PersonResponseDto { | ||||
|     required this.id, | ||||
|     required this.name, | ||||
|     required this.thumbnailPath, | ||||
|     required this.isHidden, | ||||
|   }); | ||||
| 
 | ||||
|   String id; | ||||
| @@ -24,27 +25,32 @@ class PersonResponseDto { | ||||
| 
 | ||||
|   String thumbnailPath; | ||||
| 
 | ||||
|   bool isHidden; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is PersonResponseDto && | ||||
|      other.id == id && | ||||
|      other.name == name && | ||||
|      other.thumbnailPath == thumbnailPath; | ||||
|      other.thumbnailPath == thumbnailPath && | ||||
|      other.isHidden == isHidden; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (id.hashCode) + | ||||
|     (name.hashCode) + | ||||
|     (thumbnailPath.hashCode); | ||||
|     (thumbnailPath.hashCode) + | ||||
|     (isHidden.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'PersonResponseDto[id=$id, name=$name, thumbnailPath=$thumbnailPath]'; | ||||
|   String toString() => 'PersonResponseDto[id=$id, name=$name, thumbnailPath=$thumbnailPath, isHidden=$isHidden]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'id'] = this.id; | ||||
|       json[r'name'] = this.name; | ||||
|       json[r'thumbnailPath'] = this.thumbnailPath; | ||||
|       json[r'isHidden'] = this.isHidden; | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -59,6 +65,7 @@ class PersonResponseDto { | ||||
|         id: mapValueOfType<String>(json, r'id')!, | ||||
|         name: mapValueOfType<String>(json, r'name')!, | ||||
|         thumbnailPath: mapValueOfType<String>(json, r'thumbnailPath')!, | ||||
|         isHidden: mapValueOfType<bool>(json, r'isHidden')!, | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
| @@ -109,6 +116,7 @@ class PersonResponseDto { | ||||
|     'id', | ||||
|     'name', | ||||
|     'thumbnailPath', | ||||
|     'isHidden', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										24
									
								
								mobile/openapi/lib/model/person_update_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										24
									
								
								mobile/openapi/lib/model/person_update_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -15,6 +15,7 @@ class PersonUpdateDto { | ||||
|   PersonUpdateDto({ | ||||
|     this.name, | ||||
|     this.featureFaceAssetId, | ||||
|     this.isHidden, | ||||
|   }); | ||||
| 
 | ||||
|   /// Person name. | ||||
| @@ -35,19 +36,30 @@ class PersonUpdateDto { | ||||
|   /// | ||||
|   String? featureFaceAssetId; | ||||
| 
 | ||||
|   /// Person visibility | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isHidden; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is PersonUpdateDto && | ||||
|      other.name == name && | ||||
|      other.featureFaceAssetId == featureFaceAssetId; | ||||
|      other.featureFaceAssetId == featureFaceAssetId && | ||||
|      other.isHidden == isHidden; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (name == null ? 0 : name!.hashCode) + | ||||
|     (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode); | ||||
|     (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) + | ||||
|     (isHidden == null ? 0 : isHidden!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'PersonUpdateDto[name=$name, featureFaceAssetId=$featureFaceAssetId]'; | ||||
|   String toString() => 'PersonUpdateDto[name=$name, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
| @@ -61,6 +73,11 @@ class PersonUpdateDto { | ||||
|     } else { | ||||
|     //  json[r'featureFaceAssetId'] = null; | ||||
|     } | ||||
|     if (this.isHidden != null) { | ||||
|       json[r'isHidden'] = this.isHidden; | ||||
|     } else { | ||||
|     //  json[r'isHidden'] = null; | ||||
|     } | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
| @@ -74,6 +91,7 @@ class PersonUpdateDto { | ||||
|       return PersonUpdateDto( | ||||
|         name: mapValueOfType<String>(json, r'name'), | ||||
|         featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'), | ||||
|         isHidden: mapValueOfType<bool>(json, r'isHidden'), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   | ||||
							
								
								
									
										37
									
								
								mobile/openapi/test/people_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								mobile/openapi/test/people_response_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for PeopleResponseDto | ||||
| void main() { | ||||
|   // final instance = PeopleResponseDto(); | ||||
| 
 | ||||
|   group('test PeopleResponseDto', () { | ||||
|     // num total | ||||
|     test('to test the property `total`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // num visible | ||||
|     test('to test the property `visible`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // List<PersonResponseDto> people (default value: const []) | ||||
|     test('to test the property `people`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										2
									
								
								mobile/openapi/test/person_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/test/person_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -17,7 +17,7 @@ void main() { | ||||
|   // final instance = PersonApi(); | ||||
| 
 | ||||
|   group('tests for PersonApi', () { | ||||
|     //Future<List<PersonResponseDto>> getAllPeople() async | ||||
|     //Future<PeopleResponseDto> getAllPeople({ bool withHidden }) async | ||||
|     test('test getAllPeople', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
| @@ -31,6 +31,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // bool isHidden | ||||
|     test('to test the property `isHidden`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										6
									
								
								mobile/openapi/test/person_update_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								mobile/openapi/test/person_update_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -28,6 +28,12 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // Person visibility | ||||
|     // bool isHidden | ||||
|     test('to test the property `isHidden`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
|   | ||||
| @@ -2509,17 +2509,24 @@ | ||||
|     "/person": { | ||||
|       "get": { | ||||
|         "operationId": "getAllPeople", | ||||
|         "parameters": [], | ||||
|         "parameters": [ | ||||
|           { | ||||
|             "name": "withHidden", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "default": false, | ||||
|               "type": "boolean" | ||||
|             } | ||||
|           } | ||||
|         ], | ||||
|         "responses": { | ||||
|           "200": { | ||||
|             "description": "", | ||||
|             "content": { | ||||
|               "application/json": { | ||||
|                 "schema": { | ||||
|                   "type": "array", | ||||
|                   "items": { | ||||
|                     "$ref": "#/components/schemas/PersonResponseDto" | ||||
|                   } | ||||
|                   "$ref": "#/components/schemas/PeopleResponseDto" | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
| @@ -5877,6 +5884,28 @@ | ||||
|           "passwordLoginEnabled" | ||||
|         ] | ||||
|       }, | ||||
|       "PeopleResponseDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
|           "total": { | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "visible": { | ||||
|             "type": "number" | ||||
|           }, | ||||
|           "people": { | ||||
|             "type": "array", | ||||
|             "items": { | ||||
|               "$ref": "#/components/schemas/PersonResponseDto" | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "total", | ||||
|           "visible", | ||||
|           "people" | ||||
|         ] | ||||
|       }, | ||||
|       "PersonResponseDto": { | ||||
|         "type": "object", | ||||
|         "properties": { | ||||
| @@ -5888,12 +5917,16 @@ | ||||
|           }, | ||||
|           "thumbnailPath": { | ||||
|             "type": "string" | ||||
|           }, | ||||
|           "isHidden": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "id", | ||||
|           "name", | ||||
|           "thumbnailPath" | ||||
|           "thumbnailPath", | ||||
|           "isHidden" | ||||
|         ] | ||||
|       }, | ||||
|       "PersonUpdateDto": { | ||||
| @@ -5906,6 +5939,10 @@ | ||||
|           "featureFaceAssetId": { | ||||
|             "type": "string", | ||||
|             "description": "Asset is used to get the feature face thumbnail." | ||||
|           }, | ||||
|           "isHidden": { | ||||
|             "type": "boolean", | ||||
|             "description": "Person visibility" | ||||
|           } | ||||
|         } | ||||
|       }, | ||||
|   | ||||
| @@ -54,7 +54,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { | ||||
|     smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, | ||||
|     livePhotoVideoId: entity.livePhotoVideoId, | ||||
|     tags: entity.tags?.map(mapTag), | ||||
|     people: entity.faces?.map(mapFace), | ||||
|     people: entity.faces?.map(mapFace).filter((person) => !person.isHidden), | ||||
|     checksum: entity.checksum.toString('base64'), | ||||
|   }; | ||||
| } | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; | ||||
| import { IsOptional, IsString } from 'class-validator'; | ||||
| import { ValidateUUID } from '../domain.util'; | ||||
| import { Transform } from 'class-transformer'; | ||||
| import { IsBoolean, IsOptional, IsString } from 'class-validator'; | ||||
| import { toBoolean, ValidateUUID } from '../domain.util'; | ||||
|  | ||||
| export class PersonUpdateDto { | ||||
|   /** | ||||
| @@ -16,6 +17,13 @@ export class PersonUpdateDto { | ||||
|   @IsOptional() | ||||
|   @IsString() | ||||
|   featureFaceAssetId?: string; | ||||
|  | ||||
|   /** | ||||
|    * Person visibility | ||||
|    */ | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   isHidden?: boolean; | ||||
| } | ||||
|  | ||||
| export class MergePersonDto { | ||||
| @@ -23,10 +31,23 @@ export class MergePersonDto { | ||||
|   ids!: string[]; | ||||
| } | ||||
|  | ||||
| export class PersonSearchDto { | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   withHidden?: boolean = false; | ||||
| } | ||||
|  | ||||
| export class PersonResponseDto { | ||||
|   id!: string; | ||||
|   name!: string; | ||||
|   thumbnailPath!: string; | ||||
|   isHidden!: boolean; | ||||
| } | ||||
|  | ||||
| export class PeopleResponseDto { | ||||
|   total!: number; | ||||
|   visible!: number; | ||||
|   people!: PersonResponseDto[]; | ||||
| } | ||||
|  | ||||
| export function mapPerson(person: PersonEntity): PersonResponseDto { | ||||
| @@ -34,6 +55,7 @@ export function mapPerson(person: PersonEntity): PersonResponseDto { | ||||
|     id: person.id, | ||||
|     name: person.name, | ||||
|     thumbnailPath: person.thumbnailPath, | ||||
|     isHidden: person.isHidden, | ||||
|   }; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -19,6 +19,7 @@ const responseDto: PersonResponseDto = { | ||||
|   id: 'person-1', | ||||
|   name: 'Person 1', | ||||
|   thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|   isHidden: false, | ||||
| }; | ||||
|  | ||||
| describe(PersonService.name, () => { | ||||
| @@ -41,7 +42,37 @@ describe(PersonService.name, () => { | ||||
|   describe('getAll', () => { | ||||
|     it('should get all people with thumbnails', async () => { | ||||
|       personMock.getAll.mockResolvedValue([personStub.withName, personStub.noThumbnail]); | ||||
|       await expect(sut.getAll(authStub.admin)).resolves.toEqual([responseDto]); | ||||
|       await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({ | ||||
|         total: 1, | ||||
|         visible: 1, | ||||
|         people: [responseDto], | ||||
|       }); | ||||
|       expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 }); | ||||
|     }); | ||||
|     it('should get all visible people with thumbnails', async () => { | ||||
|       personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]); | ||||
|       await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({ | ||||
|         total: 2, | ||||
|         visible: 1, | ||||
|         people: [responseDto], | ||||
|       }); | ||||
|       expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 }); | ||||
|     }); | ||||
|     it('should get all hidden and visible people with thumbnails', async () => { | ||||
|       personMock.getAll.mockResolvedValue([personStub.withName, personStub.hidden]); | ||||
|       await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({ | ||||
|         total: 2, | ||||
|         visible: 1, | ||||
|         people: [ | ||||
|           responseDto, | ||||
|           { | ||||
|             id: 'person-1', | ||||
|             name: '', | ||||
|             thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|             isHidden: true, | ||||
|           }, | ||||
|         ], | ||||
|       }); | ||||
|       expect(personMock.getAll).toHaveBeenCalledWith(authStub.admin.id, { minimumFaceCount: 1 }); | ||||
|     }); | ||||
|   }); | ||||
| @@ -111,6 +142,21 @@ describe(PersonService.name, () => { | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it('should update a person visibility', async () => { | ||||
|       personMock.getById.mockResolvedValue(personStub.hidden); | ||||
|       personMock.update.mockResolvedValue(personStub.withName); | ||||
|       personMock.getAssets.mockResolvedValue([assetEntityStub.image]); | ||||
|  | ||||
|       await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); | ||||
|  | ||||
|       expect(personMock.getById).toHaveBeenCalledWith('admin_id', 'person-1'); | ||||
|       expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); | ||||
|       expect(jobMock.queue).toHaveBeenCalledWith({ | ||||
|         name: JobName.SEARCH_INDEX_ASSET, | ||||
|         data: { ids: [assetEntityStub.image.id] }, | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     it("should update a person's thumbnailPath", async () => { | ||||
|       personMock.getById.mockResolvedValue(personStub.withName); | ||||
|       personMock.getFaceById.mockResolvedValue(faceStub.face1); | ||||
|   | ||||
| @@ -4,7 +4,14 @@ import { AuthUserDto } from '../auth'; | ||||
| import { mimeTypes } from '../domain.constant'; | ||||
| import { IJobRepository, JobName } from '../job'; | ||||
| import { ImmichReadStream, IStorageRepository } from '../storage'; | ||||
| import { mapPerson, MergePersonDto, PersonResponseDto, PersonUpdateDto } from './person.dto'; | ||||
| import { | ||||
|   mapPerson, | ||||
|   MergePersonDto, | ||||
|   PeopleResponseDto, | ||||
|   PersonResponseDto, | ||||
|   PersonSearchDto, | ||||
|   PersonUpdateDto, | ||||
| } from './person.dto'; | ||||
| import { IPersonRepository, UpdateFacesData } from './person.repository'; | ||||
|  | ||||
| @Injectable() | ||||
| @@ -17,16 +24,21 @@ export class PersonService { | ||||
|     @Inject(IJobRepository) private jobRepository: IJobRepository, | ||||
|   ) {} | ||||
|  | ||||
|   async getAll(authUser: AuthUserDto): Promise<PersonResponseDto[]> { | ||||
|   async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> { | ||||
|     const people = await this.repository.getAll(authUser.id, { minimumFaceCount: 1 }); | ||||
|     const named = people.filter((person) => !!person.name); | ||||
|     const unnamed = people.filter((person) => !person.name); | ||||
|     return ( | ||||
|       [...named, ...unnamed] | ||||
|         // with thumbnails | ||||
|         .filter((person) => !!person.thumbnailPath) | ||||
|         .map((person) => mapPerson(person)) | ||||
|     ); | ||||
|  | ||||
|     const persons: PersonResponseDto[] = [...named, ...unnamed] | ||||
|       // with thumbnails | ||||
|       .filter((person) => !!person.thumbnailPath) | ||||
|       .map((person) => mapPerson(person)); | ||||
|  | ||||
|     return { | ||||
|       people: persons.filter((person) => dto.withHidden || !person.isHidden), | ||||
|       total: persons.length, | ||||
|       visible: persons.filter((person: PersonResponseDto) => !person.isHidden).length, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   getById(authUser: AuthUserDto, id: string): Promise<PersonResponseDto> { | ||||
| @@ -50,8 +62,8 @@ export class PersonService { | ||||
|   async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> { | ||||
|     let person = await this.findOrFail(authUser, id); | ||||
|  | ||||
|     if (dto.name !== undefined) { | ||||
|       person = await this.repository.update({ id, name: dto.name }); | ||||
|     if (dto.name != undefined || dto.isHidden !== undefined) { | ||||
|       person = await this.repository.update({ id, name: dto.name, isHidden: dto.isHidden }); | ||||
|       const assets = await this.repository.getAssets(authUser.id, id); | ||||
|       const ids = assets.map((asset) => asset.id); | ||||
|       await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } }); | ||||
|   | ||||
| @@ -4,11 +4,13 @@ import { | ||||
|   BulkIdResponseDto, | ||||
|   ImmichReadStream, | ||||
|   MergePersonDto, | ||||
|   PeopleResponseDto, | ||||
|   PersonResponseDto, | ||||
|   PersonSearchDto, | ||||
|   PersonService, | ||||
|   PersonUpdateDto, | ||||
| } from '@app/domain'; | ||||
| import { Body, Controller, Get, Param, Post, Put, StreamableFile } from '@nestjs/common'; | ||||
| import { Body, Controller, Get, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; | ||||
| import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated, AuthUser } from '../app.guard'; | ||||
| import { UseValidation } from '../app.utils'; | ||||
| @@ -26,8 +28,8 @@ export class PersonController { | ||||
|   constructor(private service: PersonService) {} | ||||
|  | ||||
|   @Get() | ||||
|   getAllPeople(@AuthUser() authUser: AuthUserDto): Promise<PersonResponseDto[]> { | ||||
|     return this.service.getAll(authUser); | ||||
|   getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise<PeopleResponseDto> { | ||||
|     return this.service.getAll(authUser, withHidden); | ||||
|   } | ||||
|  | ||||
|   @Get(':id') | ||||
|   | ||||
| @@ -35,4 +35,7 @@ export class PersonEntity { | ||||
|  | ||||
|   @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person) | ||||
|   faces!: AssetFaceEntity[]; | ||||
|  | ||||
|   @Column({ default: false }) | ||||
|   isHidden!: boolean; | ||||
| } | ||||
|   | ||||
							
								
								
									
										14
									
								
								server/src/infra/migrations/1689281196844-AddHiddenFaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/src/infra/migrations/1689281196844-AddHiddenFaces.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import { MigrationInterface, QueryRunner } from "typeorm"; | ||||
|  | ||||
| export class Infra1689281196844 implements MigrationInterface { | ||||
|     name = 'Infra1689281196844' | ||||
|  | ||||
|     public async up(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "person" ADD "isHidden" boolean NOT NULL DEFAULT false`); | ||||
|     } | ||||
|  | ||||
|     public async down(queryRunner: QueryRunner): Promise<void> { | ||||
|         await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "isHidden"`); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -385,7 +385,8 @@ export class TypesenseRepository implements ISearchRepository { | ||||
|       custom = { ...custom, geo: [lat, lng] }; | ||||
|     } | ||||
|  | ||||
|     const people = asset.faces?.map((face) => face.person.name).filter((name) => name) || []; | ||||
|     const people = | ||||
|       asset.faces?.filter((face) => !face.person.isHidden && face.person.name).map((face) => face.person.name) || []; | ||||
|     if (people.length) { | ||||
|       custom = { ...custom, people }; | ||||
|     } | ||||
|   | ||||
| @@ -1094,6 +1094,18 @@ export const personStub = { | ||||
|     name: '', | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   hidden: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
|     createdAt: new Date('2021-01-01'), | ||||
|     updatedAt: new Date('2021-01-01'), | ||||
|     ownerId: userEntityStub.admin.id, | ||||
|     owner: userEntityStub.admin, | ||||
|     name: '', | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: true, | ||||
|   }), | ||||
|   withName: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
| @@ -1104,6 +1116,7 @@ export const personStub = { | ||||
|     name: 'Person 1', | ||||
|     thumbnailPath: '/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   noThumbnail: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
| @@ -1114,6 +1127,7 @@ export const personStub = { | ||||
|     name: '', | ||||
|     thumbnailPath: '', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   newThumbnail: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
| @@ -1124,6 +1138,7 @@ export const personStub = { | ||||
|     name: '', | ||||
|     thumbnailPath: '/new/path/to/thumbnail.jpg', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   primaryPerson: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-1', | ||||
| @@ -1134,6 +1149,7 @@ export const personStub = { | ||||
|     name: 'Person 1', | ||||
|     thumbnailPath: '/path/to/thumbnail', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
|   mergePerson: Object.freeze<PersonEntity>({ | ||||
|     id: 'person-2', | ||||
| @@ -1144,6 +1160,7 @@ export const personStub = { | ||||
|     name: 'Person 2', | ||||
|     thumbnailPath: '/path/to/thumbnail', | ||||
|     faces: [], | ||||
|     isHidden: false, | ||||
|   }), | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										73
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										73
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1777,6 +1777,31 @@ export interface OAuthConfigResponseDto { | ||||
|      */ | ||||
|     'autoLaunch'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface PeopleResponseDto | ||||
|  */ | ||||
| export interface PeopleResponseDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof PeopleResponseDto | ||||
|      */ | ||||
|     'total': number; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {number} | ||||
|      * @memberof PeopleResponseDto | ||||
|      */ | ||||
|     'visible': number; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<PersonResponseDto>} | ||||
|      * @memberof PeopleResponseDto | ||||
|      */ | ||||
|     'people': Array<PersonResponseDto>; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -1801,6 +1826,12 @@ export interface PersonResponseDto { | ||||
|      * @memberof PersonResponseDto | ||||
|      */ | ||||
|     'thumbnailPath': string; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof PersonResponseDto | ||||
|      */ | ||||
|     'isHidden': boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -1820,6 +1851,12 @@ export interface PersonUpdateDto { | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|     'featureFaceAssetId'?: string; | ||||
|     /** | ||||
|      * Person visibility | ||||
|      * @type {boolean} | ||||
|      * @memberof PersonUpdateDto | ||||
|      */ | ||||
|     'isHidden'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
| @@ -8688,10 +8725,11 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {boolean} [withHidden]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllPeople: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getAllPeople: async (withHidden?: boolean, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             const localVarPath = `/person`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
| @@ -8713,6 +8751,10 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
|             if (withHidden !== undefined) { | ||||
|                 localVarQueryParameter['withHidden'] = withHidden; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
| @@ -8958,11 +9000,12 @@ export const PersonApiFp = function(configuration?: Configuration) { | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {boolean} [withHidden]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getAllPeople(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<PersonResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(options); | ||||
|         async getAllPeople(withHidden?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<PeopleResponseDto>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(withHidden, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -9029,11 +9072,12 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat | ||||
|     return { | ||||
|         /** | ||||
|          *  | ||||
|          * @param {boolean} [withHidden]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getAllPeople(options?: any): AxiosPromise<Array<PersonResponseDto>> { | ||||
|             return localVarFp.getAllPeople(options).then((request) => request(axios, basePath)); | ||||
|         getAllPeople(withHidden?: boolean, options?: any): AxiosPromise<PeopleResponseDto> { | ||||
|             return localVarFp.getAllPeople(withHidden, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -9085,6 +9129,20 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for getAllPeople operation in PersonApi. | ||||
|  * @export | ||||
|  * @interface PersonApiGetAllPeopleRequest | ||||
|  */ | ||||
| export interface PersonApiGetAllPeopleRequest { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof PersonApiGetAllPeople | ||||
|      */ | ||||
|     readonly withHidden?: boolean | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for getPerson operation in PersonApi. | ||||
|  * @export | ||||
| @@ -9178,12 +9236,13 @@ export interface PersonApiUpdatePersonRequest { | ||||
| export class PersonApi extends BaseAPI { | ||||
|     /** | ||||
|      *  | ||||
|      * @param {PersonApiGetAllPeopleRequest} requestParameters Request parameters. | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof PersonApi | ||||
|      */ | ||||
|     public getAllPeople(options?: AxiosRequestConfig) { | ||||
|         return PersonApiFp(this.configuration).getAllPeople(options).then((request) => request(this.axios, this.basePath)); | ||||
|     public getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: AxiosRequestConfig) { | ||||
|         return PersonApiFp(this.configuration).getAllPeople(requestParameters.withHidden, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
| @@ -3,6 +3,7 @@ | ||||
|   import { fade } from 'svelte/transition'; | ||||
|   import { thumbHashToDataURL } from 'thumbhash'; | ||||
|   import { Buffer } from 'buffer'; | ||||
|   import EyeOffOutline from 'svelte-material-icons/EyeOffOutline.svelte'; | ||||
|  | ||||
|   export let url: string; | ||||
|   export let altText: string; | ||||
| @@ -12,16 +13,17 @@ | ||||
|   export let curve = false; | ||||
|   export let shadow = false; | ||||
|   export let circle = false; | ||||
|  | ||||
|   export let hidden = false; | ||||
|   let complete = false; | ||||
| </script> | ||||
|  | ||||
| <img | ||||
|   style:width={widthStyle} | ||||
|   style:height={heightStyle} | ||||
|   style:filter={hidden ? 'grayscale(75%)' : 'none'} | ||||
|   src={url} | ||||
|   alt={altText} | ||||
|   class="object-cover transition-opacity duration-300" | ||||
|   class="object-cover transition duration-300" | ||||
|   class:rounded-lg={curve} | ||||
|   class:shadow-lg={shadow} | ||||
|   class:rounded-full={circle} | ||||
| @@ -30,6 +32,11 @@ | ||||
|   use:imageLoad | ||||
|   on:image-load|once={() => (complete = true)} | ||||
| /> | ||||
| {#if hidden} | ||||
|   <div class="absolute top-1/2 left-1/2 transform translate-x-[-50%] translate-y-[-50%]"> | ||||
|     <EyeOffOutline size="2em" /> | ||||
|   </div> | ||||
| {/if} | ||||
|  | ||||
| {#if thumbhash && !complete} | ||||
|   <img | ||||
|   | ||||
| @@ -25,8 +25,8 @@ | ||||
|   $: unselectedPeople = people.filter((source) => !selectedPeople.includes(source) && source.id !== person.id); | ||||
|  | ||||
|   onMount(async () => { | ||||
|     const { data } = await api.personApi.getAllPeople(); | ||||
|     people = data; | ||||
|     const { data } = await api.personApi.getAllPeople({ withHidden: true }); | ||||
|     people = data.people; | ||||
|   }); | ||||
|  | ||||
|   const onClose = () => { | ||||
|   | ||||
| @@ -24,12 +24,12 @@ | ||||
|  | ||||
| <div id="people-card" class="relative"> | ||||
|   <a href="/people/{person.id}" draggable="false"> | ||||
|     <div class="filter brightness-95 rounded-xl w-48"> | ||||
|     <div class="w-48 rounded-xl brightness-95 filter"> | ||||
|       <ImageThumbnail shadow url={api.getPeopleThumbnailUrl(person.id)} altText={person.name} widthStyle="100%" /> | ||||
|     </div> | ||||
|     {#if person.name} | ||||
|       <span | ||||
|         class="absolute bottom-2 w-full text-center font-medium text-white text-ellipsis w-100 px-1 hover:cursor-pointer backdrop-blur-[1px]" | ||||
|         class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center font-medium text-white backdrop-blur-[1px] hover:cursor-pointer" | ||||
|       > | ||||
|         {person.name} | ||||
|       </span> | ||||
| @@ -37,7 +37,7 @@ | ||||
|   </a> | ||||
|  | ||||
|   <button | ||||
|     class="absolute top-2 right-2 z-20" | ||||
|     class="absolute right-2 top-2 z-20" | ||||
|     on:click|stopPropagation|preventDefault={() => { | ||||
|       showContextMenu = !showContextMenu; | ||||
|     }} | ||||
| @@ -59,6 +59,6 @@ | ||||
|  | ||||
| {#if showContextMenu} | ||||
|   <Portal target="body"> | ||||
|     <div class="absolute top-0 left-0 heyo w-screen h-screen bg-transparent z-10" /> | ||||
|     <div class="heyo absolute left-0 top-0 z-10 h-screen w-screen bg-transparent" /> | ||||
|   </Portal> | ||||
| {/if} | ||||
|   | ||||
							
								
								
									
										30
									
								
								web/src/lib/components/faces-page/show-hide.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								web/src/lib/components/faces-page/show-hide.svelte
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <script> | ||||
|   import { fly } from 'svelte/transition'; | ||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
|   import { quintOut } from 'svelte/easing'; | ||||
|   import Close from 'svelte-material-icons/Close.svelte'; | ||||
|   import IconButton from '../elements/buttons/icon-button.svelte'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|  | ||||
|   const dispatch = createEventDispatcher(); | ||||
| </script> | ||||
|  | ||||
| <section | ||||
|   transition:fly={{ y: 500, duration: 100, easing: quintOut }} | ||||
|   class="absolute top-0 left-0 w-full h-full bg-immich-bg dark:bg-immich-dark-bg z-[9999]" | ||||
| > | ||||
|   <div | ||||
|     class="absolute border-b dark:border-immich-dark-gray flex justify-between place-items-center dark:text-immich-dark-fg w-full h-16" | ||||
|   > | ||||
|     <div class="flex items-center justify-between p-8 w-full"> | ||||
|       <div class="flex items-center"> | ||||
|         <CircleIconButton logo={Close} on:click={() => dispatch('closeClick')} /> | ||||
|         <p class="ml-4">Show & hide faces</p> | ||||
|       </div> | ||||
|       <IconButton on:click={() => dispatch('doneClick')}>Done</IconButton> | ||||
|     </div> | ||||
|     <div class="absolute top-16 h-[calc(100%-theme(spacing.16))] w-full immich-scrollbar p-4 pb-8"> | ||||
|       <slot /> | ||||
|     </div> | ||||
|   </div> | ||||
| </section> | ||||
| @@ -16,7 +16,6 @@ | ||||
|  | ||||
|   <slot name="header" /> | ||||
| </header> | ||||
|  | ||||
| <main | ||||
|   class="grid md:grid-cols-[theme(spacing.64)_auto] grid-cols-[theme(spacing.18)_auto] relative pt-[var(--navbar-height)] h-screen overflow-hidden bg-immich-bg dark:bg-immich-dark-bg" | ||||
| > | ||||
|   | ||||
| @@ -9,11 +9,11 @@ export const load = (async ({ locals, parent }) => { | ||||
|   } | ||||
|  | ||||
|   const { data: items } = await locals.api.searchApi.getExploreData(); | ||||
|   const { data: people } = await locals.api.personApi.getAllPeople(); | ||||
|   const { data: response } = await locals.api.personApi.getAllPeople({ withHidden: false }); | ||||
|   return { | ||||
|     user, | ||||
|     items, | ||||
|     people, | ||||
|     response, | ||||
|     meta: { | ||||
|       title: 'Explore', | ||||
|     }, | ||||
|   | ||||
| @@ -19,7 +19,6 @@ | ||||
|   } | ||||
|  | ||||
|   const MAX_ITEMS = 12; | ||||
|  | ||||
|   const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => { | ||||
|     const targetField = items.find((item) => item.fieldName === field); | ||||
|     return targetField?.items || []; | ||||
| @@ -27,21 +26,20 @@ | ||||
|  | ||||
|   $: things = getFieldItems(data.items, Field.OBJECTS); | ||||
|   $: places = getFieldItems(data.items, Field.CITY); | ||||
|   $: people = data.people.slice(0, MAX_ITEMS); | ||||
|   $: people = data.response.people.slice(0, MAX_ITEMS); | ||||
|   $: hasPeople = data.response.total > 0; | ||||
| </script> | ||||
|  | ||||
| <UserPageLayout user={data.user} title={data.meta.title}> | ||||
|   {#if people.length > 0} | ||||
|   {#if hasPeople} | ||||
|     <div class="mb-6 mt-2"> | ||||
|       <div class="flex justify-between"> | ||||
|         <p class="mb-4 dark:text-immich-dark-fg font-medium">People</p> | ||||
|         {#if data.people.length > MAX_ITEMS} | ||||
|           <a | ||||
|             href={AppRoute.PEOPLE} | ||||
|             class="font-medium text-sm pr-4 hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg" | ||||
|             draggable="false">View All</a | ||||
|           > | ||||
|         {/if} | ||||
|         <a | ||||
|           href={AppRoute.PEOPLE} | ||||
|           class="font-medium text-sm pr-4 hover:text-immich-primary dark:hover:text-immich-dark-primary dark:text-immich-dark-fg" | ||||
|           draggable="false">View All</a | ||||
|         > | ||||
|       </div> | ||||
|       <div class="flex flex-row flex-wrap gap-4"> | ||||
|         {#each people as person (person.id)} | ||||
|   | ||||
| @@ -8,8 +8,7 @@ export const load = (async ({ locals, parent }) => { | ||||
|     throw redirect(302, AppRoute.AUTH_LOGIN); | ||||
|   } | ||||
|  | ||||
|   const { data: people } = await locals.api.personApi.getAllPeople(); | ||||
|  | ||||
|   const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: true }); | ||||
|   return { | ||||
|     user, | ||||
|     people, | ||||
|   | ||||
| @@ -6,14 +6,74 @@ | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import Button from '$lib/components/elements/buttons/button.svelte'; | ||||
|   import { api, type PersonResponseDto } from '@api'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { | ||||
|     notificationController, | ||||
|     NotificationType, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { goto } from '$app/navigation'; | ||||
|   import { AppRoute } from '$lib/constants'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { | ||||
|     NotificationType, | ||||
|     notificationController, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import ShowHide from '$lib/components/faces-page/show-hide.svelte'; | ||||
|   import IconButton from '$lib/components/elements/buttons/icon-button.svelte'; | ||||
|   import EyeOutline from 'svelte-material-icons/EyeOutline.svelte'; | ||||
|   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|   let selectHidden = false; | ||||
|   let changeCounter = 0; | ||||
|   let initialHiddenValues: Record<string, boolean> = {}; | ||||
|  | ||||
|   let people = data.people.people; | ||||
|   let countTotalPeople = data.people.total; | ||||
|   let countVisiblePeople = data.people.visible; | ||||
|  | ||||
|   people.forEach((person: PersonResponseDto) => { | ||||
|     initialHiddenValues[person.id] = person.isHidden; | ||||
|   }); | ||||
|  | ||||
|   const handleCloseClick = () => { | ||||
|     selectHidden = false; | ||||
|     people.forEach((person: PersonResponseDto) => { | ||||
|       person.isHidden = initialHiddenValues[person.id]; | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   const handleDoneClick = async () => { | ||||
|     selectHidden = false; | ||||
|     try { | ||||
|       // Reset the counter before checking changes | ||||
|       let changeCounter = 0; | ||||
|  | ||||
|       // Check if the visibility for each person has been changed | ||||
|       for (const person of people) { | ||||
|         if (person.isHidden !== initialHiddenValues[person.id]) { | ||||
|           changeCounter++; | ||||
|           await api.personApi.updatePerson({ | ||||
|             id: person.id, | ||||
|             personUpdateDto: { isHidden: person.isHidden }, | ||||
|           }); | ||||
|  | ||||
|           // Update the initial hidden values | ||||
|           initialHiddenValues[person.id] = person.isHidden; | ||||
|  | ||||
|           // Update the count of hidden/visible people | ||||
|           countVisiblePeople += person.isHidden ? -1 : 1; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (changeCounter > 0) { | ||||
|         notificationController.show({ | ||||
|           type: NotificationType.Info, | ||||
|           message: `Visibility changed for ${changeCounter} ${changeCounter <= 1 ? 'person' : 'people'}`, | ||||
|         }); | ||||
|       } | ||||
|     } catch (error) { | ||||
|       handleError( | ||||
|         error, | ||||
|         `Unable to change the visibility for ${changeCounter} ${changeCounter <= 1 ? 'person' : 'people'}`, | ||||
|       ); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   let showChangeNameModal = false; | ||||
|   let personName = ''; | ||||
| @@ -37,7 +97,7 @@ | ||||
|           personUpdateDto: { name: personName }, | ||||
|         }); | ||||
|  | ||||
|         data.people = data.people.map((person: PersonResponseDto) => { | ||||
|         people = people.map((person: PersonResponseDto) => { | ||||
|           if (person.id === updatedPerson.id) { | ||||
|             return updatedPerson; | ||||
|           } | ||||
| @@ -57,35 +117,48 @@ | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <UserPageLayout user={data.user} showUploadButton title="People"> | ||||
|   <section> | ||||
|     {#if data.people.length > 0} | ||||
|       <div class="pl-4"> | ||||
|         <div class="flex flex-row flex-wrap gap-1"> | ||||
|           {#each data.people as person (person.id)} | ||||
|             <PeopleCard {person} on:change-name={handleChangeName} on:merge-faces={handleMergeFaces} /> | ||||
|           {/each} | ||||
| <UserPageLayout user={data.user} title="People"> | ||||
|   <svelte:fragment slot="buttons"> | ||||
|     {#if countTotalPeople > 0} | ||||
|       <IconButton on:click={() => (selectHidden = !selectHidden)}> | ||||
|         <div class="flex flex-wrap place-items-center justify-center gap-x-1 text-sm"> | ||||
|           <EyeOutline size="18" /> | ||||
|           <p class="ml-2">Show & hide faces</p> | ||||
|         </div> | ||||
|       </div> | ||||
|     {:else} | ||||
|       <div class="flex items-center place-content-center w-full min-h-[calc(66vh_-_11rem)] dark:text-white"> | ||||
|         <div class="flex flex-col content-center items-center text-center"> | ||||
|           <AccountOff size="3.5em" /> | ||||
|           <p class="font-medium text-3xl mt-5">No people</p> | ||||
|         </div> | ||||
|       </div> | ||||
|       </IconButton> | ||||
|     {/if} | ||||
|   </section> | ||||
|   </svelte:fragment> | ||||
|  | ||||
|   {#if countVisiblePeople > 0} | ||||
|     <div class="pl-4"> | ||||
|       <div class="flex flex-row flex-wrap gap-1"> | ||||
|         {#key selectHidden} | ||||
|           {#each people as person (person.id)} | ||||
|             {#if !person.isHidden} | ||||
|               <PeopleCard {person} on:change-name={handleChangeName} on:merge-faces={handleMergeFaces} /> | ||||
|             {/if} | ||||
|           {/each} | ||||
|         {/key} | ||||
|       </div> | ||||
|     </div> | ||||
|   {:else} | ||||
|     <div class="flex min-h-[calc(66vh_-_11rem)] w-full place-content-center items-center dark:text-white"> | ||||
|       <div class="flex flex-col content-center items-center text-center"> | ||||
|         <AccountOff size="3.5em" /> | ||||
|         <p class="mt-5 text-3xl font-medium">No people</p> | ||||
|       </div> | ||||
|     </div> | ||||
|   {/if} | ||||
|  | ||||
|   {#if showChangeNameModal} | ||||
|     <FullScreenModal on:clickOutside={() => (showChangeNameModal = false)}> | ||||
|       <div | ||||
|         class="border bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-3xl py-8 dark:text-immich-dark-fg" | ||||
|         class="bg-immich-bg dark:bg-immich-dark-gray dark:border-immich-dark-gray dark:text-immich-dark-fg w-[500px] max-w-[95vw] rounded-3xl border p-4 py-8 shadow-sm" | ||||
|       > | ||||
|         <div | ||||
|           class="flex flex-col place-items-center place-content-center gap-4 px-4 text-immich-primary dark:text-immich-dark-primary" | ||||
|           class="text-immich-primary dark:text-immich-dark-primary flex flex-col place-content-center place-items-center gap-4 px-4" | ||||
|         > | ||||
|           <h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Change name</h1> | ||||
|           <h1 class="text-immich-primary dark:text-immich-dark-primary text-2xl font-medium">Change name</h1> | ||||
|         </div> | ||||
|  | ||||
|         <form on:submit|preventDefault={submitNameChange} autocomplete="off"> | ||||
| @@ -95,7 +168,7 @@ | ||||
|             <input class="immich-form-input" id="name" name="name" type="text" bind:value={personName} autofocus /> | ||||
|           </div> | ||||
|  | ||||
|           <div class="flex w-full px-4 gap-4 mt-8"> | ||||
|           <div class="mt-8 flex w-full gap-4 px-4"> | ||||
|             <Button | ||||
|               color="gray" | ||||
|               fullwidth | ||||
| @@ -110,3 +183,33 @@ | ||||
|     </FullScreenModal> | ||||
|   {/if} | ||||
| </UserPageLayout> | ||||
| {#if selectHidden} | ||||
|   <ShowHide on:doneClick={handleDoneClick} on:closeClick={handleCloseClick}> | ||||
|     <div class="pl-4"> | ||||
|       <div class="flex flex-row flex-wrap gap-1"> | ||||
|         {#each people as person (person.id)} | ||||
|           <div class="relative"> | ||||
|             <div class="h-48 w-48 rounded-xl brightness-95 filter"> | ||||
|               <button class="h-full w-full" on:click={() => (person.isHidden = !person.isHidden)}> | ||||
|                 <ImageThumbnail | ||||
|                   bind:hidden={person.isHidden} | ||||
|                   shadow | ||||
|                   url={api.getPeopleThumbnailUrl(person.id)} | ||||
|                   altText={person.name} | ||||
|                   widthStyle="100%" | ||||
|                 /> | ||||
|               </button> | ||||
|             </div> | ||||
|             {#if person.name} | ||||
|               <span | ||||
|                 class="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center font-medium text-white backdrop-blur-[1px] hover:cursor-pointer" | ||||
|               > | ||||
|                 {person.name} | ||||
|               </span> | ||||
|             {/if} | ||||
|           </div> | ||||
|         {/each} | ||||
|       </div> | ||||
|     </div> | ||||
|   </ShowHide> | ||||
| {/if} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user