diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index f6a1127a58..5201db9e12 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 5813653cc7..8314ed1096 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 5c610108b5..5e8a7ee704 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 b8fa7a4d24..5bb42236ea 100644 Binary files a/mobile/openapi/README.md and b/mobile/openapi/README.md differ diff --git a/mobile/openapi/doc/PeopleResponseDto.md b/mobile/openapi/doc/PeopleResponseDto.md new file mode 100644 index 0000000000..9d00d8608c Binary files /dev/null and b/mobile/openapi/doc/PeopleResponseDto.md differ diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index ee57d0c506..609043526e 100644 Binary files a/mobile/openapi/doc/PersonApi.md and b/mobile/openapi/doc/PersonApi.md differ diff --git a/mobile/openapi/doc/PersonResponseDto.md b/mobile/openapi/doc/PersonResponseDto.md index 05927762a9..6660b063cf 100644 Binary files a/mobile/openapi/doc/PersonResponseDto.md and b/mobile/openapi/doc/PersonResponseDto.md differ diff --git a/mobile/openapi/doc/PersonUpdateDto.md b/mobile/openapi/doc/PersonUpdateDto.md index 7496b2af62..9a59c852bb 100644 Binary files a/mobile/openapi/doc/PersonUpdateDto.md and b/mobile/openapi/doc/PersonUpdateDto.md differ diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 32420b9c29..819f3092ad 100644 Binary files a/mobile/openapi/lib/api.dart and b/mobile/openapi/lib/api.dart differ diff --git a/mobile/openapi/lib/api/person_api.dart b/mobile/openapi/lib/api/person_api.dart index 3a53bd5ebf..7ced3bf7af 100644 Binary files a/mobile/openapi/lib/api/person_api.dart and b/mobile/openapi/lib/api/person_api.dart differ diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index b2208df562..b36572fb4a 100644 Binary files a/mobile/openapi/lib/api_client.dart and b/mobile/openapi/lib/api_client.dart differ 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 0000000000..9470a827eb Binary files /dev/null and b/mobile/openapi/lib/model/people_response_dto.dart differ diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index ddaa733853..909901f8af 100644 Binary files a/mobile/openapi/lib/model/person_response_dto.dart and b/mobile/openapi/lib/model/person_response_dto.dart differ diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart index 3d15c71565..4b1b9967e5 100644 Binary files a/mobile/openapi/lib/model/person_update_dto.dart and b/mobile/openapi/lib/model/person_update_dto.dart differ 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 0000000000..c48f0099ad Binary files /dev/null and b/mobile/openapi/test/people_response_dto_test.dart differ diff --git a/mobile/openapi/test/person_api_test.dart b/mobile/openapi/test/person_api_test.dart index 95482f63d3..a49f40aa6b 100644 Binary files a/mobile/openapi/test/person_api_test.dart and b/mobile/openapi/test/person_api_test.dart differ diff --git a/mobile/openapi/test/person_response_dto_test.dart b/mobile/openapi/test/person_response_dto_test.dart index 9adcbe1546..129a9c3140 100644 Binary files a/mobile/openapi/test/person_response_dto_test.dart and b/mobile/openapi/test/person_response_dto_test.dart differ diff --git a/mobile/openapi/test/person_update_dto_test.dart b/mobile/openapi/test/person_update_dto_test.dart index be3b8fb741..47de2eb877 100644 Binary files a/mobile/openapi/test/person_update_dto_test.dart and b/mobile/openapi/test/person_update_dto_test.dart differ diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 877ea38673..8865bbae25 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 f5284f3908..226cc77a9e 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 b8efa65c99..41430afaf2 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 c7eb08bfdd..52a8d0f5dc 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 4310ef431b..3ec8356601 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 106961aa89..7620145128 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 40ad593e93..b93c4bbf9d 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 0000000000..234b77dd34 --- /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 40bd71ddb3..2ca81f19c4 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 6d096772a7..b4abca792e 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 414f7b147d..24f3b33379 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 4b1eea5e52..3b78b17ceb 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 6b967e832f..c91e28c006 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}