diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index f6a1127a58bfe..5201db9e12145 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -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} + * @memberof PeopleResponseDto + */ + 'people': Array; +} /** * * @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 => { + getAllPeople: async (withHidden?: boolean, options: AxiosRequestConfig = {}): Promise => { 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>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(options); + async getAllPeople(withHidden?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + 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> { - return localVarFp.getAllPeople(options).then((request) => request(axios, basePath)); + getAllPeople(requestParameters: PersonApiGetAllPeopleRequest = {}, options?: AxiosRequestConfig): AxiosPromise { + 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)); } /** diff --git a/mobile/lib/modules/search/services/person.service.dart b/mobile/lib/modules/search/services/person.service.dart index 5813653cc7af6..8314ed10964ae 100644 --- a/mobile/lib/modules/search/services/person.service.dart +++ b/mobile/lib/modules/search/services/person.service.dart @@ -18,7 +18,8 @@ class PersonService { Future?> 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; diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 5c610108b557c..5e8a7ee704790 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -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 diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b8fa7a4d24841..5bb42236eaa3d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -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) diff --git a/mobile/openapi/doc/PeopleResponseDto.md b/mobile/openapi/doc/PeopleResponseDto.md new file mode 100644 index 0000000000000..9d00d8608cd13 --- /dev/null +++ b/mobile/openapi/doc/PeopleResponseDto.md @@ -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.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) + + diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index ee57d0c5064b6..609043526e766 100644 --- a/mobile/openapi/doc/PersonApi.md +++ b/mobile/openapi/doc/PersonApi.md @@ -18,7 +18,7 @@ Method | HTTP request | Description # **getAllPeople** -> List getAllPeople() +> PeopleResponseDto getAllPeople(withHidden) @@ -41,9 +41,10 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('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.md) +[**PeopleResponseDto**](PeopleResponseDto.md) ### Authorization diff --git a/mobile/openapi/doc/PersonResponseDto.md b/mobile/openapi/doc/PersonResponseDto.md index 05927762a94f7..6660b063cf84d 100644 --- a/mobile/openapi/doc/PersonResponseDto.md +++ b/mobile/openapi/doc/PersonResponseDto.md @@ -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) diff --git a/mobile/openapi/doc/PersonUpdateDto.md b/mobile/openapi/doc/PersonUpdateDto.md index 7496b2af62d42..9a59c852bb817 100644 --- a/mobile/openapi/doc/PersonUpdateDto.md +++ b/mobile/openapi/doc/PersonUpdateDto.md @@ -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) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 32420b9c295e5..819f3092ad4c8 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -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'; diff --git a/mobile/openapi/lib/api/person_api.dart b/mobile/openapi/lib/api/person_api.dart index 3a53bd5ebfa8e..7ced3bf7af7a1 100644 --- a/mobile/openapi/lib/api/person_api.dart +++ b/mobile/openapi/lib/api/person_api.dart @@ -17,7 +17,10 @@ class PersonApi { final ApiClient apiClient; /// Performs an HTTP 'GET /person' operation and returns the [Response]. - Future getAllPeopleWithHttpInfo() async { + /// Parameters: + /// + /// * [bool] withHidden: + Future getAllPeopleWithHttpInfo({ bool? withHidden, }) async { // ignore: prefer_const_declarations final path = r'/person'; @@ -28,6 +31,10 @@ class PersonApi { final headerParams = {}; final formParams = {}; + if (withHidden != null) { + queryParams.addAll(_queryParams('', 'withHidden', withHidden)); + } + const contentTypes = []; @@ -42,8 +49,11 @@ class PersonApi { ); } - Future?> getAllPeople() async { - final response = await getAllPeopleWithHttpInfo(); + /// Parameters: + /// + /// * [bool] withHidden: + Future 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') as List) - .cast() - .toList(); - + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'PeopleResponseDto',) as PeopleResponseDto; + } return null; } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index b2208df562e0c..b36572fb4acdf 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -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': diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart new file mode 100644 index 0000000000000..9470a827ebf5a --- /dev/null +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -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 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 toJson() { + final json = {}; + 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(); + + return PeopleResponseDto( + total: num.parse('${json[r'total']}'), + visible: num.parse('${json[r'visible']}'), + people: PersonResponseDto.listFromJson(json[r'people']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + 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 mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // 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> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + 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 = { + 'total', + 'visible', + 'people', + }; +} + diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index ddaa733853d19..909901f8afb25 100644 --- a/mobile/openapi/lib/model/person_response_dto.dart +++ b/mobile/openapi/lib/model/person_response_dto.dart @@ -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 toJson() { final json = {}; 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(json, r'id')!, name: mapValueOfType(json, r'name')!, thumbnailPath: mapValueOfType(json, r'thumbnailPath')!, + isHidden: mapValueOfType(json, r'isHidden')!, ); } return null; @@ -109,6 +116,7 @@ class PersonResponseDto { 'id', 'name', 'thumbnailPath', + 'isHidden', }; } diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart index 3d15c71565b73..4b1b9967e5174 100644 --- a/mobile/openapi/lib/model/person_update_dto.dart +++ b/mobile/openapi/lib/model/person_update_dto.dart @@ -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 toJson() { final json = {}; @@ -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(json, r'name'), featureFaceAssetId: mapValueOfType(json, r'featureFaceAssetId'), + isHidden: mapValueOfType(json, r'isHidden'), ); } return null; diff --git a/mobile/openapi/test/people_response_dto_test.dart b/mobile/openapi/test/people_response_dto_test.dart new file mode 100644 index 0000000000000..c48f0099adf2e --- /dev/null +++ b/mobile/openapi/test/people_response_dto_test.dart @@ -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 people (default value: const []) + test('to test the property `people`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/person_api_test.dart b/mobile/openapi/test/person_api_test.dart index 95482f63d34b6..a49f40aa6b71a 100644 --- a/mobile/openapi/test/person_api_test.dart +++ b/mobile/openapi/test/person_api_test.dart @@ -17,7 +17,7 @@ void main() { // final instance = PersonApi(); group('tests for PersonApi', () { - //Future> getAllPeople() async + //Future getAllPeople({ bool withHidden }) async test('test getAllPeople', () async { // TODO }); diff --git a/mobile/openapi/test/person_response_dto_test.dart b/mobile/openapi/test/person_response_dto_test.dart index 9adcbe1546210..129a9c3140ac7 100644 --- a/mobile/openapi/test/person_response_dto_test.dart +++ b/mobile/openapi/test/person_response_dto_test.dart @@ -31,6 +31,11 @@ void main() { // TODO }); + // bool isHidden + test('to test the property `isHidden`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/person_update_dto_test.dart b/mobile/openapi/test/person_update_dto_test.dart index be3b8fb741ca3..47de2eb877a39 100644 --- a/mobile/openapi/test/person_update_dto_test.dart +++ b/mobile/openapi/test/person_update_dto_test.dart @@ -28,6 +28,12 @@ void main() { // TODO }); + // Person visibility + // bool isHidden + test('to test the property `isHidden`', () async { + // TODO + }); + }); diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 877ea3867380b..8865bbae25fbb 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -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" } } }, diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index f5284f3908ac1..226cc77a9e20d 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -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'), }; } diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index b8efa65c99f81..41430afaf26f2 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -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, }; } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index c7eb08bfddf25..52a8d0f5dc5ea 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -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); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 4310ef431bf61..3ec835660123d 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -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 { + async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise { 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 { @@ -50,8 +62,8 @@ export class PersonService { async update(authUser: AuthUserDto, id: string, dto: PersonUpdateDto): Promise { 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 } }); diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 106961aa897d5..7620145128188 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -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 { - return this.service.getAll(authUser); + getAllPeople(@AuthUser() authUser: AuthUserDto, @Query() withHidden: PersonSearchDto): Promise { + return this.service.getAll(authUser, withHidden); } @Get(':id') diff --git a/server/src/infra/entities/person.entity.ts b/server/src/infra/entities/person.entity.ts index 40ad593e93552..b93c4bbf9df20 100644 --- a/server/src/infra/entities/person.entity.ts +++ b/server/src/infra/entities/person.entity.ts @@ -35,4 +35,7 @@ export class PersonEntity { @OneToMany(() => AssetFaceEntity, (assetFace) => assetFace.person) faces!: AssetFaceEntity[]; + + @Column({ default: false }) + isHidden!: boolean; } diff --git a/server/src/infra/migrations/1689281196844-AddHiddenFaces.ts b/server/src/infra/migrations/1689281196844-AddHiddenFaces.ts new file mode 100644 index 0000000000000..234b77dd3404b --- /dev/null +++ b/server/src/infra/migrations/1689281196844-AddHiddenFaces.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Infra1689281196844 implements MigrationInterface { + name = 'Infra1689281196844' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "person" ADD "isHidden" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "person" DROP COLUMN "isHidden"`); + } + +} diff --git a/server/src/infra/repositories/typesense.repository.ts b/server/src/infra/repositories/typesense.repository.ts index 40bd71ddb3fcc..2ca81f19c4fbc 100644 --- a/server/src/infra/repositories/typesense.repository.ts +++ b/server/src/infra/repositories/typesense.repository.ts @@ -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 }; } diff --git a/server/test/fixtures.ts b/server/test/fixtures.ts index 6d096772a7dc4..b4abca792ed63 100644 --- a/server/test/fixtures.ts +++ b/server/test/fixtures.ts @@ -1094,6 +1094,18 @@ export const personStub = { name: '', thumbnailPath: '/path/to/thumbnail.jpg', faces: [], + isHidden: false, + }), + hidden: Object.freeze({ + 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({ id: 'person-1', @@ -1104,6 +1116,7 @@ export const personStub = { name: 'Person 1', thumbnailPath: '/path/to/thumbnail.jpg', faces: [], + isHidden: false, }), noThumbnail: Object.freeze({ id: 'person-1', @@ -1114,6 +1127,7 @@ export const personStub = { name: '', thumbnailPath: '', faces: [], + isHidden: false, }), newThumbnail: Object.freeze({ id: 'person-1', @@ -1124,6 +1138,7 @@ export const personStub = { name: '', thumbnailPath: '/new/path/to/thumbnail.jpg', faces: [], + isHidden: false, }), primaryPerson: Object.freeze({ id: 'person-1', @@ -1134,6 +1149,7 @@ export const personStub = { name: 'Person 1', thumbnailPath: '/path/to/thumbnail', faces: [], + isHidden: false, }), mergePerson: Object.freeze({ id: 'person-2', @@ -1144,6 +1160,7 @@ export const personStub = { name: 'Person 2', thumbnailPath: '/path/to/thumbnail', faces: [], + isHidden: false, }), }; diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 414f7b147dfde..24f3b333798e0 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -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} + * @memberof PeopleResponseDto + */ + 'people': Array; +} /** * * @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 => { + getAllPeople: async (withHidden?: boolean, options: AxiosRequestConfig = {}): Promise => { 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>> { - const localVarAxiosArgs = await localVarAxiosParamCreator.getAllPeople(options); + async getAllPeople(withHidden?: boolean, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + 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> { - return localVarFp.getAllPeople(options).then((request) => request(axios, basePath)); + getAllPeople(withHidden?: boolean, options?: any): AxiosPromise { + 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)); } /** diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 4b1eea5e52390..3b78b17cebda5 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -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; {altText} (complete = true)} /> +{#if hidden} +
+ +
+{/if} {#if thumbhash && !complete} !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 = () => { diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 6b967e832f3ff..c91e28c006cb9 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -24,12 +24,12 @@
-
+
{#if person.name} {person.name} @@ -37,7 +37,7 @@
+
+ {#if person.name} + + {person.name} + + {/if} +
+ {/each} + + + +{/if}