mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00:00 
			
		
		
		
	feat(web): manual stacking asset (#4650)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
		
							
								
								
									
										46
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										46
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -6323,11 +6323,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { |         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||||
|             // verify required parameter 'size' is not null or undefined
 |             // verify required parameter 'size' is not null or undefined
 | ||||||
|             assertParamExists('getByTimeBucket', 'size', size) |             assertParamExists('getByTimeBucket', 'size', size) | ||||||
|             // verify required parameter 'timeBucket' is not null or undefined
 |             // verify required parameter 'timeBucket' is not null or undefined
 | ||||||
| @@ -6381,6 +6382,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|                 localVarQueryParameter['isTrashed'] = isTrashed; |                 localVarQueryParameter['isTrashed'] = isTrashed; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (withStacked !== undefined) { | ||||||
|  |                 localVarQueryParameter['withStacked'] = withStacked; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             if (timeBucket !== undefined) { |             if (timeBucket !== undefined) { | ||||||
|                 localVarQueryParameter['timeBucket'] = timeBucket; |                 localVarQueryParameter['timeBucket'] = timeBucket; | ||||||
|             } |             } | ||||||
| @@ -6691,11 +6696,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { |         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||||
|             // verify required parameter 'size' is not null or undefined
 |             // verify required parameter 'size' is not null or undefined
 | ||||||
|             assertParamExists('getTimeBuckets', 'size', size) |             assertParamExists('getTimeBuckets', 'size', size) | ||||||
|             const localVarPath = `/asset/time-buckets`; |             const localVarPath = `/asset/time-buckets`; | ||||||
| @@ -6747,6 +6753,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|                 localVarQueryParameter['isTrashed'] = isTrashed; |                 localVarQueryParameter['isTrashed'] = isTrashed; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (withStacked !== undefined) { | ||||||
|  |                 localVarQueryParameter['withStacked'] = withStacked; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             if (key !== undefined) { |             if (key !== undefined) { | ||||||
|                 localVarQueryParameter['key'] = key; |                 localVarQueryParameter['key'] = key; | ||||||
|             } |             } | ||||||
| @@ -7485,12 +7495,13 @@ export const AssetApiFp = function(configuration?: Configuration) { | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { |         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: 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, isTrashed, key, options); |             const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
| @@ -7565,12 +7576,13 @@ export const AssetApiFp = function(configuration?: Configuration) { | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { |         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { | ||||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options); |             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
| @@ -7815,7 +7827,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | |||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { |         getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { | ||||||
|             return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, 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.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
|          *  |          *  | ||||||
| @@ -7876,7 +7888,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | |||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { |         getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { | ||||||
|             return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath)); |             return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
|          * Get all asset of a device that are in the database, ID only. |          * Get all asset of a device that are in the database, ID only. | ||||||
| @@ -8251,6 +8263,13 @@ export interface AssetApiGetByTimeBucketRequest { | |||||||
|      */ |      */ | ||||||
|     readonly isTrashed?: boolean |     readonly isTrashed?: boolean | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof AssetApiGetByTimeBucket | ||||||
|  |      */ | ||||||
|  |     readonly withStacked?: boolean | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
| @@ -8405,6 +8424,13 @@ export interface AssetApiGetTimeBucketsRequest { | |||||||
|      */ |      */ | ||||||
|     readonly isTrashed?: boolean |     readonly isTrashed?: boolean | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof AssetApiGetTimeBuckets | ||||||
|  |      */ | ||||||
|  |     readonly withStacked?: boolean | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
| @@ -8820,7 +8846,7 @@ export class AssetApi extends BaseAPI { | |||||||
|      * @memberof AssetApi |      * @memberof AssetApi | ||||||
|      */ |      */ | ||||||
|     public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { |     public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { | ||||||
|         return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, 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.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @@ -8895,7 +8921,7 @@ export class AssetApi extends BaseAPI { | |||||||
|      * @memberof AssetApi |      * @memberof AssetApi | ||||||
|      */ |      */ | ||||||
|     public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { |     public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { | ||||||
|         return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, 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.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -669,7 +669,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) | [[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** | # **getByTimeBucket** | ||||||
| > List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key) | > List<AssetResponseDto> getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -700,10 +700,11 @@ final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | | |||||||
| final isArchived = true; // bool |  | final isArchived = true; // bool |  | ||||||
| final isFavorite = true; // bool |  | final isFavorite = true; // bool |  | ||||||
| final isTrashed = true; // bool |  | final isTrashed = true; // bool |  | ||||||
|  | final withStacked = true; // bool |  | ||||||
| final key = key_example; // String |  | final key = key_example; // String |  | ||||||
| 
 | 
 | ||||||
| try { | try { | ||||||
|     final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, key); |     final result = api_instance.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key); | ||||||
|     print(result); |     print(result); | ||||||
| } catch (e) { | } catch (e) { | ||||||
|     print('Exception when calling AssetApi->getByTimeBucket: $e\n'); |     print('Exception when calling AssetApi->getByTimeBucket: $e\n'); | ||||||
| @@ -722,6 +723,7 @@ Name | Type | Description  | Notes | |||||||
|  **isArchived** | **bool**|  | [optional]  |  **isArchived** | **bool**|  | [optional]  | ||||||
|  **isFavorite** | **bool**|  | [optional]  |  **isFavorite** | **bool**|  | [optional]  | ||||||
|  **isTrashed** | **bool**|  | [optional]  |  **isTrashed** | **bool**|  | [optional]  | ||||||
|  |  **withStacked** | **bool**|  | [optional]  | ||||||
|  **key** | **String**|  | [optional]  |  **key** | **String**|  | [optional]  | ||||||
| 
 | 
 | ||||||
| ### Return type | ### Return type | ||||||
| @@ -1072,7 +1074,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) | [[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** | # **getTimeBuckets** | ||||||
| > List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key) | > List<TimeBucketResponseDto> getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @@ -1102,10 +1104,11 @@ final personId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | | |||||||
| final isArchived = true; // bool |  | final isArchived = true; // bool |  | ||||||
| final isFavorite = true; // bool |  | final isFavorite = true; // bool |  | ||||||
| final isTrashed = true; // bool |  | final isTrashed = true; // bool |  | ||||||
|  | final withStacked = true; // bool |  | ||||||
| final key = key_example; // String |  | final key = key_example; // String |  | ||||||
| 
 | 
 | ||||||
| try { | try { | ||||||
|     final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key); |     final result = api_instance.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key); | ||||||
|     print(result); |     print(result); | ||||||
| } catch (e) { | } catch (e) { | ||||||
|     print('Exception when calling AssetApi->getTimeBuckets: $e\n'); |     print('Exception when calling AssetApi->getTimeBuckets: $e\n'); | ||||||
| @@ -1123,6 +1126,7 @@ Name | Type | Description  | Notes | |||||||
|  **isArchived** | **bool**|  | [optional]  |  **isArchived** | **bool**|  | [optional]  | ||||||
|  **isFavorite** | **bool**|  | [optional]  |  **isFavorite** | **bool**|  | [optional]  | ||||||
|  **isTrashed** | **bool**|  | [optional]  |  **isTrashed** | **bool**|  | [optional]  | ||||||
|  |  **withStacked** | **bool**|  | [optional]  | ||||||
|  **key** | **String**|  | [optional]  |  **key** | **String**|  | [optional]  | ||||||
| 
 | 
 | ||||||
| ### Return type | ### Return type | ||||||
|   | |||||||
							
								
								
									
										26
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -652,8 +652,10 @@ class AssetApi { | |||||||
|   /// |   /// | ||||||
|   /// * [bool] isTrashed: |   /// * [bool] isTrashed: | ||||||
|   /// |   /// | ||||||
|  |   /// * [bool] withStacked: | ||||||
|  |   /// | ||||||
|   /// * [String] key: |   /// * [String] key: | ||||||
|   Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async { |   Future<Response> getByTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { | ||||||
|     // ignore: prefer_const_declarations |     // ignore: prefer_const_declarations | ||||||
|     final path = r'/asset/time-bucket'; |     final path = r'/asset/time-bucket'; | ||||||
| 
 | 
 | ||||||
| @@ -682,6 +684,9 @@ class AssetApi { | |||||||
|     } |     } | ||||||
|     if (isTrashed != null) { |     if (isTrashed != null) { | ||||||
|       queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); |       queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); | ||||||
|  |     } | ||||||
|  |     if (withStacked != null) { | ||||||
|  |       queryParams.addAll(_queryParams('', 'withStacked', withStacked)); | ||||||
|     } |     } | ||||||
|       queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); |       queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); | ||||||
|     if (key != null) { |     if (key != null) { | ||||||
| @@ -720,9 +725,11 @@ class AssetApi { | |||||||
|   /// |   /// | ||||||
|   /// * [bool] isTrashed: |   /// * [bool] isTrashed: | ||||||
|   /// |   /// | ||||||
|  |   /// * [bool] withStacked: | ||||||
|  |   /// | ||||||
|   /// * [String] key: |   /// * [String] key: | ||||||
|   Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async { |   Future<List<AssetResponseDto>?> getByTimeBucket(TimeBucketSize size, String timeBucket, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { | ||||||
|     final response = await getByTimeBucketWithHttpInfo(size, timeBucket,  userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, ); |     final response = await getByTimeBucketWithHttpInfo(size, timeBucket,  userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, withStacked: withStacked, key: key, ); | ||||||
|     if (response.statusCode >= HttpStatus.badRequest) { |     if (response.statusCode >= HttpStatus.badRequest) { | ||||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); |       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||||
|     } |     } | ||||||
| @@ -1085,8 +1092,10 @@ class AssetApi { | |||||||
|   /// |   /// | ||||||
|   /// * [bool] isTrashed: |   /// * [bool] isTrashed: | ||||||
|   /// |   /// | ||||||
|  |   /// * [bool] withStacked: | ||||||
|  |   /// | ||||||
|   /// * [String] key: |   /// * [String] key: | ||||||
|   Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async { |   Future<Response> getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { | ||||||
|     // ignore: prefer_const_declarations |     // ignore: prefer_const_declarations | ||||||
|     final path = r'/asset/time-buckets'; |     final path = r'/asset/time-buckets'; | ||||||
| 
 | 
 | ||||||
| @@ -1116,6 +1125,9 @@ class AssetApi { | |||||||
|     if (isTrashed != null) { |     if (isTrashed != null) { | ||||||
|       queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); |       queryParams.addAll(_queryParams('', 'isTrashed', isTrashed)); | ||||||
|     } |     } | ||||||
|  |     if (withStacked != null) { | ||||||
|  |       queryParams.addAll(_queryParams('', 'withStacked', withStacked)); | ||||||
|  |     } | ||||||
|     if (key != null) { |     if (key != null) { | ||||||
|       queryParams.addAll(_queryParams('', 'key', key)); |       queryParams.addAll(_queryParams('', 'key', key)); | ||||||
|     } |     } | ||||||
| @@ -1150,9 +1162,11 @@ class AssetApi { | |||||||
|   /// |   /// | ||||||
|   /// * [bool] isTrashed: |   /// * [bool] isTrashed: | ||||||
|   /// |   /// | ||||||
|  |   /// * [bool] withStacked: | ||||||
|  |   /// | ||||||
|   /// * [String] key: |   /// * [String] key: | ||||||
|   Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, }) async { |   Future<List<TimeBucketResponseDto>?> getTimeBuckets(TimeBucketSize size, { String? userId, String? albumId, String? personId, bool? isArchived, bool? isFavorite, bool? isTrashed, bool? withStacked, String? key, }) async { | ||||||
|     final response = await getTimeBucketsWithHttpInfo(size,  userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, ); |     final response = await getTimeBucketsWithHttpInfo(size,  userId: userId, albumId: albumId, personId: personId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, withStacked: withStacked, key: key, ); | ||||||
|     if (response.statusCode >= HttpStatus.badRequest) { |     if (response.statusCode >= HttpStatus.badRequest) { | ||||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); |       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
									
									
									
								
							| @@ -80,7 +80,7 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     //Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, String key }) async |     //Future<List<AssetResponseDto>> getByTimeBucket(TimeBucketSize size, String timeBucket, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, bool withStacked, String key }) async | ||||||
|     test('test getByTimeBucket', () async { |     test('test getByTimeBucket', () async { | ||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| @@ -115,7 +115,7 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|     //Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, String key }) async |     //Future<List<TimeBucketResponseDto>> getTimeBuckets(TimeBucketSize size, { String userId, String albumId, String personId, bool isArchived, bool isFavorite, bool isTrashed, bool withStacked, String key }) async | ||||||
|     test('test getTimeBuckets', () async { |     test('test getTimeBuckets', () async { | ||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -1841,6 +1841,14 @@ | |||||||
|               "type": "boolean" |               "type": "boolean" | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|  |           { | ||||||
|  |             "name": "withStacked", | ||||||
|  |             "required": false, | ||||||
|  |             "in": "query", | ||||||
|  |             "schema": { | ||||||
|  |               "type": "boolean" | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|           { |           { | ||||||
|             "name": "timeBucket", |             "name": "timeBucket", | ||||||
|             "required": true, |             "required": true, | ||||||
| @@ -1961,6 +1969,14 @@ | |||||||
|               "type": "boolean" |               "type": "boolean" | ||||||
|             } |             } | ||||||
|           }, |           }, | ||||||
|  |           { | ||||||
|  |             "name": "withStacked", | ||||||
|  |             "required": false, | ||||||
|  |             "in": "query", | ||||||
|  |             "schema": { | ||||||
|  |               "type": "boolean" | ||||||
|  |             } | ||||||
|  |           }, | ||||||
|           { |           { | ||||||
|             "name": "key", |             "name": "key", | ||||||
|             "required": false, |             "required": false, | ||||||
|   | |||||||
| @@ -201,7 +201,7 @@ export class AssetService { | |||||||
|     await this.timeBucketChecks(authUser, dto); |     await this.timeBucketChecks(authUser, dto); | ||||||
|     const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto); |     const assets = await this.assetRepository.getByTimeBucket(dto.timeBucket, dto); | ||||||
|     if (authUser.isShowMetadata) { |     if (authUser.isShowMetadata) { | ||||||
|       return assets.map((asset) => mapAsset(asset)); |       return assets.map((asset) => mapAsset(asset, { withStack: true })); | ||||||
|     } else { |     } else { | ||||||
|       return assets.map((asset) => mapAsset(asset, { stripMetadata: true })); |       return assets.map((asset) => mapAsset(asset, { stripMetadata: true })); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -33,6 +33,11 @@ export class TimeBucketDto { | |||||||
|   @IsBoolean() |   @IsBoolean() | ||||||
|   @Transform(toBoolean) |   @Transform(toBoolean) | ||||||
|   isTrashed?: boolean; |   isTrashed?: boolean; | ||||||
|  |  | ||||||
|  |   @Optional() | ||||||
|  |   @IsBoolean() | ||||||
|  |   @Transform(toBoolean) | ||||||
|  |   withStacked?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class TimeBucketAssetDto extends TimeBucketDto { | export class TimeBucketAssetDto extends TimeBucketDto { | ||||||
|   | |||||||
| @@ -65,6 +65,7 @@ export interface TimeBucketOptions { | |||||||
|   albumId?: string; |   albumId?: string; | ||||||
|   personId?: string; |   personId?: string; | ||||||
|   userId?: string; |   userId?: string; | ||||||
|  |   withStacked?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface TimeBucketItem { | export interface TimeBucketItem { | ||||||
|   | |||||||
| @@ -30,7 +30,9 @@ const truncateMap: Record<TimeBucketSize, string> = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const dateTrunc = (options: TimeBucketOptions) => | const dateTrunc = (options: TimeBucketOptions) => | ||||||
|   `(date_trunc('${truncateMap[options.size]}', ("localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`; |   `(date_trunc('${ | ||||||
|  |     truncateMap[options.size] | ||||||
|  |   }', (asset."localDateTime" at time zone 'UTC')) at time zone 'UTC')::timestamptz`; | ||||||
|  |  | ||||||
| @Injectable() | @Injectable() | ||||||
| export class AssetRepository implements IAssetRepository { | export class AssetRepository implements IAssetRepository { | ||||||
| @@ -505,13 +507,14 @@ export class AssetRepository implements IAssetRepository { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private getBuilder(options: TimeBucketOptions) { |   private getBuilder(options: TimeBucketOptions) { | ||||||
|     const { isArchived, isFavorite, isTrashed, albumId, personId, userId } = options; |     const { isArchived, isFavorite, isTrashed, albumId, personId, userId, withStacked } = options; | ||||||
|  |  | ||||||
|     let builder = this.repository |     let builder = this.repository | ||||||
|       .createQueryBuilder('asset') |       .createQueryBuilder('asset') | ||||||
|       .where('asset.isVisible = true') |       .where('asset.isVisible = true') | ||||||
|       .andWhere('asset.fileCreatedAt < NOW()') |       .andWhere('asset.fileCreatedAt < NOW()') | ||||||
|       .leftJoinAndSelect('asset.exifInfo', 'exifInfo'); |       .leftJoinAndSelect('asset.exifInfo', 'exifInfo') | ||||||
|  |       .leftJoinAndSelect('asset.stack', 'stack'); | ||||||
|  |  | ||||||
|     if (albumId) { |     if (albumId) { | ||||||
|       builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId }); |       builder = builder.leftJoin('asset.albums', 'album').andWhere('album.id = :albumId', { albumId }); | ||||||
| @@ -540,11 +543,9 @@ export class AssetRepository implements IAssetRepository { | |||||||
|         .andWhere('person.id = :personId', { personId }); |         .andWhere('person.id = :personId', { personId }); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // Hide stack children only in main timeline |     if (withStacked) { | ||||||
|     // Uncomment after adding support for stacked assets in web client |       builder = builder.andWhere('asset.stackParentId IS NULL'); | ||||||
|     // if (!isArchived && !isFavorite && !personId && !albumId && !isTrashed) { |     } | ||||||
|     //   builder = builder.andWhere('asset.stackParent IS NULL'); |  | ||||||
|     // } |  | ||||||
|  |  | ||||||
|     return builder; |     return builder; | ||||||
|   } |   } | ||||||
|   | |||||||
							
								
								
									
										46
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										46
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -6323,11 +6323,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { |         getByTimeBucket: async (size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||||
|             // verify required parameter 'size' is not null or undefined
 |             // verify required parameter 'size' is not null or undefined
 | ||||||
|             assertParamExists('getByTimeBucket', 'size', size) |             assertParamExists('getByTimeBucket', 'size', size) | ||||||
|             // verify required parameter 'timeBucket' is not null or undefined
 |             // verify required parameter 'timeBucket' is not null or undefined
 | ||||||
| @@ -6381,6 +6382,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|                 localVarQueryParameter['isTrashed'] = isTrashed; |                 localVarQueryParameter['isTrashed'] = isTrashed; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (withStacked !== undefined) { | ||||||
|  |                 localVarQueryParameter['withStacked'] = withStacked; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             if (timeBucket !== undefined) { |             if (timeBucket !== undefined) { | ||||||
|                 localVarQueryParameter['timeBucket'] = timeBucket; |                 localVarQueryParameter['timeBucket'] = timeBucket; | ||||||
|             } |             } | ||||||
| @@ -6691,11 +6696,12 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { |         getTimeBuckets: async (size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||||
|             // verify required parameter 'size' is not null or undefined
 |             // verify required parameter 'size' is not null or undefined
 | ||||||
|             assertParamExists('getTimeBuckets', 'size', size) |             assertParamExists('getTimeBuckets', 'size', size) | ||||||
|             const localVarPath = `/asset/time-buckets`; |             const localVarPath = `/asset/time-buckets`; | ||||||
| @@ -6747,6 +6753,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | |||||||
|                 localVarQueryParameter['isTrashed'] = isTrashed; |                 localVarQueryParameter['isTrashed'] = isTrashed; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  |             if (withStacked !== undefined) { | ||||||
|  |                 localVarQueryParameter['withStacked'] = withStacked; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|             if (key !== undefined) { |             if (key !== undefined) { | ||||||
|                 localVarQueryParameter['key'] = key; |                 localVarQueryParameter['key'] = key; | ||||||
|             } |             } | ||||||
| @@ -7485,12 +7495,13 @@ export const AssetApiFp = function(configuration?: Configuration) { | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<AssetResponseDto>>> { |         async getByTimeBucket(size: TimeBucketSize, timeBucket: string, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: 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, isTrashed, key, options); |             const localVarAxiosArgs = await localVarAxiosParamCreator.getByTimeBucket(size, timeBucket, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
| @@ -7565,12 +7576,13 @@ export const AssetApiFp = function(configuration?: Configuration) { | |||||||
|          * @param {boolean} [isArchived]  |          * @param {boolean} [isArchived]  | ||||||
|          * @param {boolean} [isFavorite]  |          * @param {boolean} [isFavorite]  | ||||||
|          * @param {boolean} [isTrashed]  |          * @param {boolean} [isTrashed]  | ||||||
|  |          * @param {boolean} [withStacked]  | ||||||
|          * @param {string} [key]  |          * @param {string} [key]  | ||||||
|          * @param {*} [options] Override http request option. |          * @param {*} [options] Override http request option. | ||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { |         async getTimeBuckets(size: TimeBucketSize, userId?: string, albumId?: string, personId?: string, isArchived?: boolean, isFavorite?: boolean, isTrashed?: boolean, withStacked?: boolean, key?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<TimeBucketResponseDto>>> { | ||||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, key, options); |             const localVarAxiosArgs = await localVarAxiosParamCreator.getTimeBuckets(size, userId, albumId, personId, isArchived, isFavorite, isTrashed, withStacked, key, options); | ||||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); |             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
| @@ -7815,7 +7827,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | |||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { |         getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig): AxiosPromise<Array<AssetResponseDto>> { | ||||||
|             return localVarFp.getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, 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.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
|          *  |          *  | ||||||
| @@ -7876,7 +7888,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | |||||||
|          * @throws {RequiredError} |          * @throws {RequiredError} | ||||||
|          */ |          */ | ||||||
|         getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { |         getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig): AxiosPromise<Array<TimeBucketResponseDto>> { | ||||||
|             return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.key, options).then((request) => request(axios, basePath)); |             return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(axios, basePath)); | ||||||
|         }, |         }, | ||||||
|         /** |         /** | ||||||
|          * Get all asset of a device that are in the database, ID only. |          * Get all asset of a device that are in the database, ID only. | ||||||
| @@ -8251,6 +8263,13 @@ export interface AssetApiGetByTimeBucketRequest { | |||||||
|      */ |      */ | ||||||
|     readonly isTrashed?: boolean |     readonly isTrashed?: boolean | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof AssetApiGetByTimeBucket | ||||||
|  |      */ | ||||||
|  |     readonly withStacked?: boolean | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
| @@ -8405,6 +8424,13 @@ export interface AssetApiGetTimeBucketsRequest { | |||||||
|      */ |      */ | ||||||
|     readonly isTrashed?: boolean |     readonly isTrashed?: boolean | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {boolean} | ||||||
|  |      * @memberof AssetApiGetTimeBuckets | ||||||
|  |      */ | ||||||
|  |     readonly withStacked?: boolean | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      *  |      *  | ||||||
|      * @type {string} |      * @type {string} | ||||||
| @@ -8820,7 +8846,7 @@ export class AssetApi extends BaseAPI { | |||||||
|      * @memberof AssetApi |      * @memberof AssetApi | ||||||
|      */ |      */ | ||||||
|     public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { |     public getByTimeBucket(requestParameters: AssetApiGetByTimeBucketRequest, options?: AxiosRequestConfig) { | ||||||
|         return AssetApiFp(this.configuration).getByTimeBucket(requestParameters.size, requestParameters.timeBucket, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, 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.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @@ -8895,7 +8921,7 @@ export class AssetApi extends BaseAPI { | |||||||
|      * @memberof AssetApi |      * @memberof AssetApi | ||||||
|      */ |      */ | ||||||
|     public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { |     public getTimeBuckets(requestParameters: AssetApiGetTimeBucketsRequest, options?: AxiosRequestConfig) { | ||||||
|         return AssetApiFp(this.configuration).getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, 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.isTrashed, requestParameters.withStacked, requestParameters.key, options).then((request) => request(this.axios, this.basePath)); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|   | |||||||
| @@ -32,10 +32,11 @@ | |||||||
|   export let showDownloadButton: boolean; |   export let showDownloadButton: boolean; | ||||||
|   export let showDetailButton: boolean; |   export let showDetailButton: boolean; | ||||||
|   export let showSlideshow = false; |   export let showSlideshow = false; | ||||||
|  |   export let hasStackChildern = false; | ||||||
|  |  | ||||||
|   $: isOwner = asset.ownerId === $page.data.user?.id; |   $: isOwner = asset.ownerId === $page.data.user?.id; | ||||||
|  |  | ||||||
|   type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob' | 'playSlideShow'; |   type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob' | 'playSlideShow' | 'unstack'; | ||||||
|  |  | ||||||
|   const dispatch = createEventDispatcher<{ |   const dispatch = createEventDispatcher<{ | ||||||
|     goBack: void; |     goBack: void; | ||||||
| @@ -51,6 +52,7 @@ | |||||||
|     asProfileImage: void; |     asProfileImage: void; | ||||||
|     runJob: AssetJobName; |     runJob: AssetJobName; | ||||||
|     playSlideShow: void; |     playSlideShow: void; | ||||||
|  |     unstack: void; | ||||||
|   }>(); |   }>(); | ||||||
|  |  | ||||||
|   let contextMenuPosition = { x: 0, y: 0 }; |   let contextMenuPosition = { x: 0, y: 0 }; | ||||||
| @@ -173,6 +175,11 @@ | |||||||
|                 text={asset.isArchived ? 'Unarchive' : 'Archive'} |                 text={asset.isArchived ? 'Unarchive' : 'Archive'} | ||||||
|               /> |               /> | ||||||
|               <MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" /> |               <MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" /> | ||||||
|  |  | ||||||
|  |               {#if hasStackChildern} | ||||||
|  |                 <MenuOption on:click={() => onMenuClick('unstack')} text="Un-Stack" /> | ||||||
|  |               {/if} | ||||||
|  |  | ||||||
|               <MenuOption |               <MenuOption | ||||||
|                 on:click={() => onJobClick(AssetJobName.RefreshMetadata)} |                 on:click={() => onJobClick(AssetJobName.RefreshMetadata)} | ||||||
|                 text={api.getAssetJobName(AssetJobName.RefreshMetadata)} |                 text={api.getAssetJobName(AssetJobName.RefreshMetadata)} | ||||||
|   | |||||||
| @@ -25,6 +25,8 @@ | |||||||
|   import { featureFlags } from '$lib/stores/server-config.store'; |   import { featureFlags } from '$lib/stores/server-config.store'; | ||||||
|   import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js'; |   import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiImageBrokenVariant, mdiPause, mdiPlay } from '@mdi/js'; | ||||||
|   import Icon from '$lib/components/elements/icon.svelte'; |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |   import Thumbnail from '../assets/thumbnail/thumbnail.svelte'; | ||||||
|  |   import { stackAssetsStore } from '$lib/stores/stacked-asset.store'; | ||||||
|  |  | ||||||
|   export let assetStore: AssetStore | null = null; |   export let assetStore: AssetStore | null = null; | ||||||
|   export let asset: AssetResponseDto; |   export let asset: AssetResponseDto; | ||||||
| @@ -32,6 +34,7 @@ | |||||||
|   export let sharedLink: SharedLinkResponseDto | undefined = undefined; |   export let sharedLink: SharedLinkResponseDto | undefined = undefined; | ||||||
|   $: isTrashEnabled = $featureFlags.trash; |   $: isTrashEnabled = $featureFlags.trash; | ||||||
|   export let force = false; |   export let force = false; | ||||||
|  |   export let withStacked = false; | ||||||
|  |  | ||||||
|   const dispatch = createEventDispatcher<{ |   const dispatch = createEventDispatcher<{ | ||||||
|     archived: AssetResponseDto; |     archived: AssetResponseDto; | ||||||
| @@ -41,6 +44,7 @@ | |||||||
|     close: void; |     close: void; | ||||||
|     next: void; |     next: void; | ||||||
|     previous: void; |     previous: void; | ||||||
|  |     unstack: void; | ||||||
|   }>(); |   }>(); | ||||||
|  |  | ||||||
|   let appearsInAlbums: AlbumResponseDto[] = []; |   let appearsInAlbums: AlbumResponseDto[] = []; | ||||||
| @@ -52,6 +56,21 @@ | |||||||
|   let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; |   let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; | ||||||
|   let shouldShowDetailButton = asset.hasMetadata; |   let shouldShowDetailButton = asset.hasMetadata; | ||||||
|   let canCopyImagesToClipboard: boolean; |   let canCopyImagesToClipboard: boolean; | ||||||
|  |   let previewStackedAsset: AssetResponseDto | undefined; | ||||||
|  |   $: displayedAsset = previewStackedAsset || asset; | ||||||
|  |  | ||||||
|  |   $: { | ||||||
|  |     if (asset.stackCount && asset.stack) { | ||||||
|  |       $stackAssetsStore = asset.stack; | ||||||
|  |       $stackAssetsStore = [...$stackAssetsStore, asset].sort( | ||||||
|  |         (a, b) => new Date(b.fileCreatedAt).getTime() - new Date(a.fileCreatedAt).getTime(), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!$stackAssetsStore.map((a) => a.id).includes(asset.id)) { | ||||||
|  |       $stackAssetsStore = []; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo); |   const onKeyboardPress = (keyInfo: KeyboardEvent) => handleKeyboardPress(keyInfo); | ||||||
|  |  | ||||||
| @@ -66,6 +85,15 @@ | |||||||
|     // TODO: Move to regular import once the package correctly supports ESM. |     // TODO: Move to regular import once the package correctly supports ESM. | ||||||
|     const module = await import('copy-image-clipboard'); |     const module = await import('copy-image-clipboard'); | ||||||
|     canCopyImagesToClipboard = module.canCopyImagesToClipboard(); |     canCopyImagesToClipboard = module.canCopyImagesToClipboard(); | ||||||
|  |  | ||||||
|  |     if (asset.stackCount && asset.stack) { | ||||||
|  |       $stackAssetsStore = asset.stack; | ||||||
|  |       $stackAssetsStore = [...$stackAssetsStore, asset].sort( | ||||||
|  |         (a, b) => new Date(a.fileCreatedAt).getTime() - new Date(b.fileCreatedAt).getTime(), | ||||||
|  |       ); | ||||||
|  |     } else { | ||||||
|  |       $stackAssetsStore = []; | ||||||
|  |     } | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   onDestroy(() => { |   onDestroy(() => { | ||||||
| @@ -351,6 +379,35 @@ | |||||||
|       progressBar.restart(false); |       progressBar.restart(false); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const handleStackedAssetMouseEvent = (e: CustomEvent<{ isMouseOver: boolean }>, asset: AssetResponseDto) => { | ||||||
|  |     const { isMouseOver } = e.detail; | ||||||
|  |  | ||||||
|  |     if (isMouseOver) { | ||||||
|  |       previewStackedAsset = asset; | ||||||
|  |     } else { | ||||||
|  |       previewStackedAsset = undefined; | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const handleUnstack = async () => { | ||||||
|  |     try { | ||||||
|  |       const ids = $stackAssetsStore.map(({ id }) => id); | ||||||
|  |       await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, removeParent: true } }); | ||||||
|  |       for (const child of $stackAssetsStore) { | ||||||
|  |         child.stackParentId = null; | ||||||
|  |         assetStore?.addAsset(child); | ||||||
|  |       } | ||||||
|  |       asset.stackCount = 0; | ||||||
|  |       asset.stack = []; | ||||||
|  |       assetStore?.updateAsset(asset); | ||||||
|  |  | ||||||
|  |       dispatch('unstack'); | ||||||
|  |       notificationController.show({ type: NotificationType.Info, message: 'Un-stacked', timeout: 1500 }); | ||||||
|  |     } catch (error) { | ||||||
|  |       await handleError(error, `Unable to unstack`); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section | <section | ||||||
| @@ -390,6 +447,7 @@ | |||||||
|         showDownloadButton={shouldShowDownloadButton} |         showDownloadButton={shouldShowDownloadButton} | ||||||
|         showDetailButton={shouldShowDetailButton} |         showDetailButton={shouldShowDetailButton} | ||||||
|         showSlideshow={!!assetStore} |         showSlideshow={!!assetStore} | ||||||
|  |         hasStackChildern={$stackAssetsStore.length > 0} | ||||||
|         on:goBack={closeViewer} |         on:goBack={closeViewer} | ||||||
|         on:showDetail={showDetailInfoHandler} |         on:showDetail={showDetailInfoHandler} | ||||||
|         on:download={() => downloadFile(asset)} |         on:download={() => downloadFile(asset)} | ||||||
| @@ -403,6 +461,7 @@ | |||||||
|         on:asProfileImage={() => (isShowProfileImageCrop = true)} |         on:asProfileImage={() => (isShowProfileImageCrop = true)} | ||||||
|         on:runJob={({ detail: job }) => handleRunJob(job)} |         on:runJob={({ detail: job }) => handleRunJob(job)} | ||||||
|         on:playSlideShow={handlePlaySlideshow} |         on:playSlideShow={handlePlaySlideshow} | ||||||
|  |         on:unstack={handleUnstack} | ||||||
|       /> |       /> | ||||||
|     {/if} |     {/if} | ||||||
|   </div> |   </div> | ||||||
| @@ -413,41 +472,95 @@ | |||||||
|     </div> |     </div> | ||||||
|   {/if} |   {/if} | ||||||
|  |  | ||||||
|  |   <!-- Asset Viewer --> | ||||||
|   <div class="col-span-4 col-start-1 row-span-full row-start-1"> |   <div class="col-span-4 col-start-1 row-span-full row-start-1"> | ||||||
|     {#key asset.id} |     <!-- Condition to show preview of stacked asset on hovered --> | ||||||
|       {#if !asset.resized} |     {#if displayedAsset} | ||||||
|         <div class="flex h-full w-full justify-center"> |       {#key displayedAsset.id} | ||||||
|           <div |         {#if displayedAsset.type === AssetTypeEnum.Image} | ||||||
|             class="px-auto flex aspect-square h-full items-center justify-center bg-gray-100 dark:bg-immich-dark-gray" |           <PhotoViewer asset={displayedAsset} on:close={closeViewer} haveFadeTransition={false} /> | ||||||
|           > |  | ||||||
|             <Icon path={mdiImageBrokenVariant} size="25%" /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
|       {:else if asset.type === AssetTypeEnum.Image} |  | ||||||
|         {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} |  | ||||||
|           <VideoViewer |  | ||||||
|             assetId={asset.livePhotoVideoId} |  | ||||||
|             on:close={closeViewer} |  | ||||||
|             on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} |  | ||||||
|           /> |  | ||||||
|         {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath |  | ||||||
|               .toLowerCase() |  | ||||||
|               .endsWith('.insp'))} |  | ||||||
|           <PanoramaViewer {asset} /> |  | ||||||
|         {:else} |         {:else} | ||||||
|           <PhotoViewer {asset} on:close={closeViewer} /> |           <VideoViewer | ||||||
|  |             assetId={displayedAsset.id} | ||||||
|  |             on:close={closeViewer} | ||||||
|  |             on:onVideoEnded={handleVideoEnded} | ||||||
|  |             on:onVideoStarted={handleVideoStarted} | ||||||
|  |           /> | ||||||
|         {/if} |         {/if} | ||||||
|       {:else} |       {/key} | ||||||
|         <VideoViewer |     {:else} | ||||||
|           assetId={asset.id} |       {#key asset.id} | ||||||
|           on:close={closeViewer} |         {#if !asset.resized} | ||||||
|           on:onVideoEnded={handleVideoEnded} |           <div class="flex h-full w-full justify-center"> | ||||||
|           on:onVideoStarted={handleVideoStarted} |             <div | ||||||
|         /> |               class="px-auto flex aspect-square h-full items-center justify-center bg-gray-100 dark:bg-immich-dark-gray" | ||||||
|       {/if} |             > | ||||||
|     {/key} |               <Icon path={mdiImageBrokenVariant} size="25%" /> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         {:else if asset.type === AssetTypeEnum.Image} | ||||||
|  |           {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} | ||||||
|  |             <VideoViewer | ||||||
|  |               assetId={asset.livePhotoVideoId} | ||||||
|  |               on:close={closeViewer} | ||||||
|  |               on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} | ||||||
|  |             /> | ||||||
|  |           {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath | ||||||
|  |                 .toLowerCase() | ||||||
|  |                 .endsWith('.insp'))} | ||||||
|  |             <PanoramaViewer {asset} /> | ||||||
|  |           {:else} | ||||||
|  |             <PhotoViewer {asset} on:close={closeViewer} /> | ||||||
|  |           {/if} | ||||||
|  |         {:else} | ||||||
|  |           <VideoViewer | ||||||
|  |             assetId={asset.id} | ||||||
|  |             on:close={closeViewer} | ||||||
|  |             on:onVideoEnded={handleVideoEnded} | ||||||
|  |             on:onVideoStarted={handleVideoStarted} | ||||||
|  |           /> | ||||||
|  |         {/if} | ||||||
|  |       {/key} | ||||||
|  |     {/if} | ||||||
|  |  | ||||||
|  |     {#if $stackAssetsStore.length > 0 && withStacked} | ||||||
|  |       <div | ||||||
|  |         id="stack-slideshow" | ||||||
|  |         class="z-[1005] flex place-item-center place-content-center absolute bottom-0 w-full col-span-4 col-start-1 mb-1 overflow-x-auto horizontal-scrollbar" | ||||||
|  |       > | ||||||
|  |         <div class="relative whitespace-nowrap transition-all"> | ||||||
|  |           {#each $stackAssetsStore as stackedAsset (stackedAsset.id)} | ||||||
|  |             <div | ||||||
|  |               class="{stackedAsset.id == asset.id | ||||||
|  |                 ? '-translate-y-[1px]' | ||||||
|  |                 : '-translate-y-0'} inline-block px-1 transition-transform" | ||||||
|  |             > | ||||||
|  |               <Thumbnail | ||||||
|  |                 class="{stackedAsset.id == asset.id | ||||||
|  |                   ? 'bg-transparent border-2 border-white' | ||||||
|  |                   : 'bg-gray-700/40'} inline-block hover:bg-transparent" | ||||||
|  |                 asset={stackedAsset} | ||||||
|  |                 on:click={() => (asset = stackedAsset)} | ||||||
|  |                 on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)} | ||||||
|  |                 readonly | ||||||
|  |                 thumbnailSize={stackedAsset.id == asset.id ? 65 : 60} | ||||||
|  |                 showStackedIcon={false} | ||||||
|  |               /> | ||||||
|  |  | ||||||
|  |               {#if stackedAsset.id == asset.id} | ||||||
|  |                 <div class="w-full flex place-items-center place-content-center"> | ||||||
|  |                   <div class="w-2 h-2 bg-white rounded-full flex mt-[2px]" /> | ||||||
|  |                 </div> | ||||||
|  |               {/if} | ||||||
|  |             </div> | ||||||
|  |           {/each} | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     {/if} | ||||||
|   </div> |   </div> | ||||||
|  |  | ||||||
|  |   <!-- Stack & Stack Controller --> | ||||||
|  |  | ||||||
|   {#if !isSlideshowMode && showNavigation} |   {#if !isSlideshowMode && showNavigation} | ||||||
|     <div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end"> |     <div class="z-[999] col-span-1 col-start-4 row-span-1 row-start-2 mb-[60px] justify-self-end"> | ||||||
|       <NavigationArea on:click={navigateAssetForward}><Icon path={mdiChevronRight} size="36" /></NavigationArea> |       <NavigationArea on:click={navigateAssetForward}><Icon path={mdiChevronRight} size="36" /></NavigationArea> | ||||||
| @@ -458,7 +571,7 @@ | |||||||
|     <div |     <div | ||||||
|       transition:fly={{ duration: 150 }} |       transition:fly={{ duration: 150 }} | ||||||
|       id="detail-panel" |       id="detail-panel" | ||||||
|       class="z-[1002] row-span-full w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg" |       class="z-[1002] row-start-1 row-span-5 w-[360px] overflow-y-auto bg-immich-bg transition-all dark:border-l dark:border-l-immich-dark-gray dark:bg-immich-dark-bg" | ||||||
|       translate="yes" |       translate="yes" | ||||||
|     > |     > | ||||||
|       <DetailPanel |       <DetailPanel | ||||||
| @@ -512,4 +625,27 @@ | |||||||
|   #immich-asset-viewer { |   #immich-asset-viewer { | ||||||
|     contain: layout; |     contain: layout; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   .horizontal-scrollbar::-webkit-scrollbar { | ||||||
|  |     width: 8px; | ||||||
|  |     height: 10px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* Track */ | ||||||
|  |   .horizontal-scrollbar::-webkit-scrollbar-track { | ||||||
|  |     background: #000000; | ||||||
|  |     border-radius: 16px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* Handle */ | ||||||
|  |   .horizontal-scrollbar::-webkit-scrollbar-thumb { | ||||||
|  |     background: rgba(159, 159, 159, 0.408); | ||||||
|  |     border-radius: 16px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /* Handle on hover */ | ||||||
|  |   .horizontal-scrollbar::-webkit-scrollbar-thumb:hover { | ||||||
|  |     background: #adcbfa; | ||||||
|  |     border-radius: 16px; | ||||||
|  |   } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -40,6 +40,13 @@ | |||||||
|     } |     } | ||||||
|   })(); |   })(); | ||||||
|  |  | ||||||
|  |   $: { | ||||||
|  |     if (!asset.exifInfo) { | ||||||
|  |       api.assetApi.getAssetById({ id: asset.id }).then((res) => { | ||||||
|  |         asset.exifInfo = res.data?.exifInfo; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   $: lat = latlng ? latlng[0] : undefined; |   $: lat = latlng ? latlng[0] : undefined; | ||||||
|   $: lng = latlng ? latlng[1] : undefined; |   $: lng = latlng ? latlng[1] : undefined; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
|  |  | ||||||
|   export let asset: AssetResponseDto; |   export let asset: AssetResponseDto; | ||||||
|   export let element: HTMLDivElement | undefined = undefined; |   export let element: HTMLDivElement | undefined = undefined; | ||||||
|  |   export let haveFadeTransition = true; | ||||||
|  |  | ||||||
|   let imgElement: HTMLDivElement; |   let imgElement: HTMLDivElement; | ||||||
|   let assetData: string; |   let assetData: string; | ||||||
| @@ -116,7 +117,7 @@ | |||||||
|  |  | ||||||
| <div | <div | ||||||
|   bind:this={element} |   bind:this={element} | ||||||
|   transition:fade={{ duration: 150 }} |   transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} | ||||||
|   class="flex h-full select-none place-content-center place-items-center" |   class="flex h-full select-none place-content-center place-items-center" | ||||||
| > | > | ||||||
|   {#await loadAssetData({ loadOriginal: false })} |   {#await loadAssetData({ loadOriginal: false })} | ||||||
| @@ -124,7 +125,7 @@ | |||||||
|   {:then} |   {:then} | ||||||
|     <div bind:this={imgElement} class="h-full w-full"> |     <div bind:this={imgElement} class="h-full w-full"> | ||||||
|       <img |       <img | ||||||
|         transition:fade={{ duration: 150 }} |         transition:fade={{ duration: haveFadeTransition ? 150 : 0 }} | ||||||
|         src={assetData} |         src={assetData} | ||||||
|         alt={asset.id} |         alt={asset.id} | ||||||
|         class="h-full w-full object-contain" |         class="h-full w-full object-contain" | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ | |||||||
|   import VideoThumbnail from './video-thumbnail.svelte'; |   import VideoThumbnail from './video-thumbnail.svelte'; | ||||||
|   import { |   import { | ||||||
|     mdiArchiveArrowDownOutline, |     mdiArchiveArrowDownOutline, | ||||||
|  |     mdiCameraBurst, | ||||||
|     mdiCheckCircle, |     mdiCheckCircle, | ||||||
|     mdiHeart, |     mdiHeart, | ||||||
|     mdiImageBrokenVariant, |     mdiImageBrokenVariant, | ||||||
| @@ -18,7 +19,11 @@ | |||||||
|   } from '@mdi/js'; |   } from '@mdi/js'; | ||||||
|   import Icon from '$lib/components/elements/icon.svelte'; |   import Icon from '$lib/components/elements/icon.svelte'; | ||||||
|  |  | ||||||
|   const dispatch = createEventDispatcher(); |   const dispatch = createEventDispatcher<{ | ||||||
|  |     click: { asset: AssetResponseDto }; | ||||||
|  |     select: { asset: AssetResponseDto }; | ||||||
|  |     'mouse-event': { isMouseOver: boolean; selectedGroupIndex: number }; | ||||||
|  |   }>(); | ||||||
|  |  | ||||||
|   export let asset: AssetResponseDto; |   export let asset: AssetResponseDto; | ||||||
|   export let groupIndex = 0; |   export let groupIndex = 0; | ||||||
| @@ -31,6 +36,10 @@ | |||||||
|   export let disabled = false; |   export let disabled = false; | ||||||
|   export let readonly = false; |   export let readonly = false; | ||||||
|   export let showArchiveIcon = false; |   export let showArchiveIcon = false; | ||||||
|  |   export let showStackedIcon = true; | ||||||
|  |  | ||||||
|  |   let className = ''; | ||||||
|  |   export { className as class }; | ||||||
|  |  | ||||||
|   let mouseOver = false; |   let mouseOver = false; | ||||||
|  |  | ||||||
| @@ -66,6 +75,14 @@ | |||||||
|       dispatch('select', { asset }); |       dispatch('select', { asset }); | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const onMouseEnter = () => { | ||||||
|  |     mouseOver = true; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   const onMouseLeave = () => { | ||||||
|  |     mouseOver = false; | ||||||
|  |   }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <IntersectionObserver once={false} let:intersecting> | <IntersectionObserver once={false} let:intersecting> | ||||||
| @@ -78,13 +95,13 @@ | |||||||
|       : 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}" |       : 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}" | ||||||
|     class:cursor-not-allowed={disabled} |     class:cursor-not-allowed={disabled} | ||||||
|     class:hover:cursor-pointer={!disabled} |     class:hover:cursor-pointer={!disabled} | ||||||
|     on:mouseenter={() => (mouseOver = true)} |     on:mouseenter={() => onMouseEnter()} | ||||||
|     on:mouseleave={() => (mouseOver = false)} |     on:mouseleave={() => onMouseLeave()} | ||||||
|     on:click={thumbnailClickedHandler} |     on:click={thumbnailClickedHandler} | ||||||
|     on:keydown={thumbnailKeyDownHandler} |     on:keydown={thumbnailKeyDownHandler} | ||||||
|   > |   > | ||||||
|     {#if intersecting} |     {#if intersecting} | ||||||
|       <div class="absolute z-20 h-full w-full"> |       <div class="absolute z-20 h-full w-full {className}"> | ||||||
|         <!-- Select asset button  --> |         <!-- Select asset button  --> | ||||||
|         {#if !readonly && (mouseOver || selected || selectionCandidate)} |         {#if !readonly && (mouseOver || selected || selectionCandidate)} | ||||||
|           <button |           <button | ||||||
| @@ -140,6 +157,21 @@ | |||||||
|           </div> |           </div> | ||||||
|         {/if} |         {/if} | ||||||
|  |  | ||||||
|  |         <!-- Stacked asset --> | ||||||
|  |  | ||||||
|  |         {#if asset.stackCount && showStackedIcon} | ||||||
|  |           <div | ||||||
|  |             class="absolute {asset.type == AssetTypeEnum.Image && asset.livePhotoVideoId == null | ||||||
|  |               ? 'top-0 right-0' | ||||||
|  |               : 'top-7 right-1'} z-20 flex place-items-center gap-1 text-xs font-medium text-white" | ||||||
|  |           > | ||||||
|  |             <span class="pr-2 pt-2 flex place-items-center gap-1"> | ||||||
|  |               <p>{asset.stackCount}</p> | ||||||
|  |               <Icon path={mdiCameraBurst} size="24" /> | ||||||
|  |             </span> | ||||||
|  |           </div> | ||||||
|  |         {/if} | ||||||
|  |  | ||||||
|         {#if asset.resized} |         {#if asset.resized} | ||||||
|           <ImageThumbnail |           <ImageThumbnail | ||||||
|             url={api.getAssetThumbnailUrl(asset.id, format)} |             url={api.getAssetThumbnailUrl(asset.id, format)} | ||||||
|   | |||||||
| @@ -0,0 +1,57 @@ | |||||||
|  | <script lang="ts"> | ||||||
|  |   import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; | ||||||
|  |   import { api } from '@api'; | ||||||
|  |   import { OnStack, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||||
|  |   import { | ||||||
|  |     NotificationType, | ||||||
|  |     notificationController, | ||||||
|  |   } from '$lib/components/shared-components/notification/notification'; | ||||||
|  |   import { handleError } from '$lib/utils/handle-error'; | ||||||
|  |  | ||||||
|  |   export let onStack: OnStack | undefined = undefined; | ||||||
|  |  | ||||||
|  |   const { getAssets, clearSelect } = getAssetControlContext(); | ||||||
|  |  | ||||||
|  |   const handleStack = async () => { | ||||||
|  |     try { | ||||||
|  |       const assets = Array.from(getAssets()); | ||||||
|  |       const parent = assets.at(0); | ||||||
|  |  | ||||||
|  |       if (parent == undefined) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const children = assets.slice(1); | ||||||
|  |       const ids = children.map(({ id }) => id); | ||||||
|  |  | ||||||
|  |       if (children.length > 0) { | ||||||
|  |         await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, stackParentId: parent.id } }); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let childrenCount = parent.stackCount ?? 0; | ||||||
|  |       for (const asset of children) { | ||||||
|  |         asset.stackParentId = parent?.id; | ||||||
|  |         // Add grand-children's count to new parent | ||||||
|  |         childrenCount += asset.stackCount == null ? 1 : asset.stackCount + 1; | ||||||
|  |         // Reset children stack info | ||||||
|  |         asset.stackCount = null; | ||||||
|  |         asset.stack = []; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       parent.stackCount = childrenCount; | ||||||
|  |       onStack?.(ids); | ||||||
|  |  | ||||||
|  |       notificationController.show({ | ||||||
|  |         message: `Stacked ${ids.length + 1} assets`, | ||||||
|  |         type: NotificationType.Info, | ||||||
|  |         timeout: 1500, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       clearSelect(); | ||||||
|  |     } catch (error) { | ||||||
|  |       handleError(error, `Unable to stack`); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <MenuOption text="Stack" on:click={handleStack} /> | ||||||
| @@ -20,6 +20,7 @@ | |||||||
|   export let isSelectionMode = false; |   export let isSelectionMode = false; | ||||||
|   export let viewport: Viewport; |   export let viewport: Viewport; | ||||||
|   export let singleSelect = false; |   export let singleSelect = false; | ||||||
|  |   export let withStacked = false; | ||||||
|  |  | ||||||
|   export let assetStore: AssetStore; |   export let assetStore: AssetStore; | ||||||
|   export let assetInteractionStore: AssetInteractionStore; |   export let assetInteractionStore: AssetInteractionStore; | ||||||
| @@ -178,6 +179,7 @@ | |||||||
|             style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px" |             style="width: {box.width}px; height: {box.height}px; top: {box.top}px; left: {box.left}px" | ||||||
|           > |           > | ||||||
|             <Thumbnail |             <Thumbnail | ||||||
|  |               showStackedIcon={withStacked} | ||||||
|               {asset} |               {asset} | ||||||
|               {groupIndex} |               {groupIndex} | ||||||
|               on:click={() => assetClickHandler(asset, groupAssets, groupTitle)} |               on:click={() => assetClickHandler(asset, groupAssets, groupTitle)} | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ | |||||||
|   export let assetStore: AssetStore; |   export let assetStore: AssetStore; | ||||||
|   export let assetInteractionStore: AssetInteractionStore; |   export let assetInteractionStore: AssetInteractionStore; | ||||||
|   export let removeAction: AssetAction | null = null; |   export let removeAction: AssetAction | null = null; | ||||||
|  |   export let withStacked = false; | ||||||
|  |  | ||||||
|   $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; |   $: isTrashEnabled = $featureFlags.loaded && $featureFlags.trash; | ||||||
|   export let forceDelete = false; |   export let forceDelete = false; | ||||||
| @@ -365,6 +366,7 @@ | |||||||
|           <div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}> |           <div id={'bucket_' + bucket.bucketDate} style:height={bucket.bucketHeight + 'px'}> | ||||||
|             {#if intersecting} |             {#if intersecting} | ||||||
|               <AssetDateGroup |               <AssetDateGroup | ||||||
|  |                 {withStacked} | ||||||
|                 {assetStore} |                 {assetStore} | ||||||
|                 {assetInteractionStore} |                 {assetInteractionStore} | ||||||
|                 {isSelectionMode} |                 {isSelectionMode} | ||||||
| @@ -389,6 +391,7 @@ | |||||||
| <Portal target="body"> | <Portal target="body"> | ||||||
|   {#if $showAssetViewer} |   {#if $showAssetViewer} | ||||||
|     <AssetViewer |     <AssetViewer | ||||||
|  |       {withStacked} | ||||||
|       {assetStore} |       {assetStore} | ||||||
|       asset={$viewingAsset} |       asset={$viewingAsset} | ||||||
|       force={forceDelete || !isTrashEnabled} |       force={forceDelete || !isTrashEnabled} | ||||||
| @@ -399,6 +402,7 @@ | |||||||
|       on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)} |       on:unarchived={({ detail: asset }) => handleAction(asset, AssetAction.UNARCHIVE)} | ||||||
|       on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)} |       on:favorite={({ detail: asset }) => handleAction(asset, AssetAction.FAVORITE)} | ||||||
|       on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)} |       on:unfavorite={({ detail: asset }) => handleAction(asset, AssetAction.UNFAVORITE)} | ||||||
|  |       on:unstack={() => handleClose()} | ||||||
|     /> |     /> | ||||||
|   {/if} |   {/if} | ||||||
| </Portal> | </Portal> | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
|   export type OnRestore = (ids: string[]) => void; |   export type OnRestore = (ids: string[]) => void; | ||||||
|   export type OnArchive = (ids: string[], isArchived: boolean) => void; |   export type OnArchive = (ids: string[], isArchived: boolean) => void; | ||||||
|   export type OnFavorite = (ids: string[], favorite: boolean) => void; |   export type OnFavorite = (ids: string[], favorite: boolean) => void; | ||||||
|  |   export type OnStack = (ids: string[]) => void; | ||||||
|  |  | ||||||
|   export interface AssetControlContext { |   export interface AssetControlContext { | ||||||
|     // Wrap assets in a function, because context isn't reactive. |     // Wrap assets in a function, because context isn't reactive. | ||||||
|   | |||||||
| @@ -222,6 +222,7 @@ export class AssetStore { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       bucket.assets = assets; |       bucket.assets = assets; | ||||||
|  |  | ||||||
|       this.emit(true); |       this.emit(true); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|       handleError(error, 'Failed to load assets'); |       handleError(error, 'Failed to load assets'); | ||||||
| @@ -251,7 +252,7 @@ export class AssetStore { | |||||||
|     return scrollTimeline ? delta : 0; |     return scrollTimeline ? delta : 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private addAsset(asset: AssetResponseDto): void { |   addAsset(asset: AssetResponseDto): void { | ||||||
|     if ( |     if ( | ||||||
|       this.assetToBucket[asset.id] || |       this.assetToBucket[asset.id] || | ||||||
|       this.options.userId || |       this.options.userId || | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								web/src/lib/stores/stacked-asset.store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								web/src/lib/stores/stacked-asset.store.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | |||||||
|  | import { writable } from 'svelte/store'; | ||||||
|  | import type { AssetResponseDto } from '../../api/open-api'; | ||||||
|  |  | ||||||
|  | export const stackAssetsStore = writable<AssetResponseDto[]>([]); | ||||||
| @@ -7,6 +7,7 @@ | |||||||
|   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; |   import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; | ||||||
|   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; |   import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte'; | ||||||
|   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; |   import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte'; | ||||||
|  |   import StackAction from '$lib/components/photos-page/actions/stack-action.svelte'; | ||||||
|   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; |   import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; | ||||||
|   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; |   import AssetGrid from '$lib/components/photos-page/asset-grid.svelte'; | ||||||
|   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; |   import AssetSelectContextMenu from '$lib/components/photos-page/asset-select-context-menu.svelte'; | ||||||
| @@ -25,7 +26,7 @@ | |||||||
|  |  | ||||||
|   let { isViewing: showAssetViewer } = assetViewingStore; |   let { isViewing: showAssetViewer } = assetViewingStore; | ||||||
|   let handleEscapeKey = false; |   let handleEscapeKey = false; | ||||||
|   const assetStore = new AssetStore({ isArchived: false }); |   const assetStore = new AssetStore({ isArchived: false, withStacked: true }); | ||||||
|   const assetInteractionStore = createAssetInteractionStore(); |   const assetInteractionStore = createAssetInteractionStore(); | ||||||
|   const { isMultiSelectState, selectedAssets } = assetInteractionStore; |   const { isMultiSelectState, selectedAssets } = assetInteractionStore; | ||||||
|  |  | ||||||
| @@ -62,13 +63,22 @@ | |||||||
|       <FavoriteAction menuItem removeFavorite={isAllFavorite} /> |       <FavoriteAction menuItem removeFavorite={isAllFavorite} /> | ||||||
|       <DownloadAction menuItem /> |       <DownloadAction menuItem /> | ||||||
|       <ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} /> |       <ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} /> | ||||||
|  |       {#if $selectedAssets.size > 1} | ||||||
|  |         <StackAction onStack={(ids) => assetStore.removeAssets(ids)} /> | ||||||
|  |       {/if} | ||||||
|       <AssetJobActions /> |       <AssetJobActions /> | ||||||
|     </AssetSelectContextMenu> |     </AssetSelectContextMenu> | ||||||
|   </AssetSelectControlBar> |   </AssetSelectControlBar> | ||||||
| {/if} | {/if} | ||||||
|  |  | ||||||
| <UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}> | <UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton scrollbar={false}> | ||||||
|   <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} on:escape={handleEscape}> |   <AssetGrid | ||||||
|  |     {assetStore} | ||||||
|  |     {assetInteractionStore} | ||||||
|  |     removeAction={AssetAction.ARCHIVE} | ||||||
|  |     on:escape={handleEscape} | ||||||
|  |     withStacked | ||||||
|  |   > | ||||||
|     {#if data.user.memoriesEnabled} |     {#if data.user.memoriesEnabled} | ||||||
|       <MemoryLane /> |       <MemoryLane /> | ||||||
|     {/if} |     {/if} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user