You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	restore: bulk actions (#3730)
* feat: improve bulk isArchive and isFavorite updates * chore: open api
This commit is contained in:
		
							
								
								
									
										113
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										113
									
								
								cli/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -344,6 +344,31 @@ export interface AllJobStatusResponseDto { | ||||
|      */ | ||||
|     'videoConversion': JobStatusDto; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface AssetBulkUpdateDto | ||||
|  */ | ||||
| export interface AssetBulkUpdateDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof AssetBulkUpdateDto | ||||
|      */ | ||||
|     'ids': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof AssetBulkUpdateDto | ||||
|      */ | ||||
|     'isArchived'?: boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof AssetBulkUpdateDto | ||||
|      */ | ||||
|     'isFavorite'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -5871,6 +5896,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetBulkUpdateDto} assetBulkUpdateDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         updateAssets: async (assetBulkUpdateDto: AssetBulkUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'assetBulkUpdateDto' is not null or undefined
 | ||||
|             assertParamExists('updateAssets', 'assetBulkUpdateDto', assetBulkUpdateDto) | ||||
|             const localVarPath = `/asset`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication cookie required
 | ||||
| 
 | ||||
|             // authentication api_key required
 | ||||
|             await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
| 
 | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(assetBulkUpdateDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {File} assetData  | ||||
| @@ -6259,6 +6328,16 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(id, updateAssetDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetBulkUpdateDto} assetBulkUpdateDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async updateAssets(assetBulkUpdateDto: AssetBulkUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssets(assetBulkUpdateDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {File} assetData  | ||||
| @@ -6495,6 +6574,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         updateAsset(requestParameters: AssetApiUpdateAssetRequest, options?: AxiosRequestConfig): AxiosPromise<AssetResponseDto> { | ||||
|             return localVarFp.updateAsset(requestParameters.id, requestParameters.updateAssetDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetApiUpdateAssetsRequest} requestParameters Request parameters. | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         updateAssets(requestParameters: AssetApiUpdateAssetsRequest, options?: AxiosRequestConfig): AxiosPromise<void> { | ||||
|             return localVarFp.updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetApiUploadFileRequest} requestParameters Request parameters. | ||||
| @@ -7011,6 +7099,20 @@ export interface AssetApiUpdateAssetRequest { | ||||
|     readonly updateAssetDto: UpdateAssetDto | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for updateAssets operation in AssetApi. | ||||
|  * @export | ||||
|  * @interface AssetApiUpdateAssetsRequest | ||||
|  */ | ||||
| export interface AssetApiUpdateAssetsRequest { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {AssetBulkUpdateDto} | ||||
|      * @memberof AssetApiUpdateAssets | ||||
|      */ | ||||
|     readonly assetBulkUpdateDto: AssetBulkUpdateDto | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for uploadFile operation in AssetApi. | ||||
|  * @export | ||||
| @@ -7366,6 +7468,17 @@ export class AssetApi extends BaseAPI { | ||||
|         return AssetApiFp(this.configuration).updateAsset(requestParameters.id, requestParameters.updateAssetDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {AssetApiUpdateAssetsRequest} requestParameters Request parameters. | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public updateAssets(requestParameters: AssetApiUpdateAssetsRequest, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {AssetApiUploadFileRequest} requestParameters Request parameters. | ||||
|   | ||||
							
								
								
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3
									
								
								mobile/openapi/.openapi-generator/FILES
									
									
									
										generated
									
									
									
								
							| @@ -15,6 +15,7 @@ doc/AlbumCountResponseDto.md | ||||
| doc/AlbumResponseDto.md | ||||
| doc/AllJobStatusResponseDto.md | ||||
| doc/AssetApi.md | ||||
| doc/AssetBulkUpdateDto.md | ||||
| doc/AssetBulkUploadCheckDto.md | ||||
| doc/AssetBulkUploadCheckItem.md | ||||
| doc/AssetBulkUploadCheckResponseDto.md | ||||
| @@ -158,6 +159,7 @@ lib/model/api_key_create_dto.dart | ||||
| lib/model/api_key_create_response_dto.dart | ||||
| lib/model/api_key_response_dto.dart | ||||
| lib/model/api_key_update_dto.dart | ||||
| lib/model/asset_bulk_update_dto.dart | ||||
| lib/model/asset_bulk_upload_check_dto.dart | ||||
| lib/model/asset_bulk_upload_check_item.dart | ||||
| lib/model/asset_bulk_upload_check_response_dto.dart | ||||
| @@ -270,6 +272,7 @@ test/api_key_create_response_dto_test.dart | ||||
| test/api_key_response_dto_test.dart | ||||
| test/api_key_update_dto_test.dart | ||||
| test/asset_api_test.dart | ||||
| test/asset_bulk_update_dto_test.dart | ||||
| test/asset_bulk_upload_check_dto_test.dart | ||||
| test/asset_bulk_upload_check_item_test.dart | ||||
| test/asset_bulk_upload_check_response_dto_test.dart | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/README.md
									
									
									
										generated
									
									
									
								
							| @@ -110,6 +110,7 @@ Class | Method | HTTP request | Description | ||||
| *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |  | ||||
| *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{id} |  | ||||
| *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{id} |  | ||||
| *AssetApi* | [**updateAssets**](doc//AssetApi.md#updateassets) | **PUT** /asset |  | ||||
| *AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |  | ||||
| *AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |  | ||||
| *AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |  | ||||
| @@ -187,6 +188,7 @@ Class | Method | HTTP request | Description | ||||
|  - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) | ||||
|  - [AlbumResponseDto](doc//AlbumResponseDto.md) | ||||
|  - [AllJobStatusResponseDto](doc//AllJobStatusResponseDto.md) | ||||
|  - [AssetBulkUpdateDto](doc//AssetBulkUpdateDto.md) | ||||
|  - [AssetBulkUploadCheckDto](doc//AssetBulkUploadCheckDto.md) | ||||
|  - [AssetBulkUploadCheckItem](doc//AssetBulkUploadCheckItem.md) | ||||
|  - [AssetBulkUploadCheckResponseDto](doc//AssetBulkUploadCheckResponseDto.md) | ||||
|   | ||||
							
								
								
									
										55
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										55
									
								
								mobile/openapi/doc/AssetApi.md
									
									
									
										generated
									
									
									
								
							| @@ -32,6 +32,7 @@ Method | HTTP request | Description | ||||
| [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |  | ||||
| [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{id} |  | ||||
| [**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{id} |  | ||||
| [**updateAssets**](AssetApi.md#updateassets) | **PUT** /asset |  | ||||
| [**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |  | ||||
| 
 | ||||
| 
 | ||||
| @@ -1366,6 +1367,60 @@ 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) | ||||
| 
 | ||||
| # **updateAssets** | ||||
| > updateAssets(assetBulkUpdateDto) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| ### Example | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| // TODO Configure API key authorization: cookie | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY'; | ||||
| // uncomment below to setup prefix (e.g. Bearer) for API key, if needed | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer'; | ||||
| // TODO Configure API key authorization: api_key | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY'; | ||||
| // uncomment below to setup prefix (e.g. Bearer) for API key, if needed | ||||
| //defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer'; | ||||
| // TODO Configure HTTP Bearer authorization: bearer | ||||
| // Case 1. Use String Token | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); | ||||
| // Case 2. Use Function which generate token. | ||||
| // String yourTokenGeneratorFunction() { ... } | ||||
| //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction); | ||||
| 
 | ||||
| final api_instance = AssetApi(); | ||||
| final assetBulkUpdateDto = AssetBulkUpdateDto(); // AssetBulkUpdateDto |  | ||||
| 
 | ||||
| try { | ||||
|     api_instance.updateAssets(assetBulkUpdateDto); | ||||
| } catch (e) { | ||||
|     print('Exception when calling AssetApi->updateAssets: $e\n'); | ||||
| } | ||||
| ``` | ||||
| 
 | ||||
| ### Parameters | ||||
| 
 | ||||
| Name | Type | Description  | Notes | ||||
| ------------- | ------------- | ------------- | ------------- | ||||
|  **assetBulkUpdateDto** | [**AssetBulkUpdateDto**](AssetBulkUpdateDto.md)|  |  | ||||
| 
 | ||||
| ### Return type | ||||
| 
 | ||||
| void (empty response body) | ||||
| 
 | ||||
| ### Authorization | ||||
| 
 | ||||
| [cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) | ||||
| 
 | ||||
| ### HTTP request headers | ||||
| 
 | ||||
|  - **Content-Type**: application/json | ||||
|  - **Accept**: Not defined | ||||
| 
 | ||||
| [[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) | ||||
| 
 | ||||
| # **uploadFile** | ||||
| > AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, duration, isArchived, isReadOnly, isVisible, livePhotoData, sidecarData) | ||||
| 
 | ||||
|   | ||||
							
								
								
									
										17
									
								
								mobile/openapi/doc/AssetBulkUpdateDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								mobile/openapi/doc/AssetBulkUpdateDto.md
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| # openapi.model.AssetBulkUpdateDto | ||||
| 
 | ||||
| ## Load the model package | ||||
| ```dart | ||||
| import 'package:openapi/api.dart'; | ||||
| ``` | ||||
| 
 | ||||
| ## Properties | ||||
| Name | Type | Description | Notes | ||||
| ------------ | ------------- | ------------- | ------------- | ||||
| **ids** | **List<String>** |  | [default to const []] | ||||
| **isArchived** | **bool** |  | [optional]  | ||||
| **isFavorite** | **bool** |  | [optional]  | ||||
| 
 | ||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/lib/api.dart
									
									
									
										generated
									
									
									
								
							| @@ -52,6 +52,7 @@ part 'model/admin_signup_response_dto.dart'; | ||||
| part 'model/album_count_response_dto.dart'; | ||||
| part 'model/album_response_dto.dart'; | ||||
| part 'model/all_job_status_response_dto.dart'; | ||||
| part 'model/asset_bulk_update_dto.dart'; | ||||
| part 'model/asset_bulk_upload_check_dto.dart'; | ||||
| part 'model/asset_bulk_upload_check_item.dart'; | ||||
| part 'model/asset_bulk_upload_check_response_dto.dart'; | ||||
|   | ||||
							
								
								
									
										39
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										39
									
								
								mobile/openapi/lib/api/asset_api.dart
									
									
									
										generated
									
									
									
								
							| @@ -1404,6 +1404,45 @@ class AssetApi { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'PUT /asset' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [AssetBulkUpdateDto] assetBulkUpdateDto (required): | ||||
|   Future<Response> updateAssetsWithHttpInfo(AssetBulkUpdateDto assetBulkUpdateDto,) async { | ||||
|     // ignore: prefer_const_declarations | ||||
|     final path = r'/asset'; | ||||
| 
 | ||||
|     // ignore: prefer_final_locals | ||||
|     Object? postBody = assetBulkUpdateDto; | ||||
| 
 | ||||
|     final queryParams = <QueryParam>[]; | ||||
|     final headerParams = <String, String>{}; | ||||
|     final formParams = <String, String>{}; | ||||
| 
 | ||||
|     const contentTypes = <String>['application/json']; | ||||
| 
 | ||||
| 
 | ||||
|     return apiClient.invokeAPI( | ||||
|       path, | ||||
|       'PUT', | ||||
|       queryParams, | ||||
|       postBody, | ||||
|       headerParams, | ||||
|       formParams, | ||||
|       contentTypes.isEmpty ? null : contentTypes.first, | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   /// * [AssetBulkUpdateDto] assetBulkUpdateDto (required): | ||||
|   Future<void> updateAssets(AssetBulkUpdateDto assetBulkUpdateDto,) async { | ||||
|     final response = await updateAssetsWithHttpInfo(assetBulkUpdateDto,); | ||||
|     if (response.statusCode >= HttpStatus.badRequest) { | ||||
|       throw ApiException(response.statusCode, await _decodeBodyBytes(response)); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /// Performs an HTTP 'POST /asset/upload' operation and returns the [Response]. | ||||
|   /// Parameters: | ||||
|   /// | ||||
|   | ||||
							
								
								
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										2
									
								
								mobile/openapi/lib/api_client.dart
									
									
									
										generated
									
									
									
								
							| @@ -199,6 +199,8 @@ class ApiClient { | ||||
|           return AlbumResponseDto.fromJson(value); | ||||
|         case 'AllJobStatusResponseDto': | ||||
|           return AllJobStatusResponseDto.fromJson(value); | ||||
|         case 'AssetBulkUpdateDto': | ||||
|           return AssetBulkUpdateDto.fromJson(value); | ||||
|         case 'AssetBulkUploadCheckDto': | ||||
|           return AssetBulkUploadCheckDto.fromJson(value); | ||||
|         case 'AssetBulkUploadCheckItem': | ||||
|   | ||||
							
								
								
									
										134
									
								
								mobile/openapi/lib/model/asset_bulk_update_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								mobile/openapi/lib/model/asset_bulk_update_dto.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,134 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| part of openapi.api; | ||||
| 
 | ||||
| class AssetBulkUpdateDto { | ||||
|   /// Returns a new [AssetBulkUpdateDto] instance. | ||||
|   AssetBulkUpdateDto({ | ||||
|     this.ids = const [], | ||||
|     this.isArchived, | ||||
|     this.isFavorite, | ||||
|   }); | ||||
| 
 | ||||
|   List<String> ids; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isArchived; | ||||
| 
 | ||||
|   /// | ||||
|   /// Please note: This property should have been non-nullable! Since the specification file | ||||
|   /// does not include a default value (using the "default:" property), however, the generated | ||||
|   /// source code must fall back to having a nullable type. | ||||
|   /// Consider adding a "default:" property in the specification file to hide this note. | ||||
|   /// | ||||
|   bool? isFavorite; | ||||
| 
 | ||||
|   @override | ||||
|   bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && | ||||
|      other.ids == ids && | ||||
|      other.isArchived == isArchived && | ||||
|      other.isFavorite == isFavorite; | ||||
| 
 | ||||
|   @override | ||||
|   int get hashCode => | ||||
|     // ignore: unnecessary_parenthesis | ||||
|     (ids.hashCode) + | ||||
|     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode); | ||||
| 
 | ||||
|   @override | ||||
|   String toString() => 'AssetBulkUpdateDto[ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite]'; | ||||
| 
 | ||||
|   Map<String, dynamic> toJson() { | ||||
|     final json = <String, dynamic>{}; | ||||
|       json[r'ids'] = this.ids; | ||||
|     if (this.isArchived != null) { | ||||
|       json[r'isArchived'] = this.isArchived; | ||||
|     } else { | ||||
|     //  json[r'isArchived'] = null; | ||||
|     } | ||||
|     if (this.isFavorite != null) { | ||||
|       json[r'isFavorite'] = this.isFavorite; | ||||
|     } else { | ||||
|     //  json[r'isFavorite'] = null; | ||||
|     } | ||||
|     return json; | ||||
|   } | ||||
| 
 | ||||
|   /// Returns a new [AssetBulkUpdateDto] instance and imports its values from | ||||
|   /// [value] if it's a [Map], null otherwise. | ||||
|   // ignore: prefer_constructors_over_static_methods | ||||
|   static AssetBulkUpdateDto? fromJson(dynamic value) { | ||||
|     if (value is Map) { | ||||
|       final json = value.cast<String, dynamic>(); | ||||
| 
 | ||||
|       return AssetBulkUpdateDto( | ||||
|         ids: json[r'ids'] is List | ||||
|             ? (json[r'ids'] as List).cast<String>() | ||||
|             : const [], | ||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||
|       ); | ||||
|     } | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   static List<AssetBulkUpdateDto> listFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final result = <AssetBulkUpdateDto>[]; | ||||
|     if (json is List && json.isNotEmpty) { | ||||
|       for (final row in json) { | ||||
|         final value = AssetBulkUpdateDto.fromJson(row); | ||||
|         if (value != null) { | ||||
|           result.add(value); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return result.toList(growable: growable); | ||||
|   } | ||||
| 
 | ||||
|   static Map<String, AssetBulkUpdateDto> mapFromJson(dynamic json) { | ||||
|     final map = <String, AssetBulkUpdateDto>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       json = json.cast<String, dynamic>(); // ignore: parameter_assignments | ||||
|       for (final entry in json.entries) { | ||||
|         final value = AssetBulkUpdateDto.fromJson(entry.value); | ||||
|         if (value != null) { | ||||
|           map[entry.key] = value; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   // maps a json object with a list of AssetBulkUpdateDto-objects as value to a dart map | ||||
|   static Map<String, List<AssetBulkUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) { | ||||
|     final map = <String, List<AssetBulkUpdateDto>>{}; | ||||
|     if (json is Map && json.isNotEmpty) { | ||||
|       // ignore: parameter_assignments | ||||
|       json = json.cast<String, dynamic>(); | ||||
|       for (final entry in json.entries) { | ||||
|         map[entry.key] = AssetBulkUpdateDto.listFromJson(entry.value, growable: growable,); | ||||
|       } | ||||
|     } | ||||
|     return map; | ||||
|   } | ||||
| 
 | ||||
|   /// The list of required keys that must be present in a JSON. | ||||
|   static const requiredKeys = <String>{ | ||||
|     'ids', | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										5
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/asset_api_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -146,6 +146,11 @@ void main() { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future updateAssets(AssetBulkUpdateDto assetBulkUpdateDto) async | ||||
|     test('test updateAssets', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     //Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, String duration, bool isArchived, bool isReadOnly, bool isVisible, MultipartFile livePhotoData, MultipartFile sidecarData }) async | ||||
|     test('test uploadFile', () async { | ||||
|       // TODO | ||||
|   | ||||
							
								
								
									
										37
									
								
								mobile/openapi/test/asset_bulk_update_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								mobile/openapi/test/asset_bulk_update_dto_test.dart
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| // AUTO-GENERATED FILE, DO NOT MODIFY! | ||||
| // | ||||
| // @dart=2.12 | ||||
| 
 | ||||
| // ignore_for_file: unused_element, unused_import | ||||
| // ignore_for_file: always_put_required_named_parameters_first | ||||
| // ignore_for_file: constant_identifier_names | ||||
| // ignore_for_file: lines_longer_than_80_chars | ||||
| 
 | ||||
| import 'package:openapi/api.dart'; | ||||
| import 'package:test/test.dart'; | ||||
| 
 | ||||
| // tests for AssetBulkUpdateDto | ||||
| void main() { | ||||
|   // final instance = AssetBulkUpdateDto(); | ||||
| 
 | ||||
|   group('test AssetBulkUpdateDto', () { | ||||
|     // List<String> ids (default value: const []) | ||||
|     test('to test the property `ids`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // bool isArchived | ||||
|     test('to test the property `isArchived`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
|     // bool isFavorite | ||||
|     test('to test the property `isFavorite`', () async { | ||||
|       // TODO | ||||
|     }); | ||||
| 
 | ||||
| 
 | ||||
|   }); | ||||
| 
 | ||||
| } | ||||
| @@ -808,6 +808,39 @@ | ||||
|         "tags": [ | ||||
|           "Asset" | ||||
|         ] | ||||
|       }, | ||||
|       "put": { | ||||
|         "operationId": "updateAssets", | ||||
|         "parameters": [], | ||||
|         "requestBody": { | ||||
|           "content": { | ||||
|             "application/json": { | ||||
|               "schema": { | ||||
|                 "$ref": "#/components/schemas/AssetBulkUpdateDto" | ||||
|               } | ||||
|             } | ||||
|           }, | ||||
|           "required": true | ||||
|         }, | ||||
|         "responses": { | ||||
|           "204": { | ||||
|             "description": "" | ||||
|           } | ||||
|         }, | ||||
|         "security": [ | ||||
|           { | ||||
|             "bearer": [] | ||||
|           }, | ||||
|           { | ||||
|             "cookie": [] | ||||
|           }, | ||||
|           { | ||||
|             "api_key": [] | ||||
|           } | ||||
|         ], | ||||
|         "tags": [ | ||||
|           "Asset" | ||||
|         ] | ||||
|       } | ||||
|     }, | ||||
|     "/asset/assetById/{id}": { | ||||
| @@ -4841,6 +4874,27 @@ | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AssetBulkUpdateDto": { | ||||
|         "properties": { | ||||
|           "ids": { | ||||
|             "items": { | ||||
|               "format": "uuid", | ||||
|               "type": "string" | ||||
|             }, | ||||
|             "type": "array" | ||||
|           }, | ||||
|           "isArchived": { | ||||
|             "type": "boolean" | ||||
|           }, | ||||
|           "isFavorite": { | ||||
|             "type": "boolean" | ||||
|           } | ||||
|         }, | ||||
|         "required": [ | ||||
|           "ids" | ||||
|         ], | ||||
|         "type": "object" | ||||
|       }, | ||||
|       "AssetBulkUploadCheckDto": { | ||||
|         "properties": { | ||||
|           "assets": { | ||||
|   | ||||
| @@ -79,6 +79,7 @@ export interface IAssetRepository { | ||||
|   getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>; | ||||
|   deleteAll(ownerId: string): Promise<void>; | ||||
|   getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>; | ||||
|   updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void>; | ||||
|   save(asset: Partial<AssetEntity>): Promise<AssetEntity>; | ||||
|   findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>; | ||||
|   getMapMarkers(ownerId: string, options?: MapMarkerSearchOptions): Promise<MapMarker[]>; | ||||
|   | ||||
| @@ -514,4 +514,22 @@ describe(AssetService.name, () => { | ||||
|       expect(assetMock.getStatistics).toHaveBeenCalledWith(authStub.admin.id, {}); | ||||
|     }); | ||||
|   }); | ||||
|  | ||||
|   describe('updateAll', () => { | ||||
|     it('should require asset write access for all ids', async () => { | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(false); | ||||
|       await expect( | ||||
|         sut.updateAll(authStub.admin, { | ||||
|           ids: ['asset-1'], | ||||
|           isArchived: false, | ||||
|         }), | ||||
|       ).rejects.toBeInstanceOf(BadRequestException); | ||||
|     }); | ||||
|  | ||||
|     it('should update all assets', async () => { | ||||
|       accessMock.asset.hasOwnerAccess.mockResolvedValue(true); | ||||
|       await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true }); | ||||
|       expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true }); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import { HumanReadableSize, usePagination } from '../domain.util'; | ||||
| import { ImmichReadStream, IStorageRepository, StorageCore, StorageFolder } from '../storage'; | ||||
| import { IAssetRepository } from './asset.repository'; | ||||
| import { | ||||
|   AssetBulkUpdateDto, | ||||
|   AssetIdsDto, | ||||
|   DownloadArchiveInfo, | ||||
|   DownloadInfoDto, | ||||
| @@ -268,4 +269,10 @@ export class AssetService { | ||||
|     const stats = await this.assetRepository.getStatistics(authUser.id, dto); | ||||
|     return mapStats(stats); | ||||
|   } | ||||
|  | ||||
|   async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto) { | ||||
|     const { ids, ...options } = dto; | ||||
|     await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids); | ||||
|     await this.assetRepository.updateAll(ids, options); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										12
									
								
								server/src/domain/asset/dto/asset.dto.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								server/src/domain/asset/dto/asset.dto.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| import { IsBoolean, IsOptional } from 'class-validator'; | ||||
| import { BulkIdsDto } from '../response-dto'; | ||||
|  | ||||
| export class AssetBulkUpdateDto extends BulkIdsDto { | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   isFavorite?: boolean; | ||||
|  | ||||
|   @IsOptional() | ||||
|   @IsBoolean() | ||||
|   isArchived?: boolean; | ||||
| } | ||||
| @@ -1,5 +1,6 @@ | ||||
| export * from './asset-ids.dto'; | ||||
| export * from './asset-statistics.dto'; | ||||
| export * from './asset.dto'; | ||||
| export * from './download.dto'; | ||||
| export * from './map-marker.dto'; | ||||
| export * from './memory-lane.dto'; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import { | ||||
|   AssetBulkUpdateDto, | ||||
|   AssetIdsDto, | ||||
|   AssetResponseDto, | ||||
|   AssetService, | ||||
| @@ -15,7 +16,7 @@ import { | ||||
| } from '@app/domain'; | ||||
| import { MapMarkerDto } from '@app/domain/asset/dto/map-marker.dto'; | ||||
| import { MemoryLaneResponseDto } from '@app/domain/asset/response-dto/memory-lane-response.dto'; | ||||
| import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Query, StreamableFile } from '@nestjs/common'; | ||||
| import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query, StreamableFile } from '@nestjs/common'; | ||||
| import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; | ||||
| import { Authenticated, AuthUser, SharedLinkRoute } from '../app.guard'; | ||||
| import { asStreamableFile, UseValidation } from '../app.utils'; | ||||
| @@ -76,4 +77,10 @@ export class AssetController { | ||||
|   getByTimeBucket(@AuthUser() authUser: AuthUserDto, @Query() dto: TimeBucketAssetDto): Promise<AssetResponseDto[]> { | ||||
|     return this.service.getByTimeBucket(authUser, dto); | ||||
|   } | ||||
|  | ||||
|   @Put() | ||||
|   @HttpCode(HttpStatus.NO_CONTENT) | ||||
|   updateAssets(@AuthUser() authUser: AuthUserDto, @Body() dto: AssetBulkUpdateDto): Promise<void> { | ||||
|     return this.service.updateAll(authUser, dto); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -129,6 +129,10 @@ export class AssetRepository implements IAssetRepository { | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   async updateAll(ids: string[], options: Partial<AssetEntity>): Promise<void> { | ||||
|     await this.repository.update({ id: In(ids) }, options); | ||||
|   } | ||||
|  | ||||
|   async save(asset: Partial<AssetEntity>): Promise<AssetEntity> { | ||||
|     const { id } = await this.repository.save(asset); | ||||
|     return this.repository.findOneOrFail({ | ||||
|   | ||||
| @@ -11,6 +11,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => { | ||||
|     getFirstAssetForAlbumId: jest.fn(), | ||||
|     getLastUpdatedAssetForAlbumId: jest.fn(), | ||||
|     getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }), | ||||
|     updateAll: jest.fn(), | ||||
|     deleteAll: jest.fn(), | ||||
|     save: jest.fn(), | ||||
|     findLivePhotoMatch: jest.fn(), | ||||
|   | ||||
							
								
								
									
										113
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										113
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -344,6 +344,31 @@ export interface AllJobStatusResponseDto { | ||||
|      */ | ||||
|     'videoConversion': JobStatusDto; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
|  * @interface AssetBulkUpdateDto | ||||
|  */ | ||||
| export interface AssetBulkUpdateDto { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {Array<string>} | ||||
|      * @memberof AssetBulkUpdateDto | ||||
|      */ | ||||
|     'ids': Array<string>; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof AssetBulkUpdateDto | ||||
|      */ | ||||
|     'isArchived'?: boolean; | ||||
|     /** | ||||
|      *  | ||||
|      * @type {boolean} | ||||
|      * @memberof AssetBulkUpdateDto | ||||
|      */ | ||||
|     'isFavorite'?: boolean; | ||||
| } | ||||
| /** | ||||
|  *  | ||||
|  * @export | ||||
| @@ -5871,6 +5896,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetBulkUpdateDto} assetBulkUpdateDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         updateAssets: async (assetBulkUpdateDto: AssetBulkUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => { | ||||
|             // verify required parameter 'assetBulkUpdateDto' is not null or undefined
 | ||||
|             assertParamExists('updateAssets', 'assetBulkUpdateDto', assetBulkUpdateDto) | ||||
|             const localVarPath = `/asset`; | ||||
|             // use dummy base URL string because the URL constructor only accepts absolute URLs.
 | ||||
|             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); | ||||
|             let baseOptions; | ||||
|             if (configuration) { | ||||
|                 baseOptions = configuration.baseOptions; | ||||
|             } | ||||
| 
 | ||||
|             const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; | ||||
|             const localVarHeaderParameter = {} as any; | ||||
|             const localVarQueryParameter = {} as any; | ||||
| 
 | ||||
|             // authentication cookie required
 | ||||
| 
 | ||||
|             // authentication api_key required
 | ||||
|             await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) | ||||
| 
 | ||||
|             // authentication bearer required
 | ||||
|             // http bearer authentication required
 | ||||
|             await setBearerAuthToObject(localVarHeaderParameter, configuration) | ||||
| 
 | ||||
| 
 | ||||
|      | ||||
|             localVarHeaderParameter['Content-Type'] = 'application/json'; | ||||
| 
 | ||||
|             setSearchParams(localVarUrlObj, localVarQueryParameter); | ||||
|             let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; | ||||
|             localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; | ||||
|             localVarRequestOptions.data = serializeDataIfNeeded(assetBulkUpdateDto, localVarRequestOptions, configuration) | ||||
| 
 | ||||
|             return { | ||||
|                 url: toPathString(localVarUrlObj), | ||||
|                 options: localVarRequestOptions, | ||||
|             }; | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {File} assetData  | ||||
| @@ -6259,6 +6328,16 @@ export const AssetApiFp = function(configuration?: Configuration) { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(id, updateAssetDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetBulkUpdateDto} assetBulkUpdateDto  | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         async updateAssets(assetBulkUpdateDto: AssetBulkUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<void>> { | ||||
|             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssets(assetBulkUpdateDto, options); | ||||
|             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {File} assetData  | ||||
| @@ -6495,6 +6574,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath | ||||
|         updateAsset(requestParameters: AssetApiUpdateAssetRequest, options?: AxiosRequestConfig): AxiosPromise<AssetResponseDto> { | ||||
|             return localVarFp.updateAsset(requestParameters.id, requestParameters.updateAssetDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetApiUpdateAssetsRequest} requestParameters Request parameters. | ||||
|          * @param {*} [options] Override http request option. | ||||
|          * @throws {RequiredError} | ||||
|          */ | ||||
|         updateAssets(requestParameters: AssetApiUpdateAssetsRequest, options?: AxiosRequestConfig): AxiosPromise<void> { | ||||
|             return localVarFp.updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(axios, basePath)); | ||||
|         }, | ||||
|         /** | ||||
|          *  | ||||
|          * @param {AssetApiUploadFileRequest} requestParameters Request parameters. | ||||
| @@ -7011,6 +7099,20 @@ export interface AssetApiUpdateAssetRequest { | ||||
|     readonly updateAssetDto: UpdateAssetDto | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for updateAssets operation in AssetApi. | ||||
|  * @export | ||||
|  * @interface AssetApiUpdateAssetsRequest | ||||
|  */ | ||||
| export interface AssetApiUpdateAssetsRequest { | ||||
|     /** | ||||
|      *  | ||||
|      * @type {AssetBulkUpdateDto} | ||||
|      * @memberof AssetApiUpdateAssets | ||||
|      */ | ||||
|     readonly assetBulkUpdateDto: AssetBulkUpdateDto | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Request parameters for uploadFile operation in AssetApi. | ||||
|  * @export | ||||
| @@ -7366,6 +7468,17 @@ export class AssetApi extends BaseAPI { | ||||
|         return AssetApiFp(this.configuration).updateAsset(requestParameters.id, requestParameters.updateAssetDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {AssetApiUpdateAssetsRequest} requestParameters Request parameters. | ||||
|      * @param {*} [options] Override http request option. | ||||
|      * @throws {RequiredError} | ||||
|      * @memberof AssetApi | ||||
|      */ | ||||
|     public updateAssets(requestParameters: AssetApiUpdateAssetsRequest, options?: AxiosRequestConfig) { | ||||
|         return AssetApiFp(this.configuration).updateAssets(requestParameters.assetBulkUpdateDto, options).then((request) => request(this.axios, this.basePath)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  | ||||
|      * @param {AssetApiUploadFileRequest} requestParameters Request parameters. | ||||
|   | ||||
| @@ -4,15 +4,15 @@ | ||||
|     NotificationType, | ||||
|     notificationController, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { api } from '@api'; | ||||
|   import ArchiveArrowDownOutline from 'svelte-material-icons/ArchiveArrowDownOutline.svelte'; | ||||
|   import ArchiveArrowUpOutline from 'svelte-material-icons/ArchiveArrowUpOutline.svelte'; | ||||
|   import TimerSand from 'svelte-material-icons/TimerSand.svelte'; | ||||
|   import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; | ||||
|   import { OnAssetArchive, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import { OnArchive, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|  | ||||
|   export let onAssetArchive: OnAssetArchive = (asset, isArchived) => { | ||||
|     asset.isArchived = isArchived; | ||||
|   }; | ||||
|   export let onArchive: OnArchive | undefined = undefined; | ||||
|  | ||||
|   export let menuItem = false; | ||||
|   export let unarchive = false; | ||||
| @@ -20,32 +20,50 @@ | ||||
|   $: text = unarchive ? 'Unarchive' : 'Archive'; | ||||
|   $: logo = unarchive ? ArchiveArrowUpOutline : ArchiveArrowDownOutline; | ||||
|  | ||||
|   let loading = false; | ||||
|  | ||||
|   const { getAssets, clearSelect } = getAssetControlContext(); | ||||
|  | ||||
|   const handleArchive = async () => { | ||||
|     const isArchived = !unarchive; | ||||
|     let cnt = 0; | ||||
|     loading = true; | ||||
|  | ||||
|     for (const asset of getAssets()) { | ||||
|       if (asset.isArchived !== isArchived) { | ||||
|         api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { isArchived } }); | ||||
|     try { | ||||
|       const assets = Array.from(getAssets()).filter((asset) => asset.isArchived !== isArchived); | ||||
|       const ids = assets.map(({ id }) => id); | ||||
|  | ||||
|         onAssetArchive(asset, isArchived); | ||||
|         cnt = cnt + 1; | ||||
|       if (ids.length > 0) { | ||||
|         await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, isArchived } }); | ||||
|       } | ||||
|  | ||||
|       for (const asset of assets) { | ||||
|         asset.isArchived = isArchived; | ||||
|       } | ||||
|  | ||||
|       onArchive?.(ids, isArchived); | ||||
|  | ||||
|       notificationController.show({ | ||||
|         message: `${isArchived ? 'Archived' : 'Unarchived'} ${ids.length}`, | ||||
|         type: NotificationType.Info, | ||||
|       }); | ||||
|  | ||||
|       clearSelect(); | ||||
|     } catch (error) { | ||||
|       handleError(error, `Unable to ${isArchived ? 'archive' : 'unarchive'}`); | ||||
|     } finally { | ||||
|       loading = false; | ||||
|     } | ||||
|  | ||||
|     notificationController.show({ | ||||
|       message: `${isArchived ? 'Archived' : 'Unarchived'} ${cnt}`, | ||||
|       type: NotificationType.Info, | ||||
|     }); | ||||
|  | ||||
|     clearSelect(); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| {#if menuItem} | ||||
|   <MenuOption {text} on:click={handleArchive} /> | ||||
| {:else} | ||||
|   <CircleIconButton title={text} {logo} on:click={handleArchive} /> | ||||
| {/if} | ||||
|  | ||||
| {#if !menuItem} | ||||
|   {#if loading} | ||||
|     <CircleIconButton title="Loading" logo={TimerSand} /> | ||||
|   {:else} | ||||
|     <CircleIconButton title={text} {logo} on:click={handleArchive} /> | ||||
|   {/if} | ||||
| {/if} | ||||
|   | ||||
| @@ -1,23 +1,27 @@ | ||||
| <script lang="ts"> | ||||
|   import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; | ||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; | ||||
|   import { | ||||
|     NotificationType, | ||||
|     notificationController, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { api } from '@api'; | ||||
|   import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte'; | ||||
|   import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; | ||||
|   import { handleError } from '../../../utils/handle-error'; | ||||
|   import TimerSand from 'svelte-material-icons/TimerSand.svelte'; | ||||
|   import MenuOption from '../../shared-components/context-menu/menu-option.svelte'; | ||||
|   import { OnAssetDelete, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|  | ||||
|   export let onAssetDelete: OnAssetDelete; | ||||
|   export let menuItem = false; | ||||
|   const { getAssets, clearSelect } = getAssetControlContext(); | ||||
|  | ||||
|   let isShowConfirmation = false; | ||||
|   let loading = false; | ||||
|  | ||||
|   const handleDelete = async () => { | ||||
|     loading = true; | ||||
|  | ||||
|     try { | ||||
|       let count = 0; | ||||
|  | ||||
| @@ -44,14 +48,21 @@ | ||||
|       handleError(e, 'Error deleting assets'); | ||||
|     } finally { | ||||
|       isShowConfirmation = false; | ||||
|       loading = false; | ||||
|     } | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| {#if menuItem} | ||||
|   <MenuOption text="Delete" on:click={() => (isShowConfirmation = true)} /> | ||||
| {:else} | ||||
|   <CircleIconButton title="Delete" logo={DeleteOutline} on:click={() => (isShowConfirmation = true)} /> | ||||
| {/if} | ||||
|  | ||||
| {#if !menuItem} | ||||
|   {#if loading} | ||||
|     <CircleIconButton title="Loading" logo={TimerSand} /> | ||||
|   {:else} | ||||
|     <CircleIconButton title="Delete" logo={DeleteOutline} on:click={() => (isShowConfirmation = true)} /> | ||||
|   {/if} | ||||
| {/if} | ||||
|  | ||||
| {#if isShowConfirmation} | ||||
|   | ||||
| @@ -5,14 +5,14 @@ | ||||
|     NotificationType, | ||||
|     notificationController, | ||||
|   } from '$lib/components/shared-components/notification/notification'; | ||||
|   import { handleError } from '$lib/utils/handle-error'; | ||||
|   import { api } from '@api'; | ||||
|   import HeartMinusOutline from 'svelte-material-icons/HeartMinusOutline.svelte'; | ||||
|   import HeartOutline from 'svelte-material-icons/HeartOutline.svelte'; | ||||
|   import { OnAssetFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|   import TimerSand from 'svelte-material-icons/TimerSand.svelte'; | ||||
|   import { OnFavorite, getAssetControlContext } from '../asset-select-control-bar.svelte'; | ||||
|  | ||||
|   export let onAssetFavorite: OnAssetFavorite = (asset, isFavorite) => { | ||||
|     asset.isFavorite = isFavorite; | ||||
|   }; | ||||
|   export let onFavorite: OnFavorite | undefined = undefined; | ||||
|  | ||||
|   export let menuItem = false; | ||||
|   export let removeFavorite: boolean; | ||||
| @@ -20,31 +20,50 @@ | ||||
|   $: text = removeFavorite ? 'Remove from Favorites' : 'Favorite'; | ||||
|   $: logo = removeFavorite ? HeartMinusOutline : HeartOutline; | ||||
|  | ||||
|   let loading = false; | ||||
|  | ||||
|   const { getAssets, clearSelect } = getAssetControlContext(); | ||||
|  | ||||
|   const handleFavorite = () => { | ||||
|   const handleFavorite = async () => { | ||||
|     const isFavorite = !removeFavorite; | ||||
|     loading = true; | ||||
|  | ||||
|     let cnt = 0; | ||||
|     for (const asset of getAssets()) { | ||||
|       if (asset.isFavorite !== isFavorite) { | ||||
|         api.assetApi.updateAsset({ id: asset.id, updateAssetDto: { isFavorite } }); | ||||
|         onAssetFavorite(asset, isFavorite); | ||||
|         cnt = cnt + 1; | ||||
|     try { | ||||
|       const assets = Array.from(getAssets()).filter((asset) => asset.isFavorite !== isFavorite); | ||||
|       const ids = assets.map(({ id }) => id); | ||||
|  | ||||
|       if (ids.length > 0) { | ||||
|         await api.assetApi.updateAssets({ assetBulkUpdateDto: { ids, isFavorite } }); | ||||
|       } | ||||
|  | ||||
|       for (const asset of assets) { | ||||
|         asset.isFavorite = isFavorite; | ||||
|       } | ||||
|  | ||||
|       onFavorite?.(ids, isFavorite); | ||||
|  | ||||
|       notificationController.show({ | ||||
|         message: isFavorite ? `Added ${ids.length} to favorites` : `Removed ${ids.length} from favorites`, | ||||
|         type: NotificationType.Info, | ||||
|       }); | ||||
|  | ||||
|       clearSelect(); | ||||
|     } catch (error) { | ||||
|       handleError(error, `Unable to ${isFavorite ? 'add to' : 'remove from'} favorites`); | ||||
|     } finally { | ||||
|       loading = false; | ||||
|     } | ||||
|  | ||||
|     notificationController.show({ | ||||
|       message: isFavorite ? `Added ${cnt} to favorites` : `Removed ${cnt} from favorites`, | ||||
|       type: NotificationType.Info, | ||||
|     }); | ||||
|  | ||||
|     clearSelect(); | ||||
|   }; | ||||
| </script> | ||||
|  | ||||
| {#if menuItem} | ||||
|   <MenuOption {text} on:click={handleFavorite} /> | ||||
| {:else} | ||||
|   <CircleIconButton title={text} {logo} on:click={handleFavorite} /> | ||||
| {/if} | ||||
|  | ||||
| {#if !menuItem} | ||||
|   {#if loading} | ||||
|     <CircleIconButton title="Loading" logo={TimerSand} /> | ||||
|   {:else} | ||||
|     <CircleIconButton title={text} {logo} on:click={handleFavorite} /> | ||||
|   {/if} | ||||
| {/if} | ||||
|   | ||||
| @@ -2,8 +2,8 @@ | ||||
|   import { createContext } from '$lib/utils/context'; | ||||
|  | ||||
|   export type OnAssetDelete = (assetId: string) => void; | ||||
|   export type OnAssetArchive = (asset: AssetResponseDto, archived: boolean) => void; | ||||
|   export type OnAssetFavorite = (asset: AssetResponseDto, favorite: boolean) => void; | ||||
|   export type OnArchive = (ids: string[], isArchived: boolean) => void; | ||||
|   export type OnFavorite = (ids: string[], favorite: boolean) => void; | ||||
|  | ||||
|   export interface AssetControlContext { | ||||
|     // Wrap assets in a function, because context isn't reactive. | ||||
|   | ||||
| @@ -180,12 +180,19 @@ export class AssetStore { | ||||
|     this.emit(false); | ||||
|   } | ||||
|  | ||||
|   removeAsset(assetId: string) { | ||||
|   removeAssets(ids: string[]) { | ||||
|     // TODO: this could probably be more efficient | ||||
|     for (const id of ids) { | ||||
|       this.removeAsset(id); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   removeAsset(id: string) { | ||||
|     for (let i = 0; i < this.buckets.length; i++) { | ||||
|       const bucket = this.buckets[i]; | ||||
|       for (let j = 0; j < bucket.assets.length; j++) { | ||||
|         const asset = bucket.assets[j]; | ||||
|         if (asset.id !== assetId) { | ||||
|         if (asset.id !== id) { | ||||
|           continue; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,7 @@ | ||||
|  | ||||
| {#if $isMultiSelectState} | ||||
|   <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> | ||||
|     <ArchiveAction unarchive onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} /> | ||||
|     <ArchiveAction unarchive onArchive={(ids) => assetStore.removeAssets(ids)} /> | ||||
|     <CreateSharedLink /> | ||||
|     <SelectAllAssets {assetStore} {assetInteractionStore} /> | ||||
|     <AssetSelectContextMenu icon={Plus} title="Add"> | ||||
|   | ||||
| @@ -38,7 +38,7 @@ | ||||
| <!-- Multiselection mode app bar --> | ||||
| {#if $isMultiSelectState} | ||||
|   <AssetSelectControlBar assets={$selectedAssets} clearSelect={() => assetInteractionStore.clearMultiselect()}> | ||||
|     <FavoriteAction removeFavorite onAssetFavorite={(asset) => assetStore.removeAsset(asset.id)} /> | ||||
|     <FavoriteAction removeFavorite onFavorite={(ids) => assetStore.removeAssets(ids)} /> | ||||
|     <CreateSharedLink /> | ||||
|     <SelectAllAssets {assetStore} {assetInteractionStore} /> | ||||
|     <AssetSelectContextMenu icon={Plus} title="Add"> | ||||
|   | ||||
| @@ -202,11 +202,7 @@ | ||||
|       <AssetSelectContextMenu icon={DotsVertical} title="Add"> | ||||
|         <DownloadAction menuItem filename="{data.person.name || 'immich'}.zip" /> | ||||
|         <FavoriteAction menuItem removeFavorite={isAllFavorite} /> | ||||
|         <ArchiveAction | ||||
|           menuItem | ||||
|           unarchive={isAllArchive} | ||||
|           onAssetArchive={(asset) => $assetStore.removeAsset(asset.id)} | ||||
|         /> | ||||
|         <ArchiveAction menuItem unarchive={isAllArchive} onArchive={(ids) => $assetStore.removeAssets(ids)} /> | ||||
|       </AssetSelectContextMenu> | ||||
|     </AssetSelectControlBar> | ||||
|   {:else} | ||||
|   | ||||
| @@ -51,7 +51,7 @@ | ||||
|         <AssetSelectContextMenu icon={DotsVertical} title="Menu"> | ||||
|           <FavoriteAction menuItem removeFavorite={isAllFavorite} /> | ||||
|           <DownloadAction menuItem /> | ||||
|           <ArchiveAction menuItem onAssetArchive={(asset) => assetStore.removeAsset(asset.id)} /> | ||||
|           <ArchiveAction menuItem onArchive={(ids) => assetStore.removeAssets(ids)} /> | ||||
|         </AssetSelectContextMenu> | ||||
|       </AssetSelectControlBar> | ||||
|     {/if} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user