mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:29:32 -05:00 
			
		
		
		
	feat(web): manual stacking asset (#4650)
Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									72dcde9e0f
								
							
						
					
					
						commit
						8b5b6d0821
					
				
							
								
								
									
										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,7 +472,23 @@
 | 
				
			|||||||
    </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">
 | 
				
			||||||
 | 
					    <!-- Condition to show preview of stacked asset on hovered -->
 | 
				
			||||||
 | 
					    {#if displayedAsset}
 | 
				
			||||||
 | 
					      {#key displayedAsset.id}
 | 
				
			||||||
 | 
					        {#if displayedAsset.type === AssetTypeEnum.Image}
 | 
				
			||||||
 | 
					          <PhotoViewer asset={displayedAsset} on:close={closeViewer} haveFadeTransition={false} />
 | 
				
			||||||
 | 
					        {:else}
 | 
				
			||||||
 | 
					          <VideoViewer
 | 
				
			||||||
 | 
					            assetId={displayedAsset.id}
 | 
				
			||||||
 | 
					            on:close={closeViewer}
 | 
				
			||||||
 | 
					            on:onVideoEnded={handleVideoEnded}
 | 
				
			||||||
 | 
					            on:onVideoStarted={handleVideoStarted}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        {/if}
 | 
				
			||||||
 | 
					      {/key}
 | 
				
			||||||
 | 
					    {:else}
 | 
				
			||||||
      {#key asset.id}
 | 
					      {#key asset.id}
 | 
				
			||||||
        {#if !asset.resized}
 | 
					        {#if !asset.resized}
 | 
				
			||||||
          <div class="flex h-full w-full justify-center">
 | 
					          <div class="flex h-full w-full justify-center">
 | 
				
			||||||
@ -446,7 +521,45 @@
 | 
				
			|||||||
          />
 | 
					          />
 | 
				
			||||||
        {/if}
 | 
					        {/if}
 | 
				
			||||||
      {/key}
 | 
					      {/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>
 | 
					                </div>
 | 
				
			||||||
 | 
					              {/if}
 | 
				
			||||||
 | 
					            </div>
 | 
				
			||||||
 | 
					          {/each}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					  </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">
 | 
				
			||||||
@ -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}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user