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