Browse Source

fix: hide faces (#3352)

* fix: hide faces

* remove unused variable

* fix: work even if one fails

* better style for hidden people

* add hide face in the menu dropdown

* add buttons to toggle visibility for all faces

* add server test

* close modal with escape key

* fix: explore page

* improve show & hide faces modal

* keep name on people card

* simplify layout

* sticky app bar in show-hide page

* fix format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
martin 1 year ago
parent
commit
ed64c91da6

+ 132 - 0
cli/src/api/open-api/api.ts

@@ -1802,6 +1802,50 @@ export interface PeopleResponseDto {
      */
     'people': Array<PersonResponseDto>;
 }
+/**
+ * 
+ * @export
+ * @interface PeopleUpdateDto
+ */
+export interface PeopleUpdateDto {
+    /**
+     * 
+     * @type {Array<PeopleUpdateItem>}
+     * @memberof PeopleUpdateDto
+     */
+    'people': Array<PeopleUpdateItem>;
+}
+/**
+ * 
+ * @export
+ * @interface PeopleUpdateItem
+ */
+export interface PeopleUpdateItem {
+    /**
+     * Person id.
+     * @type {string}
+     * @memberof PeopleUpdateItem
+     */
+    'id': string;
+    /**
+     * Person name.
+     * @type {string}
+     * @memberof PeopleUpdateItem
+     */
+    'name'?: string;
+    /**
+     * Asset is used to get the feature face thumbnail.
+     * @type {string}
+     * @memberof PeopleUpdateItem
+     */
+    'featureFaceAssetId'?: string;
+    /**
+     * Person visibility
+     * @type {boolean}
+     * @memberof PeopleUpdateItem
+     */
+    'isHidden'?: boolean;
+}
 /**
  * 
  * @export
@@ -8896,6 +8940,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {PeopleUpdateDto} peopleUpdateDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updatePeople: async (peopleUpdateDto: PeopleUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'peopleUpdateDto' is not null or undefined
+            assertParamExists('updatePeople', 'peopleUpdateDto', peopleUpdateDto)
+            const localVarPath = `/person`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(peopleUpdateDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {string} id 
@@ -9005,6 +9093,16 @@ export const PersonApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {PeopleUpdateDto} peopleUpdateDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async updatePeople(peopleUpdateDto: PeopleUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePeople(peopleUpdateDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {string} id 
@@ -9071,6 +9169,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
         mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
             return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updatePeople(requestParameters: PersonApiUpdatePeopleRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
+            return localVarFp.updatePeople(requestParameters.peopleUpdateDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {PersonApiUpdatePersonRequest} requestParameters Request parameters.
@@ -9160,6 +9267,20 @@ export interface PersonApiMergePersonRequest {
     readonly mergePersonDto: MergePersonDto
 }
 
+/**
+ * Request parameters for updatePeople operation in PersonApi.
+ * @export
+ * @interface PersonApiUpdatePeopleRequest
+ */
+export interface PersonApiUpdatePeopleRequest {
+    /**
+     * 
+     * @type {PeopleUpdateDto}
+     * @memberof PersonApiUpdatePeople
+     */
+    readonly peopleUpdateDto: PeopleUpdateDto
+}
+
 /**
  * Request parameters for updatePerson operation in PersonApi.
  * @export
@@ -9243,6 +9364,17 @@ export class PersonApi extends BaseAPI {
         return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof PersonApi
+     */
+    public updatePeople(requestParameters: PersonApiUpdatePeopleRequest, options?: AxiosRequestConfig) {
+        return PersonApiFp(this.configuration).updatePeople(requestParameters.peopleUpdateDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {PersonApiUpdatePersonRequest} requestParameters Request parameters.

+ 6 - 0
mobile/openapi/.openapi-generator/FILES

@@ -72,6 +72,8 @@ doc/OAuthConfigDto.md
 doc/OAuthConfigResponseDto.md
 doc/PartnerApi.md
 doc/PeopleResponseDto.md
+doc/PeopleUpdateDto.md
+doc/PeopleUpdateItem.md
 doc/PersonApi.md
 doc/PersonResponseDto.md
 doc/PersonUpdateDto.md
@@ -210,6 +212,8 @@ 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/people_update_dto.dart
+lib/model/people_update_item.dart
 lib/model/person_response_dto.dart
 lib/model/person_update_dto.dart
 lib/model/queue_status_dto.dart
@@ -325,6 +329,8 @@ 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/people_update_dto_test.dart
+test/people_update_item_test.dart
 test/person_api_test.dart
 test/person_response_dto_test.dart
 test/person_update_dto_test.dart

+ 3 - 0
mobile/openapi/README.md

@@ -134,6 +134,7 @@ Class | Method | HTTP request | Description
 *PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | 
 *PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | 
 *PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge | 
+*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person | 
 *PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} | 
 *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | 
 *SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config | 
@@ -239,6 +240,8 @@ Class | Method | HTTP request | Description
  - [OAuthConfigDto](doc//OAuthConfigDto.md)
  - [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
  - [PeopleResponseDto](doc//PeopleResponseDto.md)
+ - [PeopleUpdateDto](doc//PeopleUpdateDto.md)
+ - [PeopleUpdateItem](doc//PeopleUpdateItem.md)
  - [PersonResponseDto](doc//PersonResponseDto.md)
  - [PersonUpdateDto](doc//PersonUpdateDto.md)
  - [QueueStatusDto](doc//QueueStatusDto.md)

+ 15 - 0
mobile/openapi/doc/PeopleUpdateDto.md

@@ -0,0 +1,15 @@
+# openapi.model.PeopleUpdateDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**people** | [**List<PeopleUpdateItem>**](PeopleUpdateItem.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)
+
+

+ 18 - 0
mobile/openapi/doc/PeopleUpdateItem.md

@@ -0,0 +1,18 @@
+# openapi.model.PeopleUpdateItem
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**id** | **String** | Person id. | 
+**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)
+
+

+ 56 - 0
mobile/openapi/doc/PersonApi.md

@@ -14,6 +14,7 @@ Method | HTTP request | Description
 [**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets | 
 [**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail | 
 [**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge | 
+[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person | 
 [**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} | 
 
 
@@ -294,6 +295,61 @@ 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)
 
+# **updatePeople**
+> List<BulkIdResponseDto> updatePeople(peopleUpdateDto)
+
+
+
+### 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 = PersonApi();
+final peopleUpdateDto = PeopleUpdateDto(); // PeopleUpdateDto | 
+
+try {
+    final result = api_instance.updatePeople(peopleUpdateDto);
+    print(result);
+} catch (e) {
+    print('Exception when calling PersonApi->updatePeople: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **peopleUpdateDto** | [**PeopleUpdateDto**](PeopleUpdateDto.md)|  | 
+
+### Return type
+
+[**List<BulkIdResponseDto>**](BulkIdResponseDto.md)
+
+### Authorization
+
+[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **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)
+
 # **updatePerson**
 > PersonResponseDto updatePerson(id, personUpdateDto)
 

+ 2 - 0
mobile/openapi/lib/api.dart

@@ -105,6 +105,8 @@ 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/people_update_dto.dart';
+part 'model/people_update_item.dart';
 part 'model/person_response_dto.dart';
 part 'model/person_update_dto.dart';
 part 'model/queue_status_dto.dart';

+ 50 - 0
mobile/openapi/lib/api/person_api.dart

@@ -269,6 +269,56 @@ class PersonApi {
     return null;
   }
 
+  /// Performs an HTTP 'PUT /person' operation and returns the [Response].
+  /// Parameters:
+  ///
+  /// * [PeopleUpdateDto] peopleUpdateDto (required):
+  Future<Response> updatePeopleWithHttpInfo(PeopleUpdateDto peopleUpdateDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/person';
+
+    // ignore: prefer_final_locals
+    Object? postBody = peopleUpdateDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'PUT',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// Parameters:
+  ///
+  /// * [PeopleUpdateDto] peopleUpdateDto (required):
+  Future<List<BulkIdResponseDto>?> updatePeople(PeopleUpdateDto peopleUpdateDto,) async {
+    final response = await updatePeopleWithHttpInfo(peopleUpdateDto,);
+    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<BulkIdResponseDto>') as List)
+        .cast<BulkIdResponseDto>()
+        .toList();
+
+    }
+    return null;
+  }
+
   /// Performs an HTTP 'PUT /person/{id}' operation and returns the [Response].
   /// Parameters:
   ///

+ 4 - 0
mobile/openapi/lib/api_client.dart

@@ -305,6 +305,10 @@ class ApiClient {
           return OAuthConfigResponseDto.fromJson(value);
         case 'PeopleResponseDto':
           return PeopleResponseDto.fromJson(value);
+        case 'PeopleUpdateDto':
+          return PeopleUpdateDto.fromJson(value);
+        case 'PeopleUpdateItem':
+          return PeopleUpdateItem.fromJson(value);
         case 'PersonResponseDto':
           return PersonResponseDto.fromJson(value);
         case 'PersonUpdateDto':

+ 98 - 0
mobile/openapi/lib/model/people_update_dto.dart

@@ -0,0 +1,98 @@
+//
+// 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 PeopleUpdateDto {
+  /// Returns a new [PeopleUpdateDto] instance.
+  PeopleUpdateDto({
+    this.people = const [],
+  });
+
+  List<PeopleUpdateItem> people;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateDto &&
+     other.people == people;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (people.hashCode);
+
+  @override
+  String toString() => 'PeopleUpdateDto[people=$people]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'people'] = this.people;
+    return json;
+  }
+
+  /// Returns a new [PeopleUpdateDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static PeopleUpdateDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return PeopleUpdateDto(
+        people: PeopleUpdateItem.listFromJson(json[r'people']),
+      );
+    }
+    return null;
+  }
+
+  static List<PeopleUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <PeopleUpdateDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = PeopleUpdateDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, PeopleUpdateDto> mapFromJson(dynamic json) {
+    final map = <String, PeopleUpdateDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = PeopleUpdateDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of PeopleUpdateDto-objects as value to a dart map
+  static Map<String, List<PeopleUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<PeopleUpdateDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = PeopleUpdateDto.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'people',
+  };
+}
+

+ 153 - 0
mobile/openapi/lib/model/people_update_item.dart

@@ -0,0 +1,153 @@
+//
+// 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 PeopleUpdateItem {
+  /// Returns a new [PeopleUpdateItem] instance.
+  PeopleUpdateItem({
+    required this.id,
+    this.name,
+    this.featureFaceAssetId,
+    this.isHidden,
+  });
+
+  /// Person id.
+  String id;
+
+  /// Person name.
+  ///
+  /// 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.
+  ///
+  String? name;
+
+  /// Asset is used to get the feature face thumbnail.
+  ///
+  /// 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.
+  ///
+  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 PeopleUpdateItem &&
+     other.id == id &&
+     other.name == name &&
+     other.featureFaceAssetId == featureFaceAssetId &&
+     other.isHidden == isHidden;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (id.hashCode) +
+    (name == null ? 0 : name!.hashCode) +
+    (featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
+    (isHidden == null ? 0 : isHidden!.hashCode);
+
+  @override
+  String toString() => 'PeopleUpdateItem[id=$id, name=$name, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'id'] = this.id;
+    if (this.name != null) {
+      json[r'name'] = this.name;
+    } else {
+    //  json[r'name'] = null;
+    }
+    if (this.featureFaceAssetId != null) {
+      json[r'featureFaceAssetId'] = this.featureFaceAssetId;
+    } else {
+    //  json[r'featureFaceAssetId'] = null;
+    }
+    if (this.isHidden != null) {
+      json[r'isHidden'] = this.isHidden;
+    } else {
+    //  json[r'isHidden'] = null;
+    }
+    return json;
+  }
+
+  /// Returns a new [PeopleUpdateItem] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static PeopleUpdateItem? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      return PeopleUpdateItem(
+        id: mapValueOfType<String>(json, r'id')!,
+        name: mapValueOfType<String>(json, r'name'),
+        featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
+        isHidden: mapValueOfType<bool>(json, r'isHidden'),
+      );
+    }
+    return null;
+  }
+
+  static List<PeopleUpdateItem> listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <PeopleUpdateItem>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = PeopleUpdateItem.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, PeopleUpdateItem> mapFromJson(dynamic json) {
+    final map = <String, PeopleUpdateItem>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = PeopleUpdateItem.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of PeopleUpdateItem-objects as value to a dart map
+  static Map<String, List<PeopleUpdateItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<PeopleUpdateItem>>{};
+    if (json is Map && json.isNotEmpty) {
+      // ignore: parameter_assignments
+      json = json.cast<String, dynamic>();
+      for (final entry in json.entries) {
+        map[entry.key] = PeopleUpdateItem.listFromJson(entry.value, growable: growable,);
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'id',
+  };
+}
+

+ 27 - 0
mobile/openapi/test/people_update_dto_test.dart

@@ -0,0 +1,27 @@
+//
+// 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 PeopleUpdateDto
+void main() {
+  // final instance = PeopleUpdateDto();
+
+  group('test PeopleUpdateDto', () {
+    // List<PeopleUpdateItem> people (default value: const [])
+    test('to test the property `people`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 46 - 0
mobile/openapi/test/people_update_item_test.dart

@@ -0,0 +1,46 @@
+//
+// 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 PeopleUpdateItem
+void main() {
+  // final instance = PeopleUpdateItem();
+
+  group('test PeopleUpdateItem', () {
+    // Person id.
+    // String id
+    test('to test the property `id`', () async {
+      // TODO
+    });
+
+    // Person name.
+    // String name
+    test('to test the property `name`', () async {
+      // TODO
+    });
+
+    // Asset is used to get the feature face thumbnail.
+    // String featureFaceAssetId
+    test('to test the property `featureFaceAssetId`', () async {
+      // TODO
+    });
+
+    // Person visibility
+    // bool isHidden
+    test('to test the property `isHidden`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 5 - 0
mobile/openapi/test/person_api_test.dart

@@ -42,6 +42,11 @@ void main() {
       // TODO
     });
 
+    //Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
+    test('test updatePeople', () async {
+      // TODO
+    });
+
     //Future<PersonResponseDto> updatePerson(String id, PersonUpdateDto personUpdateDto) async
     test('test updatePerson', () async {
       // TODO

+ 83 - 2
server/immich-openapi-specs.json

@@ -2546,6 +2546,49 @@
             "api_key": []
           }
         ]
+      },
+      "put": {
+        "operationId": "updatePeople",
+        "parameters": [],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/PeopleUpdateDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "array",
+                  "items": {
+                    "$ref": "#/components/schemas/BulkIdResponseDto"
+                  }
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Person"
+        ],
+        "security": [
+          {
+            "bearer": []
+          },
+          {
+            "cookie": []
+          },
+          {
+            "api_key": []
+          }
+        ]
       }
     },
     "/person/{id}": {
@@ -5028,13 +5071,13 @@
             "type": "boolean"
           },
           "error": {
-            "type": "string",
             "enum": [
               "duplicate",
               "no_permission",
               "not_found",
               "unknown"
-            ]
+            ],
+            "type": "string"
           }
         },
         "required": [
@@ -5906,6 +5949,44 @@
           "people"
         ]
       },
+      "PeopleUpdateDto": {
+        "type": "object",
+        "properties": {
+          "people": {
+            "type": "array",
+            "items": {
+              "$ref": "#/components/schemas/PeopleUpdateItem"
+            }
+          }
+        },
+        "required": [
+          "people"
+        ]
+      },
+      "PeopleUpdateItem": {
+        "type": "object",
+        "properties": {
+          "id": {
+            "type": "string",
+            "description": "Person id."
+          },
+          "name": {
+            "type": "string",
+            "description": "Person name."
+          },
+          "featureFaceAssetId": {
+            "type": "string",
+            "description": "Asset is used to get the feature face thumbnail."
+          },
+          "isHidden": {
+            "type": "boolean",
+            "description": "Person visibility"
+          }
+        },
+        "required": [
+          "id"
+        ]
+      },
       "PersonResponseDto": {
         "type": "object",
         "properties": {

+ 39 - 2
server/src/domain/person/person.dto.ts

@@ -1,6 +1,6 @@
 import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
-import { Transform } from 'class-transformer';
-import { IsBoolean, IsOptional, IsString } from 'class-validator';
+import { Transform, Type } from 'class-transformer';
+import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
 import { toBoolean, ValidateUUID } from '../domain.util';
 
 export class PersonUpdateDto {
@@ -26,6 +26,43 @@ export class PersonUpdateDto {
   isHidden?: boolean;
 }
 
+export class PeopleUpdateDto {
+  @IsArray()
+  @ValidateNested({ each: true })
+  @Type(() => PeopleUpdateItem)
+  people!: PeopleUpdateItem[];
+}
+
+export class PeopleUpdateItem {
+  /**
+   * Person id.
+   */
+  @IsString()
+  @IsNotEmpty()
+  id!: string;
+
+  /**
+   * Person name.
+   */
+  @IsOptional()
+  @IsString()
+  name?: string;
+
+  /**
+   * Asset is used to get the feature face thumbnail.
+   */
+  @IsOptional()
+  @IsString()
+  featureFaceAssetId?: string;
+
+  /**
+   * Person visibility
+   */
+  @IsOptional()
+  @IsBoolean()
+  isHidden?: boolean;
+}
+
 export class MergePersonDto {
   @ValidateUUID({ each: true })
   ids!: string[];

+ 10 - 0
server/src/domain/person/person.service.spec.ts

@@ -188,6 +188,16 @@ describe(PersonService.name, () => {
     });
   });
 
+  describe('updateAll', () => {
+    it('should throw an error when personId is invalid', async () => {
+      personMock.getById.mockResolvedValue(null);
+      await expect(
+        sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }),
+      ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]);
+      expect(personMock.update).not.toHaveBeenCalled();
+    });
+  });
+
   describe('handlePersonCleanup', () => {
     it('should delete people without faces', async () => {
       personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]);

+ 19 - 0
server/src/domain/person/person.service.ts

@@ -8,6 +8,7 @@ import {
   mapPerson,
   MergePersonDto,
   PeopleResponseDto,
+  PeopleUpdateDto,
   PersonResponseDto,
   PersonSearchDto,
   PersonUpdateDto,
@@ -96,6 +97,24 @@ export class PersonService {
     return mapPerson(person);
   }
 
+  async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
+    const results: BulkIdResponseDto[] = [];
+    for (const person of dto.people) {
+      try {
+        await this.update(authUser, person.id, {
+          isHidden: person.isHidden,
+          name: person.name,
+          featureFaceAssetId: person.featureFaceAssetId,
+        }),
+          results.push({ id: person.id, success: true });
+      } catch (error: Error | any) {
+        this.logger.error(`Unable to update ${person.id} : ${error}`, error?.stack);
+        results.push({ id: person.id, success: false, error: BulkIdErrorReason.UNKNOWN });
+      }
+    }
+    return results;
+  }
+
   async handlePersonCleanup() {
     const people = await this.repository.getAllWithoutFaces();
     for (const person of people) {

+ 6 - 0
server/src/immich/controllers/person.controller.ts

@@ -5,6 +5,7 @@ import {
   ImmichReadStream,
   MergePersonDto,
   PeopleResponseDto,
+  PeopleUpdateDto,
   PersonResponseDto,
   PersonSearchDto,
   PersonService,
@@ -32,6 +33,11 @@ export class PersonController {
     return this.service.getAll(authUser, withHidden);
   }
 
+  @Put()
+  updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
+    return this.service.updatePeople(authUser, dto);
+  }
+
   @Get(':id')
   getPerson(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
     return this.service.getById(authUser, id);

+ 132 - 0
web/src/api/open-api/api.ts

@@ -1802,6 +1802,50 @@ export interface PeopleResponseDto {
      */
     'people': Array<PersonResponseDto>;
 }
+/**
+ * 
+ * @export
+ * @interface PeopleUpdateDto
+ */
+export interface PeopleUpdateDto {
+    /**
+     * 
+     * @type {Array<PeopleUpdateItem>}
+     * @memberof PeopleUpdateDto
+     */
+    'people': Array<PeopleUpdateItem>;
+}
+/**
+ * 
+ * @export
+ * @interface PeopleUpdateItem
+ */
+export interface PeopleUpdateItem {
+    /**
+     * Person id.
+     * @type {string}
+     * @memberof PeopleUpdateItem
+     */
+    'id': string;
+    /**
+     * Person name.
+     * @type {string}
+     * @memberof PeopleUpdateItem
+     */
+    'name'?: string;
+    /**
+     * Asset is used to get the feature face thumbnail.
+     * @type {string}
+     * @memberof PeopleUpdateItem
+     */
+    'featureFaceAssetId'?: string;
+    /**
+     * Person visibility
+     * @type {boolean}
+     * @memberof PeopleUpdateItem
+     */
+    'isHidden'?: boolean;
+}
 /**
  * 
  * @export
@@ -8940,6 +8984,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {PeopleUpdateDto} peopleUpdateDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updatePeople: async (peopleUpdateDto: PeopleUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'peopleUpdateDto' is not null or undefined
+            assertParamExists('updatePeople', 'peopleUpdateDto', peopleUpdateDto)
+            const localVarPath = `/person`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication cookie required
+
+            // authentication api_key required
+            await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
+
+            // authentication bearer required
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+
+
+    
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            localVarRequestOptions.data = serializeDataIfNeeded(peopleUpdateDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {string} id 
@@ -9049,6 +9137,16 @@ export const PersonApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {PeopleUpdateDto} peopleUpdateDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async updatePeople(peopleUpdateDto: PeopleUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.updatePeople(peopleUpdateDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {string} id 
@@ -9116,6 +9214,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
         mergePerson(id: string, mergePersonDto: MergePersonDto, options?: any): AxiosPromise<Array<BulkIdResponseDto>> {
             return localVarFp.mergePerson(id, mergePersonDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {PeopleUpdateDto} peopleUpdateDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updatePeople(peopleUpdateDto: PeopleUpdateDto, options?: any): AxiosPromise<Array<BulkIdResponseDto>> {
+            return localVarFp.updatePeople(peopleUpdateDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {string} id 
@@ -9206,6 +9313,20 @@ export interface PersonApiMergePersonRequest {
     readonly mergePersonDto: MergePersonDto
 }
 
+/**
+ * Request parameters for updatePeople operation in PersonApi.
+ * @export
+ * @interface PersonApiUpdatePeopleRequest
+ */
+export interface PersonApiUpdatePeopleRequest {
+    /**
+     * 
+     * @type {PeopleUpdateDto}
+     * @memberof PersonApiUpdatePeople
+     */
+    readonly peopleUpdateDto: PeopleUpdateDto
+}
+
 /**
  * Request parameters for updatePerson operation in PersonApi.
  * @export
@@ -9289,6 +9410,17 @@ export class PersonApi extends BaseAPI {
         return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof PersonApi
+     */
+    public updatePeople(requestParameters: PersonApiUpdatePeopleRequest, options?: AxiosRequestConfig) {
+        return PersonApiFp(this.configuration).updatePeople(requestParameters.peopleUpdateDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {PersonApiUpdatePersonRequest} requestParameters Request parameters.

+ 6 - 2
web/src/lib/components/assets/thumbnail/image-thumbnail.svelte

@@ -15,12 +15,15 @@
   export let circle = false;
   export let hidden = false;
   let complete = false;
+
+  export let eyeColor = 'white';
 </script>
 
 <img
   style:width={widthStyle}
   style:height={heightStyle}
-  style:filter={hidden ? 'grayscale(75%)' : 'none'}
+  style:filter={hidden ? 'grayscale(50%)' : 'none'}
+  style:opacity={hidden ? '0.5' : '1'}
   src={url}
   alt={altText}
   class="object-cover transition duration-300"
@@ -32,9 +35,10 @@
   use:imageLoad
   on:image-load|once={() => (complete = true)}
 />
+
 {#if hidden}
   <div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
-    <EyeOffOutline size="2em" />
+    <EyeOffOutline size="2em" color={eyeColor} />
   </div>
 {/if}
 

+ 1 - 1
web/src/lib/components/faces-page/merge-face-selector.svelte

@@ -25,7 +25,7 @@
   $: unselectedPeople = people.filter((source) => !selectedPeople.includes(source) && source.id !== person.id);
 
   onMount(async () => {
-    const { data } = await api.personApi.getAllPeople({ withHidden: true });
+    const { data } = await api.personApi.getAllPeople({ withHidden: false });
     people = data.people;
   });
 

+ 7 - 4
web/src/lib/components/faces-page/people-card.svelte

@@ -20,17 +20,19 @@
   const onMergeFacesClicked = () => {
     dispatch('merge-faces', person);
   };
+
+  const onHideFaceClicked = () => {
+    dispatch('hide-face', person);
+  };
 </script>
 
 <div id="people-card" class="relative">
   <a href="/people/{person.id}" draggable="false">
-    <div class="w-48 rounded-xl brightness-95 filter">
+    <div class="h-48 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="w-100 absolute bottom-2 w-full text-ellipsis px-1 text-center font-medium text-white backdrop-blur-[1px] hover:cursor-pointer"
-      >
+      <span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
         {person.name}
       </span>
     {/if}
@@ -50,6 +52,7 @@
 
     {#if showContextMenu}
       <ContextMenu on:outclick={() => (showContextMenu = false)}>
+        <MenuOption on:click={() => onHideFaceClicked()} text="Hide face" />
         <MenuOption on:click={() => onChangeNameClicked()} text="Change name" />
         <MenuOption on:click={() => onMergeFacesClicked()} text="Merge faces" />
       </ContextMenu>

+ 30 - 10
web/src/lib/components/faces-page/show-hide.svelte

@@ -1,12 +1,19 @@
-<script>
+<script lang="ts">
   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';
+  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
+  import Restart from 'svelte-material-icons/Restart.svelte';
+  import Eye from 'svelte-material-icons/Eye.svelte';
+  import EyeOff from 'svelte-material-icons/EyeOff.svelte';
 
   const dispatch = createEventDispatcher();
+
+  export let showLoadingSpinner: boolean;
+  export let toggleVisibility: boolean;
 </script>
 
 <section
@@ -14,17 +21,30 @@
   class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg"
 >
   <div
-    class="absolute flex h-16 w-full place-items-center justify-between border-b dark:border-immich-dark-gray dark:text-immich-dark-fg"
+    class="sticky top-0 z-10 flex h-16 w-full items-center justify-between border-b bg-white p-1 dark:border-immich-dark-gray dark:bg-black dark:text-immich-dark-fg md:p-8"
   >
-    <div class="flex w-full items-center justify-between p-8">
-      <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 class="flex items-center">
+      <CircleIconButton logo={Close} on:click={() => dispatch('closeClick')} />
+      <p class="ml-4 hidden sm:block">Show & hide faces</p>
     </div>
-    <div class="immich-scrollbar absolute top-16 h-[calc(100%-theme(spacing.16))] w-full p-4 pb-8">
-      <slot />
+    <div class="flex items-center justify-end">
+      <div class="flex items-center md:mr-8">
+        <CircleIconButton title="Reset faces visibility" logo={Restart} on:click={() => dispatch('reset-visibility')} />
+        <CircleIconButton
+          title="Toggle visibility"
+          logo={toggleVisibility ? Eye : EyeOff}
+          on:click={() => dispatch('toggle-visibility')}
+        />
+      </div>
+      {#if !showLoadingSpinner}
+        <IconButton on:click={() => dispatch('doneClick')}>Done</IconButton>
+      {:else}
+        <LoadingSpinner />
+      {/if}
     </div>
   </div>
+
+  <div class="flex w-full flex-wrap gap-1 bg-immich-bg p-2 pb-8 dark:bg-immich-dark-bg md:px-8 md:pt-4">
+    <slot />
+  </div>
 </section>

+ 148 - 50
web/src/routes/(user)/people/+page.svelte

@@ -5,7 +5,7 @@
   import PeopleCard from '$lib/components/faces-page/people-card.svelte';
   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 { api, PeopleUpdateItem, type PersonResponseDto } from '@api';
   import { goto } from '$app/navigation';
   import { AppRoute } from '$lib/constants';
   import { handleError } from '$lib/utils/handle-error';
@@ -17,41 +17,86 @@
   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';
+  import { onDestroy, onMount } from 'svelte';
+  import { browser } from '$app/environment';
 
   export let data: PageData;
   let selectHidden = false;
-  let changeCounter = 0;
   let initialHiddenValues: Record<string, boolean> = {};
 
+  let eyeColorMap: Record<string, string> = {};
+
   let people = data.people.people;
   let countTotalPeople = data.people.total;
   let countVisiblePeople = data.people.visible;
 
+  let showLoadingSpinner = false;
+  let toggleVisibility = false;
+
   people.forEach((person: PersonResponseDto) => {
     initialHiddenValues[person.id] = person.isHidden;
   });
 
+  const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event);
+
+  onMount(() => {
+    document.addEventListener('keydown', onKeyboardPress);
+  });
+
+  onDestroy(() => {
+    if (browser) {
+      document.removeEventListener('keydown', onKeyboardPress);
+    }
+  });
+
+  const handleKeyboardPress = (event: KeyboardEvent) => {
+    switch (event.key) {
+      case 'Escape':
+        handleCloseClick();
+        return;
+    }
+  };
+
   const handleCloseClick = () => {
+    for (const person of people) {
+      person.isHidden = initialHiddenValues[person.id];
+    }
+    // trigger reactivity
+    people = people;
+
+    // Reset variables used on the "Show & hide faces"   modal
+    showLoadingSpinner = false;
     selectHidden = false;
-    people.forEach((person: PersonResponseDto) => {
+    toggleVisibility = false;
+  };
+
+  const handleResetVisibility = () => {
+    for (const person of people) {
       person.isHidden = initialHiddenValues[person.id];
-    });
+    }
+
+    // trigger reactivity
+    people = people;
+  };
+
+  const handleToggleVisibility = () => {
+    toggleVisibility = !toggleVisibility;
+    for (const person of people) {
+      person.isHidden = toggleVisibility;
+    }
+
+    // trigger reactivity
+    people = people;
   };
 
   const handleDoneClick = async () => {
-    selectHidden = false;
+    showLoadingSpinner = true;
+    let changed: PeopleUpdateItem[] = [];
     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 },
-          });
+          changed.push({ id: person.id, isHidden: person.isHidden });
 
           // Update the initial hidden values
           initialHiddenValues[person.id] = person.isHidden;
@@ -61,18 +106,34 @@
         }
       }
 
-      if (changeCounter > 0) {
+      if (changed.length > 0) {
+        const { data: results } = await api.personApi.updatePeople({
+          peopleUpdateDto: { people: changed },
+        });
+        const count = results.filter(({ success }) => success).length;
+        if (results.length - count > 0) {
+          notificationController.show({
+            type: NotificationType.Error,
+            message: `Unable to change the visibility for ${results.length - count} ${
+              results.length - count <= 1 ? 'person' : 'people'
+            }`,
+          });
+        }
         notificationController.show({
           type: NotificationType.Info,
-          message: `Visibility changed for ${changeCounter} ${changeCounter <= 1 ? 'person' : 'people'}`,
+          message: `Visibility changed for ${count} ${count <= 1 ? 'person' : 'people'}`,
         });
       }
     } catch (error) {
       handleError(
         error,
-        `Unable to change the visibility for ${changeCounter} ${changeCounter <= 1 ? 'person' : 'people'}`,
+        `Unable to change the visibility for ${changed.length} ${changed.length <= 1 ? 'person' : 'people'}`,
       );
     }
+    // Reset variables used on the "Show & hide faces" modal
+    showLoadingSpinner = false;
+    selectHidden = false;
+    toggleVisibility = false;
   };
 
   let showChangeNameModal = false;
@@ -85,6 +146,37 @@
     edittingPerson = detail;
   };
 
+  const handleHideFace = async (event: CustomEvent<PersonResponseDto>) => {
+    try {
+      const { data: updatedPerson } = await api.personApi.updatePerson({
+        id: event.detail.id,
+        personUpdateDto: { isHidden: true },
+      });
+
+      people = people.map((person: PersonResponseDto) => {
+        if (person.id === updatedPerson.id) {
+          return updatedPerson;
+        }
+        return person;
+      });
+
+      people.forEach((person: PersonResponseDto) => {
+        initialHiddenValues[person.id] = person.isHidden;
+      });
+
+      countVisiblePeople--;
+
+      showChangeNameModal = false;
+
+      notificationController.show({
+        message: 'Changed visibility succesfully',
+        type: NotificationType.Info,
+      });
+    } catch (error) {
+      handleError(error, 'Unable to hide person');
+    }
+  };
+
   const handleMergeFaces = (event: CustomEvent<PersonResponseDto>) => {
     goto(`${AppRoute.PEOPLE}/${event.detail.id}?action=merge`);
   };
@@ -132,13 +224,16 @@
   {#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}
+        {#each people as person (person.id)}
+          {#if !person.isHidden}
+            <PeopleCard
+              {person}
+              on:change-name={handleChangeName}
+              on:merge-faces={handleMergeFaces}
+              on:hide-face={handleHideFace}
+            />
+          {/if}
+        {/each}
       </div>
     </div>
   {:else}
@@ -184,32 +279,35 @@
   {/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
+    on:doneClick={handleDoneClick}
+    on:closeClick={handleCloseClick}
+    on:reset-visibility={handleResetVisibility}
+    on:toggle-visibility={handleToggleVisibility}
+    bind:showLoadingSpinner
+    bind:toggleVisibility
+  >
+    {#each people as person (person.id)}
+      <button
+        class="relative h-36 w-36 md:h-48 md:w-48"
+        on:click={() => (person.isHidden = !person.isHidden)}
+        on:mouseenter={() => (eyeColorMap[person.id] = 'black')}
+        on:mouseleave={() => (eyeColorMap[person.id] = 'white')}
+      >
+        <ImageThumbnail
+          bind:hidden={person.isHidden}
+          shadow
+          url={api.getPeopleThumbnailUrl(person.id)}
+          altText={person.name}
+          widthStyle="100%"
+          bind:eyeColor={eyeColorMap[person.id]}
+        />
+        {#if person.name}
+          <span class="absolute bottom-2 left-0 w-full select-text px-1 text-center font-medium text-white">
+            {person.name}
+          </span>
+        {/if}
+      </button>
+    {/each}
   </ShowHide>
 {/if}