mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web): use time buckets of person detail page (3) (#3557)
* feat: add personId to time bucket endpoints * chore: open api * feat(web): time bucket on person detail page
This commit is contained in:
		
							
								
								
									
										46
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										46
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -5111,13 +5111,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|          * @param {string} timeBucket  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'size' is not null or undefined
 | ||||
|             assertParamExists('getByTimeBucket', 'size', size) | ||||
|             // verify required parameter 'timeBucket' is not null or undefined
 | ||||
| @@ -5155,6 +5156,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 localVarQueryParameter['albumId'] = albumId; | ||||
|             } | ||||
| 
 | ||||
|             if (personId !== undefined) { | ||||
|                 localVarQueryParameter['personId'] = personId; | ||||
|             } | ||||
| 
 | ||||
|             if (isArchived !== undefined) { | ||||
|                 localVarQueryParameter['isArchived'] = isArchived; | ||||
|             } | ||||
| @@ -5430,13 +5435,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|          * @param {TimeBucketSize} size  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'size' is not null or undefined
 | ||||
|             assertParamExists('getTimeBuckets', 'size', size) | ||||
|             const localVarPath = `/asset/time-buckets`; | ||||
| @@ -5472,6 +5478,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 localVarQueryParameter['albumId'] = albumId; | ||||
|             } | ||||
| 
 | ||||
|             if (personId !== undefined) { | ||||
|                 localVarQueryParameter['personId'] = personId; | ||||
|             } | ||||
| 
 | ||||
|             if (isArchived !== undefined) { | ||||
|                 localVarQueryParameter['isArchived'] = isArchived; | ||||
|             } | ||||
| @@ -5986,14 +5996,15 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|          * @param {string} timeBucket  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options); | ||||
|         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -6055,14 +6066,15 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|          * @param {TimeBucketSize} size  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options); | ||||
|         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -6256,7 +6268,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||
|             return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -6308,7 +6320,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { | ||||
|             return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||
|             return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Get all asset of a device that are in the database, ID only. | ||||
| @@ -6625,6 +6637,13 @@ export interface AssetApiGetByTimeBucketRequest { | ||||
|      */ | ||||
|     readonly albumId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof AssetApiGetByTimeBucket | ||||
|      */ | ||||
|     readonly personId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -6758,6 +6777,13 @@ export interface AssetApiGetTimeBucketsRequest { | ||||
|      */ | ||||
|     readonly albumId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof AssetApiGetTimeBuckets | ||||
|      */ | ||||
|     readonly personId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -7111,7 +7137,7 @@ export class AssetApi extends BaseAPI { | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -7175,7 +7201,7 @@ export class AssetApi extends BaseAPI { | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
							
								
								
									
										12
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -671,7 +671,7 @@ Name | Type | Description  | Notes | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **getByTimeBucket** | ||||
| > List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key) | ||||
| > List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -698,12 +698,13 @@ final size = ; // TimeBucketSize | | ||||
| final timeBucket = timeBucket_example; // String |  | ||||
| final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final isArchived = true; // bool |  | ||||
| final isFavorite = true; // bool |  | ||||
| final key = key_example; // String |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key); | ||||
|     final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->getByTimeBucket: $e\n'); | ||||
| @@ -718,6 +719,7 @@ Name | Type | Description  | Notes | ||||
|  **timeBucket** | **String**|  |  | ||||
|  **userId** | **String**|  | [optional]  | ||||
|  **albumId** | **String**|  | [optional]  | ||||
|  **personId** | **String**|  | [optional]  | ||||
|  **isArchived** | **bool**|  | [optional]  | ||||
|  **isFavorite** | **bool**|  | [optional]  | ||||
|  **key** | **String**|  | [optional]  | ||||
| @@ -1017,7 +1019,7 @@ Name | Type | Description  | Notes | ||||
| [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) | ||||
| 
 | ||||
| # **getTimeBuckets** | ||||
| > List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key) | ||||
| > List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| @@ -1043,12 +1045,13 @@ final api_instance = AssetApi(); | ||||
| final size = ; // TimeBucketSize |  | ||||
| final userId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final albumId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String |  | ||||
| final isArchived = true; // bool |  | ||||
| final isFavorite = true; // bool |  | ||||
| final key = key_example; // String |  | ||||
| 
 | ||||
| try { | ||||
|     final result = api_instance.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key); | ||||
|     final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key); | ||||
|     print(result); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->getTimeBuckets: $e\n'); | ||||
| @@ -1062,6 +1065,7 @@ Name | Type | Description  | Notes | ||||
|  **size** | [**TimeBucketSize**](.md)|  |  | ||||
|  **userId** | **String**|  | [optional]  | ||||
|  **albumId** | **String**|  | [optional]  | ||||
|  **personId** | **String**|  | [optional]  | ||||
|  **isArchived** | **bool**|  | [optional]  | ||||
|  **isFavorite** | **bool**|  | [optional]  | ||||
|  **key** | **String**|  | [optional]  | ||||
|   | ||||
							
								
								
									
										26
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -677,12 +677,14 @@ class AssetApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [String] personId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|   Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/time-bucket'; | ||||
| 
 | ||||
| @@ -700,6 +702,9 @@ class AssetApi { | ||||
|     if (albumId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'albumId', albumId)); | ||||
|     } | ||||
|     if (personId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'personId', personId)); | ||||
|     } | ||||
|     if (isArchived != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isArchived', isArchived)); | ||||
|     } | ||||
| @@ -735,13 +740,15 @@ class AssetApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [String] personId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|     final response = await getByTimeBucketWithHttpInfo(size, timeBucket,  userId: userId, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, key: key, ); | ||||
|   Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|     final response = await getByTimeBucketWithHttpInfo(size, timeBucket,  userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, key: key, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
| @@ -1056,12 +1063,14 @@ class AssetApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [String] personId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|   Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset/time-buckets'; | ||||
| 
 | ||||
| @@ -1079,6 +1088,9 @@ class AssetApi { | ||||
|     if (albumId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'albumId', albumId)); | ||||
|     } | ||||
|     if (personId != null) { | ||||
|       queryParams.addAll(_queryParams('', 'personId', personId)); | ||||
|     } | ||||
|     if (isArchived != null) { | ||||
|       queryParams.addAll(_queryParams('', 'isArchived', isArchived)); | ||||
|     } | ||||
| @@ -1111,13 +1123,15 @@ class AssetApi { | ||||
|   /// | ||||
|   /// * [String] albumId: | ||||
|   /// | ||||
|   /// * [String] personId: | ||||
|   /// | ||||
|   /// * [bool] isArchived: | ||||
|   /// | ||||
|   /// * [bool] isFavorite: | ||||
|   /// | ||||
|   /// * [String] key: | ||||
|   Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|     final response = await getTimeBucketsWithHttpInfo(size,  userId: userId, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, key: key, ); | ||||
|   Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, String? key, }) async { | ||||
|     final response = await getTimeBucketsWithHttpInfo(size,  userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, key: key, ); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										4
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -82,7 +82,7 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, bool isArchived, bool isFavorite, String key }) async | ||||
|     //Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, String key }) async | ||||
|     test('test getByTimeBucket', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| @@ -112,7 +112,7 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, bool isArchived, bool isFavorite, String key }) async | ||||
|     //Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, String key }) async | ||||
|     test('test getTimeBuckets', () async { | ||||
|       // TODO | ||||
|     }); | ||||
|   | ||||
| @@ -1684,6 +1684,15 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "personId", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isArchived", | ||||
|             "required": false, | ||||
| @@ -1787,6 +1796,15 @@ | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "personId", | ||||
|             "required": false, | ||||
|             "in": "query", | ||||
|             "schema": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             } | ||||
|           }, | ||||
|           { | ||||
|             "name": "isArchived", | ||||
|             "required": false, | ||||
|   | ||||
| @@ -57,6 +57,7 @@ export interface TimeBucketOptions { | ||||
|   isArchived?: boolean; | ||||
|   isFavorite?: boolean; | ||||
|   albumId?: string; | ||||
|   personId?: string; | ||||
| } | ||||
|  | ||||
| export interface TimeBucketItem { | ||||
|   | ||||
| @@ -16,6 +16,9 @@ export class TimeBucketDto { | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   albumId?: string; | ||||
|  | ||||
|   @ValidateUUID({ optional: true }) | ||||
|   personId?: string; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   @Transform(toBoolean) | ||||
|   | ||||
| @@ -386,7 +386,7 @@ export class AssetRepository implements IAssetRepository { | ||||
|   } | ||||
|  | ||||
|   private getBuilder(userId: string, options: TimeBucketOptions) { | ||||
|     const { isArchived, isFavorite, albumId } = options; | ||||
|     const { isArchived, isFavorite, albumId, personId } = options; | ||||
|  | ||||
|     let builder = this.repository | ||||
|       .createQueryBuilder('asset') | ||||
| @@ -406,6 +406,13 @@ export class AssetRepository implements IAssetRepository { | ||||
|       builder = builder.andWhere('asset.isFavorite = :isFavorite', { isFavorite }); | ||||
|     } | ||||
|  | ||||
|     if (personId !== undefined) { | ||||
|       builder = builder | ||||
|         .innerJoin('asset.faces', 'faces') | ||||
|         .innerJoin('faces.person', 'person') | ||||
|         .andWhere('person.id = :personId', { personId }); | ||||
|     } | ||||
|  | ||||
|     return builder; | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										52
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										52
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -5120,13 +5120,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|          * @param {string} timeBucket  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'size' is not null or undefined
 | ||||
|             assertParamExists('getByTimeBucket', 'size', size) | ||||
|             // verify required parameter 'timeBucket' is not null or undefined
 | ||||
| @@ -5164,6 +5165,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 localVarQueryParameter['albumId'] = albumId; | ||||
|             } | ||||
| 
 | ||||
|             if (personId !== undefined) { | ||||
|                 localVarQueryParameter['personId'] = personId; | ||||
|             } | ||||
| 
 | ||||
|             if (isArchived !== undefined) { | ||||
|                 localVarQueryParameter['isArchived'] = isArchived; | ||||
|             } | ||||
| @@ -5439,13 +5444,14 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|          * @param {TimeBucketSize} size  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'size' is not null or undefined
 | ||||
|             assertParamExists('getTimeBuckets', 'size', size) | ||||
|             const localVarPath = `/asset/time-buckets`; | ||||
| @@ -5481,6 +5487,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 localVarQueryParameter['albumId'] = albumId; | ||||
|             } | ||||
| 
 | ||||
|             if (personId !== undefined) { | ||||
|                 localVarQueryParameter['personId'] = personId; | ||||
|             } | ||||
| 
 | ||||
|             if (isArchived !== undefined) { | ||||
|                 localVarQueryParameter['isArchived'] = isArchived; | ||||
|             } | ||||
| @@ -5995,14 +6005,15 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|          * @param {string} timeBucket  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options); | ||||
|         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -6064,14 +6075,15 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|          * @param {TimeBucketSize} size  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options); | ||||
|         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
| @@ -6276,14 +6288,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|          * @param {string} timeBucket  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getByTimeBucket(size, timeBucket, userId, albumId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); | ||||
|         getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise<Array<AssetResponseDto>> { | ||||
|             return localVarFp.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
| @@ -6339,14 +6352,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|          * @param {TimeBucketSize} size  | ||||
|          * @param {string} [userId]  | ||||
|          * @param {string} [albumId]  | ||||
|          * @param {string} [personId]  | ||||
|          * @param {boolean} [isArchived]  | ||||
|          * @param {boolean} [isFavorite]  | ||||
|          * @param {string} [key]  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise<Array<TimeBucketResponseDto>> { | ||||
|             return localVarFp.getTimeBuckets(size, userId, albumId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); | ||||
|         getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, key?: string, options?: any): AxiosPromise<Array<TimeBucketResponseDto>> { | ||||
|             return localVarFp.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, key, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          * Get all asset of a device that are in the database, ID only. | ||||
| @@ -6679,6 +6693,13 @@ export interface AssetApiGetByTimeBucketRequest { | ||||
|      */ | ||||
|     readonly albumId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof AssetApiGetByTimeBucket | ||||
|      */ | ||||
|     readonly personId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -6812,6 +6833,13 @@ export interface AssetApiGetTimeBucketsRequest { | ||||
|      */ | ||||
|     readonly albumId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {string} | ||||
|      * @memberof AssetApiGetTimeBuckets | ||||
|      */ | ||||
|     readonly personId?: string | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
| @@ -7165,7 +7193,7 @@ export class AssetApi extends BaseAPI { | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @@ -7229,7 +7257,7 @@ export class AssetApi extends BaseAPI { | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|         return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|   | ||||
| @@ -15,6 +15,8 @@ | ||||
|  | ||||
| :root { | ||||
|   font-family: 'Work Sans', sans-serif; | ||||
|   /* Used by layouts to ensure proper spacing between navbar and content */ | ||||
|   --navbar-height: calc(theme(spacing.18) + 4px); | ||||
| } | ||||
|  | ||||
| html { | ||||
|   | ||||
| @@ -69,6 +69,6 @@ | ||||
|     </svelte:fragment> | ||||
|   </ControlAppBar> | ||||
|   <section class="grid h-screen bg-immich-bg pl-[70px] pt-[100px] dark:bg-immich-dark-bg"> | ||||
|     <AssetGrid {assetStore} {assetInteractionStore} isAlbumSelectionMode={true} /> | ||||
|     <AssetGrid {assetStore} {assetInteractionStore} isSelectionMode={true} /> | ||||
|   </section> | ||||
| </section> | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| <script lang="ts"> | ||||
|   import type { AssetResponseDto } from '@api'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import { quintOut } from 'svelte/easing'; | ||||
|   import { fly } from 'svelte/transition'; | ||||
|   import ControlAppBar from '../shared-components/control-app-bar.svelte'; | ||||
|   import AssetSelectionViewer from '../shared-components/gallery-viewer/asset-selection-viewer.svelte'; | ||||
|  | ||||
|   const dispatch = createEventDispatcher(); | ||||
|  | ||||
|   export let assets: AssetResponseDto[]; | ||||
|  | ||||
|   let selectedAsset: AssetResponseDto | undefined = undefined; | ||||
|  | ||||
|   const handleSelectedAsset = async (event: CustomEvent) => { | ||||
|     const { asset }: { asset: AssetResponseDto } = event.detail; | ||||
|     selectedAsset = asset; | ||||
|     onClose(); | ||||
|   }; | ||||
|  | ||||
|   const onClose = () => { | ||||
|     dispatch('go-back', { selectedAsset }); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <section | ||||
|   transition:fly={{ y: 500, duration: 100, easing: quintOut }} | ||||
|   class="absolute left-0 top-0 z-[9999] h-full w-full bg-immich-bg dark:bg-immich-dark-bg" | ||||
| > | ||||
|   <ControlAppBar on:close-button-click={onClose}> | ||||
|     <svelte:fragment slot="leading">Select feature photo</svelte:fragment> | ||||
|   </ControlAppBar> | ||||
|   <section class="bg-immich-bg pl-[70px] pt-[100px] dark:bg-immich-dark-bg"> | ||||
|     <AssetSelectionViewer {assets} on:select={handleSelectedAsset} /> | ||||
|   </section> | ||||
| </section> | ||||
| @@ -118,7 +118,7 @@ | ||||
|         </div> | ||||
|       </div> | ||||
|       <div | ||||
|         class="overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray" | ||||
|         class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray" | ||||
|         style:max-height={screenHeight - 200 - 200 + 'px'} | ||||
|       > | ||||
|         <div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10"> | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| <script lang="ts"> | ||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import { api, type PersonResponseDto } from '@api'; | ||||
|   import { createEventDispatcher } from 'svelte'; | ||||
|   import Close from 'svelte-material-icons/Close.svelte'; | ||||
|   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; | ||||
|   import type { PersonResponseDto } from '../../../api/open-api'; | ||||
|   import { api } from '@api'; | ||||
|   import Merge from 'svelte-material-icons/Merge.svelte'; | ||||
|   import Button from '../elements/buttons/button.svelte'; | ||||
|   import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||
|   import Close from 'svelte-material-icons/Close.svelte'; | ||||
|   import Merge from 'svelte-material-icons/Merge.svelte'; | ||||
|   import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte'; | ||||
|   import Button from '../elements/buttons/button.svelte'; | ||||
|   import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; | ||||
|  | ||||
|   const dispatch = createEventDispatcher<{ | ||||
|     reject: void; | ||||
| @@ -39,6 +39,7 @@ | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| <FullScreenModal on:clickOutside={() => dispatch('close')}> | ||||
|   <div class="flex h-full w-full place-content-center place-items-center overflow-hidden"> | ||||
|     <div | ||||
|       class="w-[250px] max-w-[125vw] rounded-3xl border bg-immich-bg shadow-sm dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-fg md:w-[375px]" | ||||
| @@ -126,3 +127,4 @@ | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </FullScreenModal> | ||||
|   | ||||
| @@ -18,8 +18,9 @@ | ||||
|   export let assets: AssetResponseDto[]; | ||||
|   export let bucketDate: string; | ||||
|   export let bucketHeight: number; | ||||
|   export let isAlbumSelectionMode = false; | ||||
|   export let isSelectionMode = false; | ||||
|   export let viewport: Viewport; | ||||
|   export let singleSelect = false; | ||||
|  | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
| @@ -90,16 +91,12 @@ | ||||
|     assetsInDateGroup: AssetResponseDto[], | ||||
|     dateGroupTitle: string, | ||||
|   ) => { | ||||
|     if (isAlbumSelectionMode) { | ||||
|     if (isSelectionMode || $isMultiSelectState) { | ||||
|       assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if ($isMultiSelectState) { | ||||
|       assetSelectHandler(asset, assetsInDateGroup, dateGroupTitle); | ||||
|     } else { | ||||
|     assetViewingStore.setAssetId(asset.id); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const selectAssetGroupHandler = (selectAssetGroupHandler: AssetResponseDto[], dateGroupTitle: string) => { | ||||
| @@ -166,7 +163,7 @@ | ||||
|         class="mb-2 flex h-6 place-items-center text-xs font-medium text-immich-fg dark:text-immich-dark-fg md:text-sm" | ||||
|         style="width: {geometry[groupIndex].containerWidth}px" | ||||
|       > | ||||
|         {#if (hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle)} | ||||
|         {#if !singleSelect && ((hoveredDateGroup == dateGroupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroupTitle))} | ||||
|           <div | ||||
|             transition:fly={{ x: -24, duration: 200, opacity: 0.5 }} | ||||
|             class="inline-block px-2 hover:cursor-pointer" | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
|   import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; | ||||
|   import type { AssetResponseDto } from '@api'; | ||||
|   import { DateTime } from 'luxon'; | ||||
|   import { onDestroy, onMount } from 'svelte'; | ||||
|   import { createEventDispatcher, onDestroy, onMount } from 'svelte'; | ||||
|   import AssetViewer from '../asset-viewer/asset-viewer.svelte'; | ||||
|   import IntersectionObserver from '../asset-viewer/intersection-observer.svelte'; | ||||
|   import Portal from '../shared-components/portal/portal.svelte'; | ||||
| @@ -19,7 +19,8 @@ | ||||
|   import { isSearchEnabled } from '$lib/stores/search.store'; | ||||
|   import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; | ||||
|  | ||||
|   export let isAlbumSelectionMode = false; | ||||
|   export let isSelectionMode = false; | ||||
|   export let singleSelect = false; | ||||
|   export let assetStore: AssetStore; | ||||
|   export let assetInteractionStore: AssetInteractionStore; | ||||
|   export let removeAction: AssetAction | null = null; | ||||
| @@ -33,6 +34,7 @@ | ||||
|   $: timelineY = element?.scrollTop || 0; | ||||
|  | ||||
|   const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); | ||||
|   const dispatch = createEventDispatcher<{ select: AssetResponseDto }>(); | ||||
|  | ||||
|   onMount(async () => { | ||||
|     document.addEventListener('keydown', onKeyboardPress); | ||||
| @@ -173,11 +175,17 @@ | ||||
|   }; | ||||
|  | ||||
|   const handleSelectAssets = async (e: CustomEvent) => { | ||||
|     const asset = e.detail.asset; | ||||
|     const asset = e.detail.asset as AssetResponseDto; | ||||
|     if (!asset) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     dispatch('select', asset); | ||||
|  | ||||
|     if (singleSelect) { | ||||
|       element.scrollTop = 0; | ||||
|     } | ||||
|  | ||||
|     const rangeSelection = $assetSelectionCandidates.size > 0; | ||||
|     const deselect = $selectedAssets.has(asset); | ||||
|  | ||||
| @@ -308,7 +316,8 @@ | ||||
|               <AssetDateGroup | ||||
|                 {assetStore} | ||||
|                 {assetInteractionStore} | ||||
|                 {isAlbumSelectionMode} | ||||
|                 {isSelectionMode} | ||||
|                 {singleSelect} | ||||
|                 on:shift={handleScrollTimeline} | ||||
|                 on:selectAssetCandidates={handleSelectAssetCandidates} | ||||
|                 on:selectAssets={handleSelectAssets} | ||||
|   | ||||
| @@ -132,10 +132,3 @@ | ||||
|     </div> | ||||
|   </div> | ||||
| </section> | ||||
|  | ||||
| <style> | ||||
|   :root { | ||||
|     /* Used by layouts to ensure proper spacing between navbar and content */ | ||||
|     --navbar-height: calc(theme(spacing.18) + 4px); | ||||
|   } | ||||
| </style> | ||||
|   | ||||
| @@ -9,12 +9,10 @@ export const load = (async ({ locals, parent, params }) => { | ||||
|   } | ||||
|  | ||||
|   const { data: person } = await locals.api.personApi.getPerson({ id: params.personId }); | ||||
|   const { data: assets } = await locals.api.personApi.getPersonAssets({ id: params.personId }); | ||||
|   const { data: people } = await locals.api.personApi.getAllPeople({ withHidden: false }); | ||||
|  | ||||
|   return { | ||||
|     user, | ||||
|     assets, | ||||
|     person, | ||||
|     people, | ||||
|     meta: { | ||||
|   | ||||
| @@ -3,59 +3,68 @@ | ||||
|   import { page } from '$app/stores'; | ||||
|   import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte'; | ||||
|   import EditNameInput from '$lib/components/faces-page/edit-name-input.svelte'; | ||||
|   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; | ||||
|   import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; | ||||
|   import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; | ||||
|   import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; | ||||
|   import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; | ||||
|   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; | ||||
|   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; | ||||
|   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; | ||||
|   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; | ||||
|   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | ||||
|   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; | ||||
|   import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; | ||||
|   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | ||||
|   import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; | ||||
|   import { AppRoute } from '$lib/constants'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { AssetResponseDto, PersonResponseDto, api } from '@api'; | ||||
|   import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||
|   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | ||||
|   import Plus from 'svelte-material-icons/Plus.svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||
|   import SelectAll from 'svelte-material-icons/SelectAll.svelte'; | ||||
|   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||
|   import FaceThumbnailSelector from '$lib/components/faces-page/face-thumbnail-selector.svelte'; | ||||
|   import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; | ||||
|   import { | ||||
|     NotificationType, | ||||
|     notificationController, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import MergeFaceSelector from '$lib/components/faces-page/merge-face-selector.svelte'; | ||||
|   import { AppRoute } from '$lib/constants'; | ||||
|   import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store'; | ||||
|   import { AssetStore } from '$lib/stores/assets.store'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { AssetResponseDto, PersonResponseDto, TimeBucketSize, api } from '@api'; | ||||
|   import { onMount } from 'svelte'; | ||||
|   import MergeSuggestionModal from '$lib/components/faces-page/merge-suggestion-modal.svelte'; | ||||
|   import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; | ||||
|   import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte'; | ||||
|   import DotsVertical from 'svelte-material-icons/DotsVertical.svelte'; | ||||
|   import Plus from 'svelte-material-icons/Plus.svelte'; | ||||
|   import type { PageData } from './$types'; | ||||
|  | ||||
|   export let data: PageData; | ||||
|  | ||||
|   enum ViewMode { | ||||
|     VIEW_ASSETS = 'view-assets', | ||||
|     SELECT_FACE = 'select-face', | ||||
|     MERGE_FACES = 'merge-faces', | ||||
|     SUGGEST_MERGE = 'suggest-merge', | ||||
|   } | ||||
|  | ||||
|   const assetStore = new AssetStore({ | ||||
|     size: TimeBucketSize.Month, | ||||
|     isArchived: false, | ||||
|     personId: data.person.id, | ||||
|   }); | ||||
|   const assetInteractionStore = createAssetInteractionStore(); | ||||
|   const { selectedAssets, isMultiSelectState } = assetInteractionStore; | ||||
|  | ||||
|   let viewMode: ViewMode = ViewMode.VIEW_ASSETS; | ||||
|   let isEditingName = false; | ||||
|   let showFaceThumbnailSelection = false; | ||||
|   let showMergeFacePanel = false; | ||||
|   let previousRoute: string = AppRoute.EXPLORE; | ||||
|   let selectedAssets: Set<AssetResponseDto> = new Set(); | ||||
|   let showMergeModal = false; | ||||
|   let people = data.people.people; | ||||
|   let personMerge1: PersonResponseDto; | ||||
|   let personMerge2: PersonResponseDto; | ||||
|  | ||||
|   let personName = ''; | ||||
|  | ||||
|   $: isMultiSelectionMode = selectedAssets.size > 0; | ||||
|   $: isAllArchive = Array.from(selectedAssets).every((asset) => asset.isArchived); | ||||
|   $: isAllFavorite = Array.from(selectedAssets).every((asset) => asset.isFavorite); | ||||
|  | ||||
|   $: showAssets = !showMergeFacePanel && !showFaceThumbnailSelection; | ||||
|   $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived); | ||||
|   $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite); | ||||
|  | ||||
|   onMount(() => { | ||||
|     const action = $page.url.searchParams.get('action'); | ||||
|     if (action == 'merge') { | ||||
|       showMergeFacePanel = true; | ||||
|       viewMode = ViewMode.MERGE_FACES; | ||||
|     } | ||||
|   }); | ||||
|   afterNavigate(({ from }) => { | ||||
| @@ -65,35 +74,29 @@ | ||||
|     } | ||||
|   }); | ||||
|  | ||||
|   const onAssetDelete = (assetId: string) => { | ||||
|     data.assets = data.assets.filter((asset: AssetResponseDto) => asset.id !== assetId); | ||||
|   }; | ||||
|   const handleSelectAll = () => { | ||||
|     selectedAssets = new Set(data.assets); | ||||
|   }; | ||||
|   const handleSelectFeaturePhoto = async (asset: AssetResponseDto) => { | ||||
|     if (viewMode !== ViewMode.SELECT_FACE) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|   const handleSelectFeaturePhoto = async (event: CustomEvent) => { | ||||
|     showFaceThumbnailSelection = false; | ||||
|  | ||||
|     const { selectedAsset }: { selectedAsset: AssetResponseDto | undefined } = event.detail; | ||||
|  | ||||
|     if (selectedAsset) { | ||||
|       await api.personApi.updatePerson({ | ||||
|         id: data.person.id, | ||||
|         personUpdateDto: { featureFaceAssetId: selectedAsset.id }, | ||||
|       }); | ||||
|     await api.personApi.updatePerson({ id: data.person.id, personUpdateDto: { featureFaceAssetId: asset.id } }); | ||||
|  | ||||
|     // TODO: Replace by Websocket in the future | ||||
|     notificationController.show({ | ||||
|       message: 'Feature photo updated, refresh page to see changes', | ||||
|       type: NotificationType.Info, | ||||
|     }); | ||||
|     } | ||||
|  | ||||
|     assetInteractionStore.clearMultiselect(); | ||||
|     // scroll to top | ||||
|  | ||||
|     viewMode = ViewMode.VIEW_ASSETS; | ||||
|   }; | ||||
|  | ||||
|   const handleMergeSameFace = async (response: [PersonResponseDto, PersonResponseDto]) => { | ||||
|     const [personToMerge, personToBeMergedIn] = response; | ||||
|     showMergeModal = false; | ||||
|     viewMode = ViewMode.VIEW_ASSETS; | ||||
|     isEditingName = false; | ||||
|     try { | ||||
|       await api.personApi.mergePerson({ | ||||
|         id: personToBeMergedIn.id, | ||||
| @@ -116,7 +119,7 @@ | ||||
|   }; | ||||
|  | ||||
|   const changeName = async () => { | ||||
|     showMergeModal = false; | ||||
|     viewMode = ViewMode.VIEW_ASSETS; | ||||
|     data.person.name = personName; | ||||
|     try { | ||||
|       isEditingName = false; | ||||
| @@ -142,6 +145,14 @@ | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   const handleCancelEditName = () => { | ||||
|     if (viewMode === ViewMode.SUGGEST_MERGE) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     isEditingName = false; | ||||
|   }; | ||||
|  | ||||
|   const handleNameChange = async (name: string) => { | ||||
|     personName = name; | ||||
|  | ||||
| @@ -156,62 +167,87 @@ | ||||
|     if (existingPerson) { | ||||
|       personMerge2 = existingPerson; | ||||
|       personMerge1 = data.person; | ||||
|       showMergeModal = true; | ||||
|       viewMode = ViewMode.SUGGEST_MERGE; | ||||
|       return; | ||||
|     } | ||||
|     changeName(); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| {#if showMergeModal} | ||||
|   <FullScreenModal on:clickOutside={() => (showMergeModal = false)}> | ||||
| {#if viewMode === ViewMode.SUGGEST_MERGE} | ||||
|   <MergeSuggestionModal | ||||
|     {personMerge1} | ||||
|     {personMerge2} | ||||
|     {people} | ||||
|       on:close={() => (showMergeModal = false)} | ||||
|     on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} | ||||
|     on:reject={() => changeName()} | ||||
|     on:confirm={(event) => handleMergeSameFace(event.detail)} | ||||
|   /> | ||||
|   </FullScreenModal> | ||||
| {/if} | ||||
|  | ||||
| {#if isMultiSelectionMode} | ||||
|   <AssetSelectControlBar assets={selectedAssets} clearSelect={() => (selectedAssets = new Set())}> | ||||
| {#if viewMode === ViewMode.MERGE_FACES} | ||||
|   <MergeFaceSelector person={data.person} on:go-back={() => (viewMode = ViewMode.VIEW_ASSETS)} /> | ||||
| {/if} | ||||
|  | ||||
| <header> | ||||
|   {#if $isMultiSelectState} | ||||
|     <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> | ||||
|       <CreateSharedLink /> | ||||
|     <CircleIconButton title="Select all" logo={SelectAll} on:click={handleSelectAll} /> | ||||
|       <SelectAllAssets {assetStore} {assetInteractionStore} /> | ||||
|       <AssetSelectContextMenu icon={Plus} title="Add"> | ||||
|         <AddToAlbum /> | ||||
|         <AddToAlbum shared /> | ||||
|       </AssetSelectContextMenu> | ||||
|     <DeleteAssets {onAssetDelete} /> | ||||
|       <DeleteAssets onAssetDelete={(assetId) => $assetStore.removeAsset(assetId)} /> | ||||
|       <AssetSelectContextMenu icon={DotsVertical} title="Add"> | ||||
|         <DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" /> | ||||
|         <FavoriteAction menuItem removeFavorite={isAllFavorite} /> | ||||
|       <ArchiveAction menuItem unarchive={isAllArchive} onAssetArchive={(asset) => onAssetDelete(asset.id)} /> | ||||
|         <ArchiveAction | ||||
|           menuItem | ||||
|           unarchive={isAllArchive} | ||||
|           onAssetArchive={(asset) => $assetStore.removeAsset(asset.id)} | ||||
|         /> | ||||
|       </AssetSelectContextMenu> | ||||
|     </AssetSelectControlBar> | ||||
|   {:else} | ||||
|     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} | ||||
|       <ControlAppBar showBackButton backIcon={ArrowLeft} on:close-button-click={() => goto(previousRoute)}> | ||||
|         <svelte:fragment slot="trailing"> | ||||
|           <AssetSelectContextMenu icon={DotsVertical} title="Menu"> | ||||
|         <MenuOption text="Change feature photo" on:click={() => (showFaceThumbnailSelection = true)} /> | ||||
|         <MenuOption text="Merge face" on:click={() => (showMergeFacePanel = true)} /> | ||||
|             <MenuOption text="Change feature photo" on:click={() => (viewMode = ViewMode.SELECT_FACE)} /> | ||||
|             <MenuOption text="Merge face" on:click={() => (viewMode = ViewMode.MERGE_FACES)} /> | ||||
|           </AssetSelectContextMenu> | ||||
|         </svelte:fragment> | ||||
|       </ControlAppBar> | ||||
|     {/if} | ||||
|  | ||||
|     {#if viewMode === ViewMode.SELECT_FACE} | ||||
|       <ControlAppBar on:close-button-click={() => (viewMode = ViewMode.VIEW_ASSETS)}> | ||||
|         <svelte:fragment slot="leading">Select feature photo</svelte:fragment> | ||||
|       </ControlAppBar> | ||||
|     {/if} | ||||
|   {/if} | ||||
| </header> | ||||
|  | ||||
| <main class="relative h-screen overflow-hidden bg-immich-bg pt-[var(--navbar-height)] dark:bg-immich-dark-bg"> | ||||
|   <AssetGrid | ||||
|     {assetStore} | ||||
|     {assetInteractionStore} | ||||
|     isSelectionMode={viewMode === ViewMode.SELECT_FACE} | ||||
|     singleSelect={viewMode === ViewMode.SELECT_FACE} | ||||
|     on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)} | ||||
|   > | ||||
|     {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE} | ||||
|       <!-- Face information block --> | ||||
| <section class="flex place-items-center px-4 pt-24 sm:px-6"> | ||||
|       <section class="flex place-items-center p-4 sm:px-6"> | ||||
|         {#if isEditingName} | ||||
|           <EditNameInput | ||||
|             person={data.person} | ||||
|             on:change={(event) => handleNameChange(event.detail)} | ||||
|       on:cancel={() => (isEditingName = false)} | ||||
|             on:cancel={() => handleCancelEditName()} | ||||
|           /> | ||||
|         {:else} | ||||
|     <button on:click={() => (showFaceThumbnailSelection = true)}> | ||||
|           <button on:click={() => (viewMode = ViewMode.VIEW_ASSETS)}> | ||||
|             <ImageThumbnail | ||||
|               circle | ||||
|               shadow | ||||
| @@ -236,22 +272,6 @@ | ||||
|           </button> | ||||
|         {/if} | ||||
|       </section> | ||||
|  | ||||
| <!-- Gallery Block --> | ||||
| {#if showAssets} | ||||
|   <section class="relative mb-12 bg-immich-bg pt-8 dark:bg-immich-dark-bg sm:px-4"> | ||||
|     <section class="immich-scrollbar relative overflow-y-scroll"> | ||||
|       <section id="search-content" class="relative bg-immich-bg dark:bg-immich-dark-bg"> | ||||
|         <GalleryViewer assets={data.assets} showArchiveIcon={true} bind:selectedAssets /> | ||||
|       </section> | ||||
|     </section> | ||||
|   </section> | ||||
| {/if} | ||||
|  | ||||
| {#if showFaceThumbnailSelection} | ||||
|   <FaceThumbnailSelector assets={data.assets} on:go-back={handleSelectFeaturePhoto} /> | ||||
| {/if} | ||||
|  | ||||
| {#if showMergeFacePanel} | ||||
|   <MergeFaceSelector person={data.person} on:go-back={() => (showMergeFacePanel = false)} /> | ||||
|     {/if} | ||||
|   </AssetGrid> | ||||
| </main> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user