瀏覽代碼

feat(web) Individual assets shared mechanism (#1317)

* Create shared link modal for individual asset

* Added API to create asset shared link

* Added viewer for individual shared link

* Added multiselection app bar

* Refactor gallery viewer to its own component

* Refactor

* Refactor

* Add and remove asset from shared link

* Fixed test

* Fixed notification card doesn't wrap

* Add check asset access when created asset shared link

* pr feedback
Alex 2 年之前
父節點
當前提交
e9fda40b2b
共有 66 個文件被更改,包括 2085 次插入242 次删除
  1. 6 0
      mobile/openapi/.openapi-generator/FILES
  2. 5 1
      mobile/openapi/README.md
  3. 10 0
      mobile/openapi/doc/APIKeyApi.md
  4. 24 0
      mobile/openapi/doc/AlbumApi.md
  5. 128 0
      mobile/openapi/doc/AssetApi.md
  6. 10 0
      mobile/openapi/doc/AuthenticationApi.md
  7. 18 0
      mobile/openapi/doc/CreateAssetsShareLinkDto.md
  8. 2 0
      mobile/openapi/doc/DeviceInfoApi.md
  9. 6 0
      mobile/openapi/doc/JobApi.md
  10. 10 0
      mobile/openapi/doc/OAuthApi.md
  11. 8 0
      mobile/openapi/doc/ServerInfoApi.md
  12. 10 0
      mobile/openapi/doc/ShareApi.md
  13. 8 0
      mobile/openapi/doc/SystemConfigApi.md
  14. 10 0
      mobile/openapi/doc/TagApi.md
  15. 15 0
      mobile/openapi/doc/UpdateAssetsToSharedLinkDto.md
  16. 20 0
      mobile/openapi/doc/UserApi.md
  17. 2 0
      mobile/openapi/lib/api.dart
  18. 70 12
      mobile/openapi/lib/api/album_api.dart
  19. 28 5
      mobile/openapi/lib/api/api_key_api.dart
  20. 180 14
      mobile/openapi/lib/api/asset_api.dart
  21. 26 5
      mobile/openapi/lib/api/authentication_api.dart
  22. 6 1
      mobile/openapi/lib/api/device_info_api.dart
  23. 16 3
      mobile/openapi/lib/api/job_api.dart
  24. 26 5
      mobile/openapi/lib/api/o_auth_api.dart
  25. 16 4
      mobile/openapi/lib/api/server_info_api.dart
  26. 26 5
      mobile/openapi/lib/api/share_api.dart
  27. 18 4
      mobile/openapi/lib/api/system_config_api.dart
  28. 28 5
      mobile/openapi/lib/api/tag_api.dart
  29. 58 10
      mobile/openapi/lib/api/user_api.dart
  30. 4 0
      mobile/openapi/lib/api_client.dart
  31. 164 0
      mobile/openapi/lib/model/create_assets_share_link_dto.dart
  32. 113 0
      mobile/openapi/lib/model/update_assets_to_shared_link_dto.dart
  33. 24 0
      mobile/openapi/test/album_api_test.dart
  34. 10 0
      mobile/openapi/test/api_key_api_test.dart
  35. 42 0
      mobile/openapi/test/asset_api_test.dart
  36. 10 0
      mobile/openapi/test/authentication_api_test.dart
  37. 42 0
      mobile/openapi/test/create_assets_share_link_dto_test.dart
  38. 2 0
      mobile/openapi/test/device_info_api_test.dart
  39. 6 0
      mobile/openapi/test/job_api_test.dart
  40. 10 0
      mobile/openapi/test/o_auth_api_test.dart
  41. 8 0
      mobile/openapi/test/server_info_api_test.dart
  42. 10 0
      mobile/openapi/test/share_api_test.dart
  43. 8 0
      mobile/openapi/test/system_config_api_test.dart
  44. 10 0
      mobile/openapi/test/tag_api_test.dart
  45. 27 0
      mobile/openapi/test/update_assets_to_shared_link_dto_test.dart
  46. 20 0
      mobile/openapi/test/user_api_test.dart
  47. 22 0
      server/apps/immich/src/api-v1/asset/asset.controller.ts
  48. 40 1
      server/apps/immich/src/api-v1/asset/asset.service.ts
  49. 6 0
      server/apps/immich/src/api-v1/asset/dto/add-assets-to-shared-link.dto.ts
  50. 31 0
      server/apps/immich/src/api-v1/asset/dto/create-asset-shared-link.dto.ts
  51. 115 0
      server/immich-openapi-specs.json
  52. 191 1
      web/src/api/open-api/api.ts
  53. 1 1
      web/src/api/open-api/base.ts
  54. 1 1
      web/src/api/open-api/common.ts
  55. 1 1
      web/src/api/open-api/configuration.ts
  56. 1 1
      web/src/api/open-api/index.ts
  57. 6 118
      web/src/lib/components/album-page/album-viewer.svelte
  58. 1 1
      web/src/lib/components/asset-viewer/asset-viewer.svelte
  59. 150 0
      web/src/lib/components/share-page/individual-shared-viewer.svelte
  60. 48 25
      web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte
  61. 118 0
      web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte
  62. 1 1
      web/src/lib/components/shared-components/notification/notification-card.svelte
  63. 11 9
      web/src/lib/utils/file-uploader.ts
  64. 26 4
      web/src/routes/photos/+page.svelte
  65. 5 2
      web/src/routes/share/[key]/+page.server.ts
  66. 10 2
      web/src/routes/share/[key]/+page.svelte

+ 6 - 0
mobile/openapi/.openapi-generator/FILES

@@ -31,6 +31,7 @@ doc/CheckExistingAssetsDto.md
 doc/CheckExistingAssetsResponseDto.md
 doc/CreateAlbumDto.md
 doc/CreateAlbumShareLinkDto.md
+doc/CreateAssetsShareLinkDto.md
 doc/CreateProfileImageResponseDto.md
 doc/CreateTagDto.md
 doc/CreateUserDto.md
@@ -86,6 +87,7 @@ doc/ThumbnailFormat.md
 doc/TimeGroupEnum.md
 doc/UpdateAlbumDto.md
 doc/UpdateAssetDto.md
+doc/UpdateAssetsToSharedLinkDto.md
 doc/UpdateTagDto.md
 doc/UpdateUserDto.md
 doc/UpsertDeviceInfoDto.md
@@ -140,6 +142,7 @@ lib/model/check_existing_assets_dto.dart
 lib/model/check_existing_assets_response_dto.dart
 lib/model/create_album_dto.dart
 lib/model/create_album_share_link_dto.dart
+lib/model/create_assets_share_link_dto.dart
 lib/model/create_profile_image_response_dto.dart
 lib/model/create_tag_dto.dart
 lib/model/create_user_dto.dart
@@ -188,6 +191,7 @@ lib/model/thumbnail_format.dart
 lib/model/time_group_enum.dart
 lib/model/update_album_dto.dart
 lib/model/update_asset_dto.dart
+lib/model/update_assets_to_shared_link_dto.dart
 lib/model/update_tag_dto.dart
 lib/model/update_user_dto.dart
 lib/model/upsert_device_info_dto.dart
@@ -224,6 +228,7 @@ test/check_existing_assets_dto_test.dart
 test/check_existing_assets_response_dto_test.dart
 test/create_album_dto_test.dart
 test/create_album_share_link_dto_test.dart
+test/create_assets_share_link_dto_test.dart
 test/create_profile_image_response_dto_test.dart
 test/create_tag_dto_test.dart
 test/create_user_dto_test.dart
@@ -279,6 +284,7 @@ test/thumbnail_format_test.dart
 test/time_group_enum_test.dart
 test/update_album_dto_test.dart
 test/update_asset_dto_test.dart
+test/update_assets_to_shared_link_dto_test.dart
 test/update_tag_dto_test.dart
 test/update_user_dto_test.dart
 test/upsert_device_info_dto_test.dart

+ 5 - 1
mobile/openapi/README.md

@@ -3,7 +3,7 @@ Immich API
 
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 
-- API version: 1.40.0
+- API version: 1.41.1
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 
 ## Requirements
@@ -77,6 +77,7 @@ Class | Method | HTTP request | Description
 *AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} | 
 *AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 *AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
+*AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | 
 *AssetApi* | [**deleteAsset**](doc//AssetApi.md#deleteasset) | **DELETE** /asset | 
 *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **GET** /asset/download/{assetId} | 
 *AssetApi* | [**downloadFiles**](doc//AssetApi.md#downloadfiles) | **POST** /asset/download-files | 
@@ -94,6 +95,7 @@ Class | Method | HTTP request | Description
 *AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search | 
 *AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{assetId} | 
 *AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{assetId} | 
+*AssetApi* | [**updateAssetsInSharedLink**](doc//AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link | 
 *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 | 
@@ -167,6 +169,7 @@ Class | Method | HTTP request | Description
  - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
  - [CreateAlbumDto](doc//CreateAlbumDto.md)
  - [CreateAlbumShareLinkDto](doc//CreateAlbumShareLinkDto.md)
+ - [CreateAssetsShareLinkDto](doc//CreateAssetsShareLinkDto.md)
  - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
  - [CreateTagDto](doc//CreateTagDto.md)
  - [CreateUserDto](doc//CreateUserDto.md)
@@ -215,6 +218,7 @@ Class | Method | HTTP request | Description
  - [TimeGroupEnum](doc//TimeGroupEnum.md)
  - [UpdateAlbumDto](doc//UpdateAlbumDto.md)
  - [UpdateAssetDto](doc//UpdateAssetDto.md)
+ - [UpdateAssetsToSharedLinkDto](doc//UpdateAssetsToSharedLinkDto.md)
  - [UpdateTagDto](doc//UpdateTagDto.md)
  - [UpdateUserDto](doc//UpdateUserDto.md)
  - [UpsertDeviceInfoDto](doc//UpsertDeviceInfoDto.md)

+ 10 - 0
mobile/openapi/doc/APIKeyApi.md

@@ -21,6 +21,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -62,6 +64,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -102,6 +106,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -143,6 +149,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -180,6 +188,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 24 - 0
mobile/openapi/doc/AlbumApi.md

@@ -28,6 +28,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -77,6 +79,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -126,6 +130,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -173,6 +179,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -220,6 +228,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -266,6 +276,8 @@ void (empty response body)
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -315,6 +327,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -358,6 +372,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -405,6 +421,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -454,6 +472,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -503,6 +523,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -551,6 +573,8 @@ void (empty response body)
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 128 - 0
mobile/openapi/doc/AssetApi.md

@@ -11,6 +11,7 @@ Method | HTTP request | Description
 ------------- | ------------- | -------------
 [**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check | 
 [**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist | 
+[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link | 
 [**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset | 
 [**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download/{assetId} | 
 [**downloadFiles**](AssetApi.md#downloadfiles) | **POST** /asset/download-files | 
@@ -28,6 +29,7 @@ Method | HTTP request | Description
 [**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search | 
 [**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{assetId} | 
 [**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{assetId} | 
+[**updateAssetsInSharedLink**](AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link | 
 [**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload | 
 
 
@@ -129,11 +131,62 @@ 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)
 
+# **createAssetsSharedLink**
+> SharedLinkResponseDto createAssetsSharedLink(createAssetsShareLinkDto)
+
+
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// 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 createAssetsShareLinkDto = CreateAssetsShareLinkDto(); // CreateAssetsShareLinkDto | 
+
+try {
+    final result = api_instance.createAssetsSharedLink(createAssetsShareLinkDto);
+    print(result);
+} catch (e) {
+    print('Exception when calling AssetApi->createAssetsSharedLink: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **createAssetsShareLinkDto** | [**CreateAssetsShareLinkDto**](CreateAssetsShareLinkDto.md)|  | 
+
+### Return type
+
+[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[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)
+
 # **deleteAsset**
 > List<DeleteAssetResponseDto> deleteAsset(deleteAssetDto)
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -181,6 +234,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -232,6 +287,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -279,6 +336,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -424,6 +483,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -471,6 +532,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -518,6 +581,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -561,6 +626,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -604,6 +671,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -653,6 +722,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -696,6 +767,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -788,6 +861,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -835,6 +910,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -932,11 +1009,62 @@ 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)
 
+# **updateAssetsInSharedLink**
+> SharedLinkResponseDto updateAssetsInSharedLink(updateAssetsToSharedLinkDto)
+
+
+
+
+
+### Example
+```dart
+import 'package:openapi/api.dart';
+// 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 updateAssetsToSharedLinkDto = UpdateAssetsToSharedLinkDto(); // UpdateAssetsToSharedLinkDto | 
+
+try {
+    final result = api_instance.updateAssetsInSharedLink(updateAssetsToSharedLinkDto);
+    print(result);
+} catch (e) {
+    print('Exception when calling AssetApi->updateAssetsInSharedLink: $e\n');
+}
+```
+
+### Parameters
+
+Name | Type | Description  | Notes
+------------- | ------------- | ------------- | -------------
+ **updateAssetsToSharedLinkDto** | [**UpdateAssetsToSharedLinkDto**](UpdateAssetsToSharedLinkDto.md)|  | 
+
+### Return type
+
+[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
+
+### Authorization
+
+[bearer](../README.md#bearer)
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+[[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)
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 10 - 0
mobile/openapi/doc/AuthenticationApi.md

@@ -21,6 +21,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -62,6 +64,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -109,6 +113,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -150,6 +156,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -187,6 +195,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 18 - 0
mobile/openapi/doc/CreateAssetsShareLinkDto.md

@@ -0,0 +1,18 @@
+# openapi.model.CreateAssetsShareLinkDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**assetIds** | **List<String>** |  | [default to const []]
+**expiredAt** | **String** |  | [optional] 
+**allowUpload** | **bool** |  | [optional] 
+**description** | **String** |  | [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)
+
+

+ 2 - 0
mobile/openapi/doc/DeviceInfoApi.md

@@ -117,6 +117,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 6 - 0
mobile/openapi/doc/JobApi.md

@@ -19,6 +19,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -62,6 +64,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -109,6 +113,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 10 - 0
mobile/openapi/doc/OAuthApi.md

@@ -21,6 +21,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -62,6 +64,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -103,6 +107,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -144,6 +150,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -180,6 +188,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 8 - 0
mobile/openapi/doc/ServerInfoApi.md

@@ -20,6 +20,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -57,6 +59,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -94,6 +98,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -131,6 +137,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 10 - 0
mobile/openapi/doc/ShareApi.md

@@ -21,6 +21,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -64,6 +66,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -101,6 +105,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -138,6 +144,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -179,6 +187,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 8 - 0
mobile/openapi/doc/SystemConfigApi.md

@@ -20,6 +20,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -63,6 +65,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -106,6 +110,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -149,6 +155,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 10 - 0
mobile/openapi/doc/TagApi.md

@@ -21,6 +21,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -62,6 +64,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -102,6 +106,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -139,6 +145,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -180,6 +188,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 15 - 0
mobile/openapi/doc/UpdateAssetsToSharedLinkDto.md

@@ -0,0 +1,15 @@
+# openapi.model.UpdateAssetsToSharedLinkDto
+
+## Load the model package
+```dart
+import 'package:openapi/api.dart';
+```
+
+## Properties
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+**assetIds** | **List<String>** |  | [default to const []]
+
+[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
+
+

+ 20 - 0
mobile/openapi/doc/UserApi.md

@@ -26,6 +26,8 @@ Method | HTTP request | Description
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -73,6 +75,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -120,6 +124,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -167,6 +173,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -214,6 +222,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -257,6 +267,8 @@ This endpoint does not need any parameter.
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -298,6 +310,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -339,6 +353,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -380,6 +396,8 @@ No authorization required
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';
@@ -427,6 +445,8 @@ Name | Type | Description  | Notes
 
 
 
+
+
 ### Example
 ```dart
 import 'package:openapi/api.dart';

+ 2 - 0
mobile/openapi/lib/api.dart

@@ -64,6 +64,7 @@ part 'model/check_existing_assets_dto.dart';
 part 'model/check_existing_assets_response_dto.dart';
 part 'model/create_album_dto.dart';
 part 'model/create_album_share_link_dto.dart';
+part 'model/create_assets_share_link_dto.dart';
 part 'model/create_profile_image_response_dto.dart';
 part 'model/create_tag_dto.dart';
 part 'model/create_user_dto.dart';
@@ -112,6 +113,7 @@ part 'model/thumbnail_format.dart';
 part 'model/time_group_enum.dart';
 part 'model/update_album_dto.dart';
 part 'model/update_asset_dto.dart';
+part 'model/update_assets_to_shared_link_dto.dart';
 part 'model/update_tag_dto.dart';
 part 'model/update_user_dto.dart';
 part 'model/upsert_device_info_dto.dart';

+ 70 - 12
mobile/openapi/lib/api/album_api.dart

@@ -16,7 +16,10 @@ class AlbumApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'PUT /album/{albumId}/assets' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -48,6 +51,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -68,7 +73,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'PUT /album/{albumId}/users' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -100,6 +108,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -120,7 +130,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /album' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [CreateAlbumDto] createAlbumDto (required):
@@ -149,6 +162,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [CreateAlbumDto] createAlbumDto (required):
@@ -167,7 +182,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /album/create-shared-link' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
@@ -196,6 +214,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
@@ -214,7 +234,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /album/{albumId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -244,6 +267,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -254,7 +279,10 @@ class AlbumApi {
     }
   }
 
-  /// Performs an HTTP 'GET /album/{albumId}/download' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -290,6 +318,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -310,7 +340,9 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /album/count-by-user-id' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getAlbumCountByUserIdWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/album/count-by-user-id';
@@ -336,6 +368,7 @@ class AlbumApi {
     );
   }
 
+  /// 
   Future<AlbumCountResponseDto?> getAlbumCountByUserId() async {
     final response = await getAlbumCountByUserIdWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -351,7 +384,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /album/{albumId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -381,6 +417,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -399,7 +437,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /album' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [bool] shared:
@@ -438,6 +479,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [bool] shared:
@@ -462,7 +505,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /album/{albumId}/assets' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -494,6 +540,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -514,7 +562,10 @@ class AlbumApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /album/{albumId}/user/{userId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -547,6 +598,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -559,7 +612,10 @@ class AlbumApi {
     }
   }
 
-  /// Performs an HTTP 'PATCH /album/{albumId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):
@@ -591,6 +647,8 @@ class AlbumApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] albumId (required):

+ 28 - 5
mobile/openapi/lib/api/api_key_api.dart

@@ -16,7 +16,10 @@ class APIKeyApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'POST /api-key' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [APIKeyCreateDto] aPIKeyCreateDto (required):
@@ -45,6 +48,8 @@ class APIKeyApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [APIKeyCreateDto] aPIKeyCreateDto (required):
@@ -63,7 +68,10 @@ class APIKeyApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /api-key/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [num] id (required):
@@ -93,6 +101,8 @@ class APIKeyApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [num] id (required):
@@ -103,7 +113,10 @@ class APIKeyApi {
     }
   }
 
-  /// Performs an HTTP 'GET /api-key/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [num] id (required):
@@ -133,6 +146,8 @@ class APIKeyApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [num] id (required):
@@ -151,7 +166,9 @@ class APIKeyApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /api-key' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getKeysWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/api-key';
@@ -177,6 +194,7 @@ class APIKeyApi {
     );
   }
 
+  /// 
   Future<List<APIKeyResponseDto>?> getKeys() async {
     final response = await getKeysWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -195,7 +213,10 @@ class APIKeyApi {
     return null;
   }
 
-  /// Performs an HTTP 'PUT /api-key/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [num] id (required):
@@ -227,6 +248,8 @@ class APIKeyApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [num] id (required):

+ 180 - 14
mobile/openapi/lib/api/asset_api.dart

@@ -120,7 +120,62 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /asset' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
+  /// Parameters:
+  ///
+  /// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
+  Future<Response> createAssetsSharedLinkWithHttpInfo(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/asset/shared-link';
+
+    // ignore: prefer_final_locals
+    Object? postBody = createAssetsShareLinkDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'POST',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// 
+  ///
+  /// Parameters:
+  ///
+  /// * [CreateAssetsShareLinkDto] createAssetsShareLinkDto (required):
+  Future<SharedLinkResponseDto?> createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto,) async {
+    final response = await createAssetsSharedLinkWithHttpInfo(createAssetsShareLinkDto,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
+    
+    }
+    return null;
+  }
+
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [DeleteAssetDto] deleteAssetDto (required):
@@ -149,6 +204,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [DeleteAssetDto] deleteAssetDto (required):
@@ -170,7 +227,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/download/{assetId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] assetId (required):
@@ -211,6 +271,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] assetId (required):
@@ -233,7 +295,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/download-files' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [DownloadFilesDto] downloadFilesDto (required):
@@ -262,6 +327,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [DownloadFilesDto] downloadFilesDto (required):
@@ -280,7 +347,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/download-library' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [num] skip:
@@ -313,6 +383,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [num] skip:
@@ -445,7 +517,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/time-bucket' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
@@ -474,6 +549,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [GetAssetByTimeBucketDto] getAssetByTimeBucketDto (required):
@@ -495,7 +572,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/count-by-time-bucket' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
@@ -524,6 +604,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [GetAssetCountByTimeBucketDto] getAssetCountByTimeBucketDto (required):
@@ -542,7 +624,9 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/count-by-user-id' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getAssetCountByUserIdWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/asset/count-by-user-id';
@@ -568,6 +652,7 @@ class AssetApi {
     );
   }
 
+  /// 
   Future<AssetCountByUserIdResponseDto?> getAssetCountByUserId() async {
     final response = await getAssetCountByUserIdWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -583,7 +668,9 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/search-terms' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getAssetSearchTermsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/asset/search-terms';
@@ -609,6 +696,7 @@ class AssetApi {
     );
   }
 
+  /// 
   Future<List<String>?> getAssetSearchTerms() async {
     final response = await getAssetSearchTermsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -627,7 +715,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/thumbnail/{assetId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] assetId (required):
@@ -663,6 +754,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] assetId (required):
@@ -683,7 +776,9 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/curated-locations' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getCuratedLocationsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/asset/curated-locations';
@@ -709,6 +804,7 @@ class AssetApi {
     );
   }
 
+  /// 
   Future<List<CuratedLocationsResponseDto>?> getCuratedLocations() async {
     final response = await getCuratedLocationsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -727,7 +823,9 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/curated-objects' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getCuratedObjectsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/asset/curated-objects';
@@ -753,6 +851,7 @@ class AssetApi {
     );
   }
 
+  /// 
   Future<List<CuratedObjectsResponseDto>?> getCuratedObjects() async {
     final response = await getCuratedObjectsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -827,7 +926,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/search' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [SearchAssetDto] searchAssetDto (required):
@@ -856,6 +958,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [SearchAssetDto] searchAssetDto (required):
@@ -877,7 +981,10 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /asset/file/{assetId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] assetId (required):
@@ -918,6 +1025,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] assetId (required):
@@ -997,7 +1106,62 @@ class AssetApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
+  /// Parameters:
+  ///
+  /// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required):
+  Future<Response> updateAssetsInSharedLinkWithHttpInfo(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async {
+    // ignore: prefer_const_declarations
+    final path = r'/asset/shared-link';
+
+    // ignore: prefer_final_locals
+    Object? postBody = updateAssetsToSharedLinkDto;
+
+    final queryParams = <QueryParam>[];
+    final headerParams = <String, String>{};
+    final formParams = <String, String>{};
+
+    const contentTypes = <String>['application/json'];
+
+
+    return apiClient.invokeAPI(
+      path,
+      'PATCH',
+      queryParams,
+      postBody,
+      headerParams,
+      formParams,
+      contentTypes.isEmpty ? null : contentTypes.first,
+    );
+  }
+
+  /// 
+  ///
+  /// Parameters:
+  ///
+  /// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required):
+  Future<SharedLinkResponseDto?> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async {
+    final response = await updateAssetsInSharedLinkWithHttpInfo(updateAssetsToSharedLinkDto,);
+    if (response.statusCode >= HttpStatus.badRequest) {
+      throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+    }
+    // When a remote server returns no body with a status of 204, we shall not decode it.
+    // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+    // FormatException when trying to decode an empty string.
+    if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+      return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
+    
+    }
+    return null;
+  }
+
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [MultipartFile] assetData (required):
@@ -1036,6 +1200,8 @@ class AssetApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [MultipartFile] assetData (required):

+ 26 - 5
mobile/openapi/lib/api/authentication_api.dart

@@ -16,7 +16,10 @@ class AuthenticationApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'POST /auth/admin-sign-up' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [SignUpDto] signUpDto (required):
@@ -45,6 +48,8 @@ class AuthenticationApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [SignUpDto] signUpDto (required):
@@ -63,7 +68,10 @@ class AuthenticationApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /auth/change-password' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [ChangePasswordDto] changePasswordDto (required):
@@ -92,6 +100,8 @@ class AuthenticationApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [ChangePasswordDto] changePasswordDto (required):
@@ -110,7 +120,10 @@ class AuthenticationApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /auth/login' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [LoginCredentialDto] loginCredentialDto (required):
@@ -139,6 +152,8 @@ class AuthenticationApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [LoginCredentialDto] loginCredentialDto (required):
@@ -157,7 +172,9 @@ class AuthenticationApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /auth/logout' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> logoutWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/auth/logout';
@@ -183,6 +200,7 @@ class AuthenticationApi {
     );
   }
 
+  /// 
   Future<LogoutResponseDto?> logout() async {
     final response = await logoutWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -198,7 +216,9 @@ class AuthenticationApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /auth/validateToken' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> validateAccessTokenWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/auth/validateToken';
@@ -224,6 +244,7 @@ class AuthenticationApi {
     );
   }
 
+  /// 
   Future<ValidateAccessTokenResponseDto?> validateAccessToken() async {
     final response = await validateAccessTokenWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {

+ 6 - 1
mobile/openapi/lib/api/device_info_api.dart

@@ -120,7 +120,10 @@ class DeviceInfoApi {
     return null;
   }
 
-  /// Performs an HTTP 'PUT /device-info' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [UpsertDeviceInfoDto] upsertDeviceInfoDto (required):
@@ -149,6 +152,8 @@ class DeviceInfoApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [UpsertDeviceInfoDto] upsertDeviceInfoDto (required):

+ 16 - 3
mobile/openapi/lib/api/job_api.dart

@@ -16,7 +16,9 @@ class JobApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'GET /jobs' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getAllJobsStatusWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/jobs';
@@ -42,6 +44,7 @@ class JobApi {
     );
   }
 
+  /// 
   Future<AllJobStatusResponseDto?> getAllJobsStatus() async {
     final response = await getAllJobsStatusWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -57,7 +60,10 @@ class JobApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /jobs/{jobId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [JobId] jobId (required):
@@ -87,6 +93,8 @@ class JobApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [JobId] jobId (required):
@@ -105,7 +113,10 @@ class JobApi {
     return null;
   }
 
-  /// Performs an HTTP 'PUT /jobs/{jobId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [JobId] jobId (required):
@@ -137,6 +148,8 @@ class JobApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [JobId] jobId (required):

+ 26 - 5
mobile/openapi/lib/api/o_auth_api.dart

@@ -16,7 +16,10 @@ class OAuthApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'POST /oauth/callback' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [OAuthCallbackDto] oAuthCallbackDto (required):
@@ -45,6 +48,8 @@ class OAuthApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [OAuthCallbackDto] oAuthCallbackDto (required):
@@ -63,7 +68,10 @@ class OAuthApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /oauth/config' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [OAuthConfigDto] oAuthConfigDto (required):
@@ -92,6 +100,8 @@ class OAuthApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [OAuthConfigDto] oAuthConfigDto (required):
@@ -110,7 +120,10 @@ class OAuthApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /oauth/link' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [OAuthCallbackDto] oAuthCallbackDto (required):
@@ -139,6 +152,8 @@ class OAuthApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [OAuthCallbackDto] oAuthCallbackDto (required):
@@ -157,7 +172,9 @@ class OAuthApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /oauth/mobile-redirect' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> mobileRedirectWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/oauth/mobile-redirect';
@@ -183,6 +200,7 @@ class OAuthApi {
     );
   }
 
+  /// 
   Future<void> mobileRedirect() async {
     final response = await mobileRedirectWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -190,7 +208,9 @@ class OAuthApi {
     }
   }
 
-  /// Performs an HTTP 'POST /oauth/unlink' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> unlinkWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/oauth/unlink';
@@ -216,6 +236,7 @@ class OAuthApi {
     );
   }
 
+  /// 
   Future<UserResponseDto?> unlink() async {
     final response = await unlinkWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {

+ 16 - 4
mobile/openapi/lib/api/server_info_api.dart

@@ -16,7 +16,9 @@ class ServerInfoApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'GET /server-info' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getServerInfoWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/server-info';
@@ -42,6 +44,7 @@ class ServerInfoApi {
     );
   }
 
+  /// 
   Future<ServerInfoResponseDto?> getServerInfo() async {
     final response = await getServerInfoWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -57,7 +60,9 @@ class ServerInfoApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /server-info/version' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getServerVersionWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/server-info/version';
@@ -83,6 +88,7 @@ class ServerInfoApi {
     );
   }
 
+  /// 
   Future<ServerVersionReponseDto?> getServerVersion() async {
     final response = await getServerVersionWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -98,7 +104,9 @@ class ServerInfoApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /server-info/stats' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getStatsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/server-info/stats';
@@ -124,6 +132,7 @@ class ServerInfoApi {
     );
   }
 
+  /// 
   Future<ServerStatsResponseDto?> getStats() async {
     final response = await getStatsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -139,7 +148,9 @@ class ServerInfoApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /server-info/ping' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> pingServerWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/server-info/ping';
@@ -165,6 +176,7 @@ class ServerInfoApi {
     );
   }
 
+  /// 
   Future<ServerPingResponse?> pingServer() async {
     final response = await pingServerWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {

+ 26 - 5
mobile/openapi/lib/api/share_api.dart

@@ -16,7 +16,10 @@ class ShareApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'PATCH /share/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -48,6 +51,8 @@ class ShareApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -68,7 +73,9 @@ class ShareApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /share' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getAllSharedLinksWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/share';
@@ -94,6 +101,7 @@ class ShareApi {
     );
   }
 
+  /// 
   Future<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
     final response = await getAllSharedLinksWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -112,7 +120,9 @@ class ShareApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /share/me' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getMySharedLinkWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/share/me';
@@ -138,6 +148,7 @@ class ShareApi {
     );
   }
 
+  /// 
   Future<SharedLinkResponseDto?> getMySharedLink() async {
     final response = await getMySharedLinkWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -153,7 +164,10 @@ class ShareApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /share/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -183,6 +197,8 @@ class ShareApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -201,7 +217,10 @@ class ShareApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /share/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -231,6 +250,8 @@ class ShareApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):

+ 18 - 4
mobile/openapi/lib/api/system_config_api.dart

@@ -16,7 +16,9 @@ class SystemConfigApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'GET /system-config' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getConfigWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/system-config';
@@ -42,6 +44,7 @@ class SystemConfigApi {
     );
   }
 
+  /// 
   Future<SystemConfigDto?> getConfig() async {
     final response = await getConfigWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -57,7 +60,9 @@ class SystemConfigApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /system-config/defaults' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getDefaultsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/system-config/defaults';
@@ -83,6 +88,7 @@ class SystemConfigApi {
     );
   }
 
+  /// 
   Future<SystemConfigDto?> getDefaults() async {
     final response = await getDefaultsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -98,7 +104,9 @@ class SystemConfigApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /system-config/storage-template-options' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getStorageTemplateOptionsWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/system-config/storage-template-options';
@@ -124,6 +132,7 @@ class SystemConfigApi {
     );
   }
 
+  /// 
   Future<SystemConfigTemplateStorageOptionDto?> getStorageTemplateOptions() async {
     final response = await getStorageTemplateOptionsWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -139,7 +148,10 @@ class SystemConfigApi {
     return null;
   }
 
-  /// Performs an HTTP 'PUT /system-config' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [SystemConfigDto] systemConfigDto (required):
@@ -168,6 +180,8 @@ class SystemConfigApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [SystemConfigDto] systemConfigDto (required):

+ 28 - 5
mobile/openapi/lib/api/tag_api.dart

@@ -16,7 +16,10 @@ class TagApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'POST /tag' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [CreateTagDto] createTagDto (required):
@@ -45,6 +48,8 @@ class TagApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [CreateTagDto] createTagDto (required):
@@ -63,7 +68,10 @@ class TagApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /tag/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -93,6 +101,8 @@ class TagApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -103,7 +113,9 @@ class TagApi {
     }
   }
 
-  /// Performs an HTTP 'GET /tag' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> findAllWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/tag';
@@ -129,6 +141,7 @@ class TagApi {
     );
   }
 
+  /// 
   Future<List<TagResponseDto>?> findAll() async {
     final response = await findAllWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -147,7 +160,10 @@ class TagApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /tag/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -177,6 +193,8 @@ class TagApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -195,7 +213,10 @@ class TagApi {
     return null;
   }
 
-  /// Performs an HTTP 'PATCH /tag/{id}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):
@@ -227,6 +248,8 @@ class TagApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] id (required):

+ 58 - 10
mobile/openapi/lib/api/user_api.dart

@@ -16,7 +16,10 @@ class UserApi {
 
   final ApiClient apiClient;
 
-  /// Performs an HTTP 'POST /user/profile-image' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [MultipartFile] file (required):
@@ -55,6 +58,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [MultipartFile] file (required):
@@ -73,7 +78,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /user' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [CreateUserDto] createUserDto (required):
@@ -102,6 +110,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [CreateUserDto] createUserDto (required):
@@ -120,7 +130,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'DELETE /user/{userId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -150,6 +163,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -168,7 +183,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /user' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [bool] isAll (required):
@@ -199,6 +217,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [bool] isAll (required):
@@ -220,7 +240,9 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /user/me' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
   Future<Response> getMyUserInfoWithHttpInfo() async {
     // ignore: prefer_const_declarations
     final path = r'/user/me';
@@ -246,6 +268,7 @@ class UserApi {
     );
   }
 
+  /// 
   Future<UserResponseDto?> getMyUserInfo() async {
     final response = await getMyUserInfoWithHttpInfo();
     if (response.statusCode >= HttpStatus.badRequest) {
@@ -261,7 +284,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /user/profile-image/{userId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -291,6 +317,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -309,7 +337,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /user/info/{userId}' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -339,6 +370,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -357,7 +390,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'GET /user/count' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [bool] admin:
@@ -390,6 +426,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [bool] admin:
@@ -408,7 +446,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'POST /user/{userId}/restore' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -438,6 +479,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [String] userId (required):
@@ -456,7 +499,10 @@ class UserApi {
     return null;
   }
 
-  /// Performs an HTTP 'PUT /user' operation and returns the [Response].
+  /// 
+  ///
+  /// Note: This method returns the HTTP [Response].
+  ///
   /// Parameters:
   ///
   /// * [UpdateUserDto] updateUserDto (required):
@@ -485,6 +531,8 @@ class UserApi {
     );
   }
 
+  /// 
+  ///
   /// Parameters:
   ///
   /// * [UpdateUserDto] updateUserDto (required):

+ 4 - 0
mobile/openapi/lib/api_client.dart

@@ -240,6 +240,8 @@ class ApiClient {
           return CreateAlbumDto.fromJson(value);
         case 'CreateAlbumShareLinkDto':
           return CreateAlbumShareLinkDto.fromJson(value);
+        case 'CreateAssetsShareLinkDto':
+          return CreateAssetsShareLinkDto.fromJson(value);
         case 'CreateProfileImageResponseDto':
           return CreateProfileImageResponseDto.fromJson(value);
         case 'CreateTagDto':
@@ -336,6 +338,8 @@ class ApiClient {
           return UpdateAlbumDto.fromJson(value);
         case 'UpdateAssetDto':
           return UpdateAssetDto.fromJson(value);
+        case 'UpdateAssetsToSharedLinkDto':
+          return UpdateAssetsToSharedLinkDto.fromJson(value);
         case 'UpdateTagDto':
           return UpdateTagDto.fromJson(value);
         case 'UpdateUserDto':

+ 164 - 0
mobile/openapi/lib/model/create_assets_share_link_dto.dart

@@ -0,0 +1,164 @@
+//
+// 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 CreateAssetsShareLinkDto {
+  /// Returns a new [CreateAssetsShareLinkDto] instance.
+  CreateAssetsShareLinkDto({
+    this.assetIds = const [],
+    this.expiredAt,
+    this.allowUpload,
+    this.description,
+  });
+
+  List<String> assetIds;
+
+  ///
+  /// 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.
+  ///
+  String? expiredAt;
+
+  ///
+  /// 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? allowUpload;
+
+  ///
+  /// 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.
+  ///
+  String? description;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is CreateAssetsShareLinkDto &&
+     other.assetIds == assetIds &&
+     other.expiredAt == expiredAt &&
+     other.allowUpload == allowUpload &&
+     other.description == description;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (assetIds.hashCode) +
+    (expiredAt == null ? 0 : expiredAt!.hashCode) +
+    (allowUpload == null ? 0 : allowUpload!.hashCode) +
+    (description == null ? 0 : description!.hashCode);
+
+  @override
+  String toString() => 'CreateAssetsShareLinkDto[assetIds=$assetIds, expiredAt=$expiredAt, allowUpload=$allowUpload, description=$description]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'assetIds'] = this.assetIds;
+    if (this.expiredAt != null) {
+      json[r'expiredAt'] = this.expiredAt;
+    } else {
+      // json[r'expiredAt'] = null;
+    }
+    if (this.allowUpload != null) {
+      json[r'allowUpload'] = this.allowUpload;
+    } else {
+      // json[r'allowUpload'] = null;
+    }
+    if (this.description != null) {
+      json[r'description'] = this.description;
+    } else {
+      // json[r'description'] = null;
+    }
+    return json;
+  }
+
+  /// Returns a new [CreateAssetsShareLinkDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static CreateAssetsShareLinkDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "CreateAssetsShareLinkDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "CreateAssetsShareLinkDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return CreateAssetsShareLinkDto(
+        assetIds: json[r'assetIds'] is List
+            ? (json[r'assetIds'] as List).cast<String>()
+            : const [],
+        expiredAt: mapValueOfType<String>(json, r'expiredAt'),
+        allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
+        description: mapValueOfType<String>(json, r'description'),
+      );
+    }
+    return null;
+  }
+
+  static List<CreateAssetsShareLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <CreateAssetsShareLinkDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = CreateAssetsShareLinkDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, CreateAssetsShareLinkDto> mapFromJson(dynamic json) {
+    final map = <String, CreateAssetsShareLinkDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = CreateAssetsShareLinkDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of CreateAssetsShareLinkDto-objects as value to a dart map
+  static Map<String, List<CreateAssetsShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<CreateAssetsShareLinkDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = CreateAssetsShareLinkDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'assetIds',
+  };
+}
+

+ 113 - 0
mobile/openapi/lib/model/update_assets_to_shared_link_dto.dart

@@ -0,0 +1,113 @@
+//
+// 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 UpdateAssetsToSharedLinkDto {
+  /// Returns a new [UpdateAssetsToSharedLinkDto] instance.
+  UpdateAssetsToSharedLinkDto({
+    this.assetIds = const [],
+  });
+
+  List<String> assetIds;
+
+  @override
+  bool operator ==(Object other) => identical(this, other) || other is UpdateAssetsToSharedLinkDto &&
+     other.assetIds == assetIds;
+
+  @override
+  int get hashCode =>
+    // ignore: unnecessary_parenthesis
+    (assetIds.hashCode);
+
+  @override
+  String toString() => 'UpdateAssetsToSharedLinkDto[assetIds=$assetIds]';
+
+  Map<String, dynamic> toJson() {
+    final json = <String, dynamic>{};
+      json[r'assetIds'] = this.assetIds;
+    return json;
+  }
+
+  /// Returns a new [UpdateAssetsToSharedLinkDto] instance and imports its values from
+  /// [value] if it's a [Map], null otherwise.
+  // ignore: prefer_constructors_over_static_methods
+  static UpdateAssetsToSharedLinkDto? fromJson(dynamic value) {
+    if (value is Map) {
+      final json = value.cast<String, dynamic>();
+
+      // Ensure that the map contains the required keys.
+      // Note 1: the values aren't checked for validity beyond being non-null.
+      // Note 2: this code is stripped in release mode!
+      assert(() {
+        requiredKeys.forEach((key) {
+          assert(json.containsKey(key), 'Required key "UpdateAssetsToSharedLinkDto[$key]" is missing from JSON.');
+          assert(json[key] != null, 'Required key "UpdateAssetsToSharedLinkDto[$key]" has a null value in JSON.');
+        });
+        return true;
+      }());
+
+      return UpdateAssetsToSharedLinkDto(
+        assetIds: json[r'assetIds'] is List
+            ? (json[r'assetIds'] as List).cast<String>()
+            : const [],
+      );
+    }
+    return null;
+  }
+
+  static List<UpdateAssetsToSharedLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
+    final result = <UpdateAssetsToSharedLinkDto>[];
+    if (json is List && json.isNotEmpty) {
+      for (final row in json) {
+        final value = UpdateAssetsToSharedLinkDto.fromJson(row);
+        if (value != null) {
+          result.add(value);
+        }
+      }
+    }
+    return result.toList(growable: growable);
+  }
+
+  static Map<String, UpdateAssetsToSharedLinkDto> mapFromJson(dynamic json) {
+    final map = <String, UpdateAssetsToSharedLinkDto>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = UpdateAssetsToSharedLinkDto.fromJson(entry.value);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  // maps a json object with a list of UpdateAssetsToSharedLinkDto-objects as value to a dart map
+  static Map<String, List<UpdateAssetsToSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
+    final map = <String, List<UpdateAssetsToSharedLinkDto>>{};
+    if (json is Map && json.isNotEmpty) {
+      json = json.cast<String, dynamic>(); // ignore: parameter_assignments
+      for (final entry in json.entries) {
+        final value = UpdateAssetsToSharedLinkDto.listFromJson(entry.value, growable: growable,);
+        if (value != null) {
+          map[entry.key] = value;
+        }
+      }
+    }
+    return map;
+  }
+
+  /// The list of required keys that must be present in a JSON.
+  static const requiredKeys = <String>{
+    'assetIds',
+  };
+}
+

+ 24 - 0
mobile/openapi/test/album_api_test.dart

@@ -17,61 +17,85 @@ void main() {
   // final instance = AlbumApi();
 
   group('tests for AlbumApi', () {
+    // 
+    //
     //Future<AddAssetsResponseDto> addAssetsToAlbum(String albumId, AddAssetsDto addAssetsDto) async
     test('test addAssetsToAlbum', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AlbumResponseDto> addUsersToAlbum(String albumId, AddUsersDto addUsersDto) async
     test('test addUsersToAlbum', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AlbumResponseDto> createAlbum(CreateAlbumDto createAlbumDto) async
     test('test createAlbum', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<SharedLinkResponseDto> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto) async
     test('test createAlbumSharedLink', () async {
       // TODO
     });
 
+    // 
+    //
     //Future deleteAlbum(String albumId) async
     test('test deleteAlbum', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> downloadArchive(String albumId, { num skip }) async
     test('test downloadArchive', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AlbumCountResponseDto> getAlbumCountByUserId() async
     test('test getAlbumCountByUserId', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AlbumResponseDto> getAlbumInfo(String albumId) async
     test('test getAlbumInfo', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<AlbumResponseDto>> getAllAlbums({ bool shared, String assetId }) async
     test('test getAllAlbums', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AlbumResponseDto> removeAssetFromAlbum(String albumId, RemoveAssetsDto removeAssetsDto) async
     test('test removeAssetFromAlbum', () async {
       // TODO
     });
 
+    // 
+    //
     //Future removeUserFromAlbum(String albumId, String userId) async
     test('test removeUserFromAlbum', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AlbumResponseDto> updateAlbumInfo(String albumId, UpdateAlbumDto updateAlbumDto) async
     test('test updateAlbumInfo', () async {
       // TODO

+ 10 - 0
mobile/openapi/test/api_key_api_test.dart

@@ -17,26 +17,36 @@ void main() {
   // final instance = APIKeyApi();
 
   group('tests for APIKeyApi', () {
+    // 
+    //
     //Future<APIKeyCreateResponseDto> createKey(APIKeyCreateDto aPIKeyCreateDto) async
     test('test createKey', () async {
       // TODO
     });
 
+    // 
+    //
     //Future deleteKey(num id) async
     test('test deleteKey', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<APIKeyResponseDto> getKey(num id) async
     test('test getKey', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<APIKeyResponseDto>> getKeys() async
     test('test getKeys', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<APIKeyResponseDto> updateKey(num id, APIKeyUpdateDto aPIKeyUpdateDto) async
     test('test updateKey', () async {
       // TODO

+ 42 - 0
mobile/openapi/test/asset_api_test.dart

@@ -31,21 +31,36 @@ void main() {
       // TODO
     });
 
+    // 
+    //
+    //Future<SharedLinkResponseDto> createAssetsSharedLink(CreateAssetsShareLinkDto createAssetsShareLinkDto) async
+    test('test createAssetsSharedLink', () async {
+      // TODO
+    });
+
+    // 
+    //
     //Future<List<DeleteAssetResponseDto>> deleteAsset(DeleteAssetDto deleteAssetDto) async
     test('test deleteAsset', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> downloadFile(String assetId, { bool isThumb, bool isWeb }) async
     test('test downloadFile', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> downloadFiles(DownloadFilesDto downloadFilesDto) async
     test('test downloadFiles', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> downloadLibrary({ num skip }) async
     test('test downloadLibrary', () async {
       // TODO
@@ -65,36 +80,50 @@ void main() {
       // TODO
     });
 
+    // 
+    //
     //Future<List<AssetResponseDto>> getAssetByTimeBucket(GetAssetByTimeBucketDto getAssetByTimeBucketDto) async
     test('test getAssetByTimeBucket', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AssetCountByTimeBucketResponseDto> getAssetCountByTimeBucket(GetAssetCountByTimeBucketDto getAssetCountByTimeBucketDto) async
     test('test getAssetCountByTimeBucket', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<AssetCountByUserIdResponseDto> getAssetCountByUserId() async
     test('test getAssetCountByUserId', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<String>> getAssetSearchTerms() async
     test('test getAssetSearchTerms', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> getAssetThumbnail(String assetId, { ThumbnailFormat format }) async
     test('test getAssetThumbnail', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<CuratedLocationsResponseDto>> getCuratedLocations() async
     test('test getCuratedLocations', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<CuratedObjectsResponseDto>> getCuratedObjects() async
     test('test getCuratedObjects', () async {
       // TODO
@@ -107,11 +136,15 @@ void main() {
       // TODO
     });
 
+    // 
+    //
     //Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async
     test('test searchAsset', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> serveFile(String assetId, { bool isThumb, bool isWeb }) async
     test('test serveFile', () async {
       // TODO
@@ -124,6 +157,15 @@ void main() {
       // TODO
     });
 
+    // 
+    //
+    //Future<SharedLinkResponseDto> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto) async
+    test('test updateAssetsInSharedLink', () async {
+      // TODO
+    });
+
+    // 
+    //
     //Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData) async
     test('test uploadFile', () async {
       // TODO

+ 10 - 0
mobile/openapi/test/authentication_api_test.dart

@@ -17,26 +17,36 @@ void main() {
   // final instance = AuthenticationApi();
 
   group('tests for AuthenticationApi', () {
+    // 
+    //
     //Future<AdminSignupResponseDto> adminSignUp(SignUpDto signUpDto) async
     test('test adminSignUp', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> changePassword(ChangePasswordDto changePasswordDto) async
     test('test changePassword', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<LoginResponseDto> login(LoginCredentialDto loginCredentialDto) async
     test('test login', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<LogoutResponseDto> logout() async
     test('test logout', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<ValidateAccessTokenResponseDto> validateAccessToken() async
     test('test validateAccessToken', () async {
       // TODO

+ 42 - 0
mobile/openapi/test/create_assets_share_link_dto_test.dart

@@ -0,0 +1,42 @@
+//
+// 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 CreateAssetsShareLinkDto
+void main() {
+  // final instance = CreateAssetsShareLinkDto();
+
+  group('test CreateAssetsShareLinkDto', () {
+    // List<String> assetIds (default value: const [])
+    test('to test the property `assetIds`', () async {
+      // TODO
+    });
+
+    // String expiredAt
+    test('to test the property `expiredAt`', () async {
+      // TODO
+    });
+
+    // bool allowUpload
+    test('to test the property `allowUpload`', () async {
+      // TODO
+    });
+
+    // String description
+    test('to test the property `description`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 2 - 0
mobile/openapi/test/device_info_api_test.dart

@@ -31,6 +31,8 @@ void main() {
       // TODO
     });
 
+    // 
+    //
     //Future<DeviceInfoResponseDto> upsertDeviceInfo(UpsertDeviceInfoDto upsertDeviceInfoDto) async
     test('test upsertDeviceInfo', () async {
       // TODO

+ 6 - 0
mobile/openapi/test/job_api_test.dart

@@ -17,16 +17,22 @@ void main() {
   // final instance = JobApi();
 
   group('tests for JobApi', () {
+    // 
+    //
     //Future<AllJobStatusResponseDto> getAllJobsStatus() async
     test('test getAllJobsStatus', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<JobStatusResponseDto> getJobStatus(JobId jobId) async
     test('test getJobStatus', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<num> sendJobCommand(JobId jobId, JobCommandDto jobCommandDto) async
     test('test sendJobCommand', () async {
       // TODO

+ 10 - 0
mobile/openapi/test/o_auth_api_test.dart

@@ -17,26 +17,36 @@ void main() {
   // final instance = OAuthApi();
 
   group('tests for OAuthApi', () {
+    // 
+    //
     //Future<LoginResponseDto> callback(OAuthCallbackDto oAuthCallbackDto) async
     test('test callback', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<OAuthConfigResponseDto> generateConfig(OAuthConfigDto oAuthConfigDto) async
     test('test generateConfig', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> link(OAuthCallbackDto oAuthCallbackDto) async
     test('test link', () async {
       // TODO
     });
 
+    // 
+    //
     //Future mobileRedirect() async
     test('test mobileRedirect', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> unlink() async
     test('test unlink', () async {
       // TODO

+ 8 - 0
mobile/openapi/test/server_info_api_test.dart

@@ -17,21 +17,29 @@ void main() {
   // final instance = ServerInfoApi();
 
   group('tests for ServerInfoApi', () {
+    // 
+    //
     //Future<ServerInfoResponseDto> getServerInfo() async
     test('test getServerInfo', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<ServerVersionReponseDto> getServerVersion() async
     test('test getServerVersion', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<ServerStatsResponseDto> getStats() async
     test('test getStats', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<ServerPingResponse> pingServer() async
     test('test pingServer', () async {
       // TODO

+ 10 - 0
mobile/openapi/test/share_api_test.dart

@@ -17,26 +17,36 @@ void main() {
   // final instance = ShareApi();
 
   group('tests for ShareApi', () {
+    // 
+    //
     //Future<SharedLinkResponseDto> editSharedLink(String id, EditSharedLinkDto editSharedLinkDto) async
     test('test editSharedLink', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<SharedLinkResponseDto>> getAllSharedLinks() async
     test('test getAllSharedLinks', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<SharedLinkResponseDto> getMySharedLink() async
     test('test getMySharedLink', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<SharedLinkResponseDto> getSharedLinkById(String id) async
     test('test getSharedLinkById', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<String> removeSharedLink(String id) async
     test('test removeSharedLink', () async {
       // TODO

+ 8 - 0
mobile/openapi/test/system_config_api_test.dart

@@ -17,21 +17,29 @@ void main() {
   // final instance = SystemConfigApi();
 
   group('tests for SystemConfigApi', () {
+    // 
+    //
     //Future<SystemConfigDto> getConfig() async
     test('test getConfig', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<SystemConfigDto> getDefaults() async
     test('test getDefaults', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<SystemConfigTemplateStorageOptionDto> getStorageTemplateOptions() async
     test('test getStorageTemplateOptions', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<SystemConfigDto> updateConfig(SystemConfigDto systemConfigDto) async
     test('test updateConfig', () async {
       // TODO

+ 10 - 0
mobile/openapi/test/tag_api_test.dart

@@ -17,26 +17,36 @@ void main() {
   // final instance = TagApi();
 
   group('tests for TagApi', () {
+    // 
+    //
     //Future<TagResponseDto> create(CreateTagDto createTagDto) async
     test('test create', () async {
       // TODO
     });
 
+    // 
+    //
     //Future delete(String id) async
     test('test delete', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<TagResponseDto>> findAll() async
     test('test findAll', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<TagResponseDto> findOne(String id) async
     test('test findOne', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<TagResponseDto> update(String id, UpdateTagDto updateTagDto) async
     test('test update', () async {
       // TODO

+ 27 - 0
mobile/openapi/test/update_assets_to_shared_link_dto_test.dart

@@ -0,0 +1,27 @@
+//
+// 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 UpdateAssetsToSharedLinkDto
+void main() {
+  // final instance = UpdateAssetsToSharedLinkDto();
+
+  group('test UpdateAssetsToSharedLinkDto', () {
+    // List<String> assetIds (default value: const [])
+    test('to test the property `assetIds`', () async {
+      // TODO
+    });
+
+
+  });
+
+}

+ 20 - 0
mobile/openapi/test/user_api_test.dart

@@ -17,51 +17,71 @@ void main() {
   // final instance = UserApi();
 
   group('tests for UserApi', () {
+    // 
+    //
     //Future<CreateProfileImageResponseDto> createProfileImage(MultipartFile file) async
     test('test createProfileImage', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> createUser(CreateUserDto createUserDto) async
     test('test createUser', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> deleteUser(String userId) async
     test('test deleteUser', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<List<UserResponseDto>> getAllUsers(bool isAll) async
     test('test getAllUsers', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> getMyUserInfo() async
     test('test getMyUserInfo', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<Object> getProfileImage(String userId) async
     test('test getProfileImage', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> getUserById(String userId) async
     test('test getUserById', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserCountResponseDto> getUserCount({ bool admin }) async
     test('test getUserCount', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> restoreUser(String userId) async
     test('test restoreUser', () async {
       // TODO
     });
 
+    // 
+    //
     //Future<UserResponseDto> updateUser(UpdateUserDto updateUserDto) async
     test('test updateUser', () async {
       // TODO

+ 22 - 0
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -14,6 +14,7 @@ import {
   Header,
   Put,
   UploadedFiles,
+  Patch,
 } from '@nestjs/common';
 import { Authenticated } from '../../decorators/authenticated.decorator';
 import { AssetService } from './asset.service';
@@ -50,6 +51,9 @@ import {
   IMMICH_CONTENT_LENGTH_HINT,
 } from '../../constants/download.constant';
 import { DownloadFilesDto } from './dto/download-files.dto';
+import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
+import { SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
+import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
 
 @ApiBearerAuth()
 @ApiTags('Asset')
@@ -321,4 +325,22 @@ export class AssetController {
   ): Promise<CheckExistingAssetsResponseDto> {
     return await this.assetService.checkExistingAssets(authUser, checkExistingAssetsDto);
   }
+
+  @Authenticated()
+  @Post('/shared-link')
+  async createAssetsSharedLink(
+    @GetAuthUser() authUser: AuthUserDto,
+    @Body(ValidationPipe) dto: CreateAssetsShareLinkDto,
+  ): Promise<SharedLinkResponseDto> {
+    return await this.assetService.createAssetsSharedLink(authUser, dto);
+  }
+
+  @Authenticated({ isShared: true })
+  @Patch('/shared-link')
+  async updateAssetsInSharedLink(
+    @GetAuthUser() authUser: AuthUserDto,
+    @Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
+  ): Promise<SharedLinkResponseDto> {
+    return await this.assetService.updateAssetsInSharedLink(authUser, dto);
+  }
 }

+ 40 - 1
server/apps/immich/src/api-v1/asset/asset.service.ts

@@ -13,7 +13,7 @@ import { InjectRepository } from '@nestjs/typeorm';
 import { createHash, randomUUID } from 'node:crypto';
 import { QueryFailedError, Repository } from 'typeorm';
 import { AuthUserDto } from '../../decorators/auth-user.decorator';
-import { AssetEntity, AssetType } from '@app/infra';
+import { AssetEntity, AssetType, SharedLinkType } from '@app/infra';
 import { constants, createReadStream, ReadStream, stat } from 'fs';
 import { ServeFileDto } from './dto/serve-file.dto';
 import { Response as Res } from 'express';
@@ -59,6 +59,9 @@ import { StorageService } from '@app/storage';
 import { ShareCore } from '../share/share.core';
 import { ISharedLinkRepository } from '../share/shared-link.repository';
 import { DownloadFilesDto } from './dto/download-files.dto';
+import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
+import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
+import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
 
 const fileInfo = promisify(stat);
 
@@ -699,6 +702,42 @@ export class AssetService {
       throw new ForbiddenException();
     }
   }
+
+  async createAssetsSharedLink(authUser: AuthUserDto, dto: CreateAssetsShareLinkDto): Promise<SharedLinkResponseDto> {
+    const assets = [];
+
+    await this.checkAssetsAccess(authUser, dto.assetIds);
+    for (const assetId of dto.assetIds) {
+      const asset = await this._assetRepository.getById(assetId);
+      assets.push(asset);
+    }
+
+    const sharedLink = await this.shareCore.createSharedLink(authUser.id, {
+      sharedType: SharedLinkType.INDIVIDUAL,
+      expiredAt: dto.expiredAt,
+      allowUpload: dto.allowUpload,
+      assets: assets,
+      description: dto.description,
+    });
+
+    return mapSharedLinkToResponseDto(sharedLink);
+  }
+
+  async updateAssetsInSharedLink(
+    authUser: AuthUserDto,
+    dto: UpdateAssetsToSharedLinkDto,
+  ): Promise<SharedLinkResponseDto> {
+    if (!authUser.sharedLinkId) throw new ForbiddenException();
+    const assets = [];
+
+    for (const assetId of dto.assetIds) {
+      const asset = await this._assetRepository.getById(assetId);
+      assets.push(asset);
+    }
+
+    const updatedLink = await this.shareCore.updateAssetsInSharedLink(authUser.sharedLinkId, assets);
+    return mapSharedLinkToResponseDto(updatedLink);
+  }
 }
 
 async function processETag(path: string, res: Res, headers: Record<string, string>): Promise<boolean> {

+ 6 - 0
server/apps/immich/src/api-v1/asset/dto/add-assets-to-shared-link.dto.ts

@@ -0,0 +1,6 @@
+import { IsNotEmpty } from 'class-validator';
+
+export class UpdateAssetsToSharedLinkDto {
+  @IsNotEmpty()
+  assetIds!: string[];
+}

+ 31 - 0
server/apps/immich/src/api-v1/asset/dto/create-asset-shared-link.dto.ts

@@ -0,0 +1,31 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
+
+export class CreateAssetsShareLinkDto {
+  @IsArray()
+  @IsString({ each: true })
+  @IsNotEmpty({ each: true })
+  @ApiProperty({
+    isArray: true,
+    type: String,
+    title: 'Array asset IDs to be shared',
+    example: [
+      'bf973405-3f2a-48d2-a687-2ed4167164be',
+      'dd41870b-5d00-46d2-924e-1d8489a0aa0f',
+      'fad77c3f-deef-4e7e-9608-14c1aa4e559a',
+    ],
+  })
+  assetIds!: string[];
+
+  @IsString()
+  @IsOptional()
+  expiredAt?: string;
+
+  @IsBoolean()
+  @IsOptional()
+  allowUpload?: boolean;
+
+  @IsString()
+  @IsOptional()
+  description?: string;
+}

+ 115 - 0
server/immich-openapi-specs.json

@@ -1258,6 +1258,78 @@
         ]
       }
     },
+    "/asset/shared-link": {
+      "post": {
+        "operationId": "createAssetsSharedLink",
+        "description": "",
+        "parameters": [],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/CreateAssetsShareLinkDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "201": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SharedLinkResponseDto"
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Asset"
+        ],
+        "security": [
+          {
+            "bearer": []
+          }
+        ]
+      },
+      "patch": {
+        "operationId": "updateAssetsInSharedLink",
+        "description": "",
+        "parameters": [],
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "$ref": "#/components/schemas/SharedLinkResponseDto"
+                }
+              }
+            }
+          }
+        },
+        "tags": [
+          "Asset"
+        ],
+        "security": [
+          {
+            "bearer": []
+          }
+        ]
+      }
+    },
     "/share": {
       "get": {
         "operationId": "getAllSharedLinks",
@@ -3548,6 +3620,35 @@
           "existingIds"
         ]
       },
+      "CreateAssetsShareLinkDto": {
+        "type": "object",
+        "properties": {
+          "assetIds": {
+            "title": "Array asset IDs to be shared",
+            "example": [
+              "bf973405-3f2a-48d2-a687-2ed4167164be",
+              "dd41870b-5d00-46d2-924e-1d8489a0aa0f",
+              "fad77c3f-deef-4e7e-9608-14c1aa4e559a"
+            ],
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          },
+          "expiredAt": {
+            "type": "string"
+          },
+          "allowUpload": {
+            "type": "boolean"
+          },
+          "description": {
+            "type": "string"
+          }
+        },
+        "required": [
+          "assetIds"
+        ]
+      },
       "SharedLinkType": {
         "type": "string",
         "enum": [
@@ -3654,6 +3755,20 @@
           "allowUpload"
         ]
       },
+      "UpdateAssetsToSharedLinkDto": {
+        "type": "object",
+        "properties": {
+          "assetIds": {
+            "type": "array",
+            "items": {
+              "type": "string"
+            }
+          }
+        },
+        "required": [
+          "assetIds"
+        ]
+      },
       "EditSharedLinkDto": {
         "type": "object",
         "properties": {

+ 191 - 1
web/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.40.0
+ * The version of the OpenAPI document: 1.41.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -702,6 +702,37 @@ export interface CreateAlbumShareLinkDto {
      */
     'description'?: string;
 }
+/**
+ * 
+ * @export
+ * @interface CreateAssetsShareLinkDto
+ */
+export interface CreateAssetsShareLinkDto {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof CreateAssetsShareLinkDto
+     */
+    'assetIds': Array<string>;
+    /**
+     * 
+     * @type {string}
+     * @memberof CreateAssetsShareLinkDto
+     */
+    'expiredAt'?: string;
+    /**
+     * 
+     * @type {boolean}
+     * @memberof CreateAssetsShareLinkDto
+     */
+    'allowUpload'?: boolean;
+    /**
+     * 
+     * @type {string}
+     * @memberof CreateAssetsShareLinkDto
+     */
+    'description'?: string;
+}
 /**
  * 
  * @export
@@ -2029,6 +2060,19 @@ export interface UpdateAssetDto {
      */
     'isFavorite'?: boolean;
 }
+/**
+ * 
+ * @export
+ * @interface UpdateAssetsToSharedLinkDto
+ */
+export interface UpdateAssetsToSharedLinkDto {
+    /**
+     * 
+     * @type {Array<string>}
+     * @memberof UpdateAssetsToSharedLinkDto
+     */
+    'assetIds': Array<string>;
+}
 /**
  * 
  * @export
@@ -3599,6 +3643,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        createAssetsSharedLink: async (createAssetsShareLinkDto: CreateAssetsShareLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'createAssetsShareLinkDto' is not null or undefined
+            assertParamExists('createAssetsSharedLink', 'createAssetsShareLinkDto', createAssetsShareLinkDto)
+            const localVarPath = `/asset/shared-link`;
+            // 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: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // 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(createAssetsShareLinkDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {DeleteAssetDto} deleteAssetDto 
@@ -4255,6 +4338,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
                 options: localVarRequestOptions,
             };
         },
+        /**
+         * 
+         * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updateAssetsInSharedLink: async (updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'updateAssetsToSharedLinkDto' is not null or undefined
+            assertParamExists('updateAssetsInSharedLink', 'updateAssetsToSharedLinkDto', updateAssetsToSharedLinkDto)
+            const localVarPath = `/asset/shared-link`;
+            // 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: 'PATCH', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // 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(updateAssetsToSharedLinkDto, localVarRequestOptions, configuration)
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @param {any} assetData 
@@ -4329,6 +4451,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.checkExistingAssets(checkExistingAssetsDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.createAssetsSharedLink(createAssetsShareLinkDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {DeleteAssetDto} deleteAssetDto 
@@ -4501,6 +4633,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
             const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
+        /**
+         * 
+         * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
         /**
          * 
          * @param {any} assetData 
@@ -4539,6 +4681,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         checkExistingAssets(checkExistingAssetsDto: CheckExistingAssetsDto, options?: any): AxiosPromise<CheckExistingAssetsResponseDto> {
             return localVarFp.checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
+            return localVarFp.createAssetsSharedLink(createAssetsShareLinkDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {DeleteAssetDto} deleteAssetDto 
@@ -4694,6 +4845,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
             return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath));
         },
+        /**
+         * 
+         * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
+            return localVarFp.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @param {any} assetData 
@@ -4735,6 +4895,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).checkExistingAssets(checkExistingAssetsDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {CreateAssetsShareLinkDto} createAssetsShareLinkDto 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public createAssetsSharedLink(createAssetsShareLinkDto: CreateAssetsShareLinkDto, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).createAssetsSharedLink(createAssetsShareLinkDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {DeleteAssetDto} deleteAssetDto 
@@ -4924,6 +5095,17 @@ export class AssetApi extends BaseAPI {
         return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath));
     }
 
+    /**
+     * 
+     * @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof AssetApi
+     */
+    public updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
+    }
+
     /**
      * 
      * @param {any} assetData 
@@ -5300,6 +5482,7 @@ export const DeviceInfoApiAxiosParamCreator = function (configuration?: Configur
          * @deprecated
          * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
          * @param {*} [options] Override http request option.
+         * @deprecated
          * @throws {RequiredError}
          */
         createDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@@ -5339,6 +5522,7 @@ export const DeviceInfoApiAxiosParamCreator = function (configuration?: Configur
          * @deprecated
          * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
          * @param {*} [options] Override http request option.
+         * @deprecated
          * @throws {RequiredError}
          */
         updateDeviceInfo: async (upsertDeviceInfoDto: UpsertDeviceInfoDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
@@ -5427,6 +5611,7 @@ export const DeviceInfoApiFp = function(configuration?: Configuration) {
          * @deprecated
          * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
          * @param {*} [options] Override http request option.
+         * @deprecated
          * @throws {RequiredError}
          */
         async createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> {
@@ -5437,6 +5622,7 @@ export const DeviceInfoApiFp = function(configuration?: Configuration) {
          * @deprecated
          * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
          * @param {*} [options] Override http request option.
+         * @deprecated
          * @throws {RequiredError}
          */
         async updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<DeviceInfoResponseDto>> {
@@ -5467,6 +5653,7 @@ export const DeviceInfoApiFactory = function (configuration?: Configuration, bas
          * @deprecated
          * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
          * @param {*} [options] Override http request option.
+         * @deprecated
          * @throws {RequiredError}
          */
         createDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> {
@@ -5476,6 +5663,7 @@ export const DeviceInfoApiFactory = function (configuration?: Configuration, bas
          * @deprecated
          * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
          * @param {*} [options] Override http request option.
+         * @deprecated
          * @throws {RequiredError}
          */
         updateDeviceInfo(upsertDeviceInfoDto: UpsertDeviceInfoDto, options?: any): AxiosPromise<DeviceInfoResponseDto> {
@@ -5504,6 +5692,7 @@ export class DeviceInfoApi extends BaseAPI {
      * @deprecated
      * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
      * @param {*} [options] Override http request option.
+     * @deprecated
      * @throws {RequiredError}
      * @memberof DeviceInfoApi
      */
@@ -5515,6 +5704,7 @@ export class DeviceInfoApi extends BaseAPI {
      * @deprecated
      * @param {UpsertDeviceInfoDto} upsertDeviceInfoDto 
      * @param {*} [options] Override http request option.
+     * @deprecated
      * @throws {RequiredError}
      * @memberof DeviceInfoApi
      */

+ 1 - 1
web/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.40.0
+ * The version of the OpenAPI document: 1.41.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.40.0
+ * The version of the OpenAPI document: 1.41.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.40.0
+ * The version of the OpenAPI document: 1.41.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.40.0
+ * The version of the OpenAPI document: 1.41.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 6 - 118
web/src/lib/components/album-page/album-viewer.svelte

@@ -1,13 +1,11 @@
 <script lang="ts">
 	import { afterNavigate, goto } from '$app/navigation';
-	import { page } from '$app/stores';
 	import {
 		AlbumResponseDto,
 		api,
 		AssetResponseDto,
 		SharedLinkResponseDto,
 		SharedLinkType,
-		ThumbnailFormat,
 		UserResponseDto
 	} from '@api';
 	import { onMount } from 'svelte';
@@ -15,9 +13,7 @@
 	import Plus from 'svelte-material-icons/Plus.svelte';
 	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
 	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
-	import AssetViewer from '../asset-viewer/asset-viewer.svelte';
 	import CircleAvatar from '../shared-components/circle-avatar.svelte';
-	import ImmichThumbnail from '../shared-components/immich-thumbnail.svelte';
 	import AssetSelection from './asset-selection.svelte';
 	import UserSelectionModal from './user-selection-modal.svelte';
 	import ShareInfoModal from './share-info-modal.svelte';
@@ -43,14 +39,13 @@
 	import ThemeButton from '../shared-components/theme-button.svelte';
 	import { openFileUploadDialog } from '$lib/utils/file-uploader';
 	import { bulkDownload } from '$lib/utils/asset-utils';
+	import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
 
 	export let album: AlbumResponseDto;
 	export let sharedLink: SharedLinkResponseDto | undefined = undefined;
 
 	const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
 
-	let isShowAssetViewer = false;
-
 	let isShowAssetSelection = false;
 
 	let isShowShareLinkModal = false;
@@ -72,11 +67,6 @@
 	let isShowAlbumOptions = false;
 	let isShowThumbnailSelection = false;
 
-	let selectedAsset: AssetResponseDto;
-	let currentViewAssetIndex = 0;
-
-	let viewWidth: number;
-	let thumbnailSize = 300;
 	let backUrl = '/albums';
 	let currentAlbumName = '';
 	let currentUser: UserResponseDto;
@@ -97,18 +87,6 @@
 		}
 	});
 
-	$: {
-		if (album.assets?.length < 6) {
-			thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
-		} else {
-			if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
-			else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
-			else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6);
-			else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6);
-			else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6);
-		}
-	}
-
 	const locale = navigator.language;
 	const albumDateFormat: Intl.DateTimeFormatOptions = {
 		month: 'short',
@@ -140,28 +118,6 @@
 		}
 	});
 
-	const viewAssetHandler = (event: CustomEvent) => {
-		const { asset }: { asset: AssetResponseDto } = event.detail;
-
-		currentViewAssetIndex = album.assets.findIndex((a) => a.id == asset.id);
-		selectedAsset = album.assets[currentViewAssetIndex];
-		isShowAssetViewer = true;
-		pushState(selectedAsset.id);
-	};
-
-	const selectAssetHandler = (event: CustomEvent) => {
-		const { asset }: { asset: AssetResponseDto } = event.detail;
-		let temp = new Set(multiSelectAsset);
-
-		if (multiSelectAsset.has(asset)) {
-			temp.delete(asset);
-		} else {
-			temp.add(asset);
-		}
-
-		multiSelectAsset = temp;
-	};
-
 	const clearMultiSelectAssetAssetHandler = () => {
 		multiSelectAsset = new Set();
 	};
@@ -184,40 +140,6 @@
 			}
 		}
 	};
-	const navigateAssetForward = () => {
-		try {
-			if (currentViewAssetIndex < album.assetCount - 1) {
-				currentViewAssetIndex++;
-				selectedAsset = album.assets[currentViewAssetIndex];
-				pushState(selectedAsset.id);
-			}
-		} catch (e) {
-			console.error(e);
-		}
-	};
-
-	const navigateAssetBackward = () => {
-		try {
-			if (currentViewAssetIndex > 0) {
-				currentViewAssetIndex--;
-				selectedAsset = album.assets[currentViewAssetIndex];
-				pushState(selectedAsset.id);
-			}
-		} catch (e) {
-			console.error(e);
-		}
-	};
-
-	const pushState = (assetId: string) => {
-		// add a URL to the browser's history
-		// changes the current URL in the address bar but doesn't perform any SvelteKit navigation
-		history.pushState(null, '', `${$page.url.pathname}/photos/${assetId}`);
-	};
-
-	const closeViewer = () => {
-		isShowAssetViewer = false;
-		history.pushState(null, '', `${$page.url.pathname}`);
-	};
 
 	// Update Album Name
 	$: {
@@ -606,34 +528,11 @@
 		{/if}
 
 		{#if album.assetCount > 0}
-			<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
-				{#each album.assets as asset}
-					{#key asset.id}
-						{#if album.assetCount < 7}
-							<ImmichThumbnail
-								{asset}
-								{thumbnailSize}
-								publicSharedKey={sharedLink?.key}
-								format={ThumbnailFormat.Jpeg}
-								on:click={(e) =>
-									isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
-								on:select={selectAssetHandler}
-								selected={multiSelectAsset.has(asset)}
-							/>
-						{:else}
-							<ImmichThumbnail
-								{asset}
-								{thumbnailSize}
-								publicSharedKey={sharedLink?.key}
-								on:click={(e) =>
-									isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
-								on:select={selectAssetHandler}
-								selected={multiSelectAsset.has(asset)}
-							/>
-						{/if}
-					{/key}
-				{/each}
-			</div>
+			<GalleryViewer
+				assets={album.assets}
+				key={sharedLink?.key ?? ''}
+				bind:selectedAssets={multiSelectAsset}
+			/>
 		{:else}
 			<!-- Album is empty - Show asset selectection buttons -->
 			<section id="empty-album" class=" mt-[200px] flex place-content-center place-items-center">
@@ -654,17 +553,6 @@
 	</section>
 </section>
 
-<!-- Overlay Asset Viewer -->
-{#if isShowAssetViewer}
-	<AssetViewer
-		asset={selectedAsset}
-		publicSharedKey={sharedLink?.key}
-		on:navigate-previous={navigateAssetBackward}
-		on:navigate-next={navigateAssetForward}
-		on:close={closeViewer}
-	/>
-{/if}
-
 {#if isShowAssetSelection}
 	<AssetSelection
 		albumId={album.id}

+ 1 - 1
web/src/lib/components/asset-viewer/asset-viewer.svelte

@@ -233,7 +233,7 @@
 
 <section
 	id="immich-asset-viewer"
-	class="fixed h-screen w-screen top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4"
+	class="fixed h-screen w-screen left-0 top-0 overflow-y-hidden bg-black z-[999] grid grid-rows-[64px_1fr] grid-cols-4"
 >
 	<div class="col-start-1 col-span-4 row-start-1 row-span-1 z-[1000] transition-transform">
 		<AssetViewerNavBar

+ 150 - 0
web/src/lib/components/share-page/individual-shared-viewer.svelte

@@ -0,0 +1,150 @@
+<script lang="ts">
+	import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
+
+	import { api, AssetResponseDto, SharedLinkResponseDto } from '@api';
+	import ControlAppBar from '../shared-components/control-app-bar.svelte';
+	import { goto } from '$app/navigation';
+	import CircleIconButton from '../shared-components/circle-icon-button.svelte';
+	import FileImagePlusOutline from 'svelte-material-icons/FileImagePlusOutline.svelte';
+	import FolderDownloadOutline from 'svelte-material-icons/FolderDownloadOutline.svelte';
+	import { openFileUploadDialog } from '$lib/utils/file-uploader';
+	import { bulkDownload } from '$lib/utils/asset-utils';
+	import Close from 'svelte-material-icons/Close.svelte';
+	import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
+	import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
+	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
+	import {
+		notificationController,
+		NotificationType
+	} from '../shared-components/notification/notification';
+
+	export let sharedLink: SharedLinkResponseDto;
+	export let isOwned: boolean;
+
+	let assets = sharedLink.assets;
+	let selectedAssets: Set<AssetResponseDto> = new Set();
+
+	$: isMultiSelectionMode = selectedAssets.size > 0;
+
+	const clearMultiSelectAssetAssetHandler = () => {
+		selectedAssets = new Set();
+	};
+
+	const downloadAssets = async (isAll: boolean) => {
+		await bulkDownload(
+			'immich-shared',
+			isAll ? assets : Array.from(selectedAssets),
+			() => {
+				isMultiSelectionMode = false;
+				clearMultiSelectAssetAssetHandler();
+			},
+			sharedLink?.key
+		);
+	};
+
+	const handleUploadAssets = () => {
+		openFileUploadDialog(undefined, sharedLink?.key, async (assetId) => {
+			await api.assetApi.updateAssetsInSharedLink(
+				{
+					assetIds: [...assets.map((a) => a.id), assetId]
+				},
+				{
+					params: {
+						key: sharedLink?.key
+					}
+				}
+			);
+
+			notificationController.show({
+				message: 'Add asset to shared link successfully',
+				type: NotificationType.Info
+			});
+		});
+	};
+
+	const handleRemoveAssetsFromSharedLink = async () => {
+		if (window.confirm('Do you want to remove selected assets from the shared link?')) {
+			await api.assetApi.updateAssetsInSharedLink(
+				{
+					assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id)
+				},
+				{
+					params: {
+						key: sharedLink?.key
+					}
+				}
+			);
+
+			assets = assets.filter((a) => !selectedAssets.has(a));
+			clearMultiSelectAssetAssetHandler();
+		}
+	};
+</script>
+
+<section class="bg-immich-bg dark:bg-immich-dark-bg">
+	{#if isMultiSelectionMode}
+		<ControlAppBar
+			on:close-button-click={clearMultiSelectAssetAssetHandler}
+			backIcon={Close}
+			tailwindClasses={'bg-white shadow-md'}
+		>
+			<svelte:fragment slot="leading">
+				<p class="font-medium text-immich-primary dark:text-immich-dark-primary">
+					Selected {selectedAssets.size}
+				</p>
+			</svelte:fragment>
+			<svelte:fragment slot="trailing">
+				<CircleIconButton
+					title="Download"
+					on:click={() => downloadAssets(false)}
+					logo={CloudDownloadOutline}
+				/>
+				{#if isOwned}
+					<CircleIconButton
+						title="Remove from album"
+						on:click={handleRemoveAssetsFromSharedLink}
+						logo={DeleteOutline}
+					/>
+				{/if}
+			</svelte:fragment>
+		</ControlAppBar>
+	{:else}
+		<ControlAppBar
+			on:close-button-click={() => goto('/photos')}
+			backIcon={ArrowLeft}
+			showBackButton={false}
+		>
+			<svelte:fragment slot="leading">
+				<a
+					data-sveltekit-preload-data="hover"
+					class="flex gap-2 place-items-center hover:cursor-pointer ml-6"
+					href="https://immich.app"
+				>
+					<img src="/immich-logo.svg" alt="immich logo" height="30" width="30" />
+					<h1 class="font-immich-title text-lg text-immich-primary dark:text-immich-dark-primary">
+						IMMICH
+					</h1>
+				</a>
+			</svelte:fragment>
+
+			<svelte:fragment slot="trailing">
+				{#if sharedLink?.allowUpload}
+					<CircleIconButton
+						title="Add Photos"
+						on:click={handleUploadAssets}
+						logo={FileImagePlusOutline}
+					/>
+				{/if}
+
+				<CircleIconButton
+					title="Download"
+					on:click={() => downloadAssets(true)}
+					logo={FolderDownloadOutline}
+				/>
+			</svelte:fragment>
+		</ControlAppBar>
+	{/if}
+	<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40">
+		<GalleryViewer {assets} key={sharedLink.key} bind:selectedAssets />
+	</section>
+</section>

+ 48 - 25
web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte

@@ -2,7 +2,13 @@
 	import { createEventDispatcher, onMount } from 'svelte';
 	import BaseModal from '../base-modal.svelte';
 	import Link from 'svelte-material-icons/Link.svelte';
-	import { AlbumResponseDto, api, SharedLinkResponseDto, SharedLinkType } from '@api';
+	import {
+		AlbumResponseDto,
+		api,
+		AssetResponseDto,
+		SharedLinkResponseDto,
+		SharedLinkType
+	} from '@api';
 	import { notificationController, NotificationType } from '../notification/notification';
 	import { ImmichDropDownOption } from '../dropdown-button.svelte';
 	import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
@@ -10,9 +16,11 @@
 	import SettingInputField, {
 		SettingInputFieldType
 	} from '$lib/components/admin-page/settings/setting-input-field.svelte';
+	import { handleError } from '$lib/utils/handle-error';
 
 	export let shareType: SharedLinkType;
-	export let album: AlbumResponseDto | undefined;
+	export let sharedAssets: AssetResponseDto[] = [];
+	export let album: AlbumResponseDto | undefined = undefined;
 	export let editingLink: SharedLinkResponseDto | undefined = undefined;
 
 	let isShowSharedLink = false;
@@ -37,32 +45,36 @@
 		}
 	});
 
-	const createAlbumSharedLink = async () => {
-		if (album) {
-			try {
-				const expirationTime = getExpirationTimeInMillisecond();
-				const currentTime = new Date().getTime();
-				const expirationDate = expirationTime
-					? new Date(currentTime + expirationTime).toISOString()
-					: undefined;
+	const handleCreateSharedLink = async () => {
+		const expirationTime = getExpirationTimeInMillisecond();
+		const currentTime = new Date().getTime();
+		const expirationDate = expirationTime
+			? new Date(currentTime + expirationTime).toISOString()
+			: undefined;
 
+		try {
+			if (shareType === SharedLinkType.Album && album) {
 				const { data } = await api.albumApi.createAlbumSharedLink({
 					albumId: album.id,
 					expiredAt: expirationDate,
 					allowUpload: isAllowUpload,
 					description: description
 				});
-
 				buildSharedLink(data);
-				isShowSharedLink = true;
-			} catch (e) {
-				console.error('[createAlbumSharedLink] Error: ', e);
-				notificationController.show({
-					type: NotificationType.Error,
-					message: 'Failed to create shared link'
+			} else {
+				const { data } = await api.assetApi.createAssetsSharedLink({
+					assetIds: sharedAssets.map((a) => a.id),
+					expiredAt: expirationDate,
+					allowUpload: isAllowUpload,
+					description: description
 				});
+				buildSharedLink(data);
 			}
+		} catch (e) {
+			handleError(e, 'Failed to create shared link');
 		}
+
+		isShowSharedLink = true;
 	};
 
 	const buildSharedLink = (createdLink: SharedLinkResponseDto) => {
@@ -76,8 +88,11 @@
 				message: 'Copied to clipboard!',
 				type: NotificationType.Info
 			});
-		} catch (error) {
-			console.error('Error', error);
+		} catch (e) {
+			handleError(
+				e,
+				'Cannot copy to clipboard, make sure you are accessing the page through https'
+			);
 		}
 	};
 
@@ -127,11 +142,7 @@
 
 				dispatch('close');
 			} catch (e) {
-				console.error('[handleEditLink]', e);
-				notificationController.show({
-					type: NotificationType.Error,
-					message: 'Failed to edit shared link'
-				});
+				handleError(e, 'Failed to edit shared link');
 			}
 		}
 	};
@@ -162,6 +173,18 @@
 			{/if}
 		{/if}
 
+		{#if shareType == SharedLinkType.Individual}
+			{#if !editingLink}
+				<div>Let anyone with the link see the selected photo(s)</div>
+			{:else}
+				<div class="text-sm">
+					Individual shared | <span class="text-immich-primary dark:text-immich-dark-primary"
+						>{editingLink.description}</span
+					>
+				</div>
+			{/if}
+		{/if}
+
 		<div class="mt-6 mb-2">
 			<p class="text-xs">LINK OPTIONS</p>
 		</div>
@@ -215,7 +238,7 @@
 			{:else}
 				<div class="flex justify-end">
 					<button
-						on:click={createAlbumSharedLink}
+						on:click={handleCreateSharedLink}
 						class="text-white dark:text-black bg-immich-primary px-4 py-2 rounded-lg text-sm transition-colors hover:bg-immich-primary/75 dark:bg-immich-dark-primary dark:hover:bg-immich-dark-primary/75"
 					>
 						Create Link

+ 118 - 0
web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte

@@ -0,0 +1,118 @@
+<script lang="ts">
+	import { page } from '$app/stores';
+	import { handleError } from '$lib/utils/handle-error';
+	import { AssetResponseDto, ThumbnailFormat } from '@api';
+
+	import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
+	import ImmichThumbnail from '../../shared-components/immich-thumbnail.svelte';
+
+	export let assets: AssetResponseDto[];
+	export let key: string;
+	export let selectedAssets: Set<AssetResponseDto> = new Set();
+
+	let isShowAssetViewer = false;
+
+	let selectedAsset: AssetResponseDto;
+	let currentViewAssetIndex = 0;
+
+	let viewWidth: number;
+	let thumbnailSize = 300;
+
+	$: isMultiSelectionMode = selectedAssets.size > 0;
+
+	$: {
+		if (assets.length < 6) {
+			thumbnailSize = Math.floor(viewWidth / assets.length - assets.length);
+		} else {
+			if (viewWidth > 600) thumbnailSize = Math.floor(viewWidth / 6 - 6);
+			else if (viewWidth > 400) thumbnailSize = Math.floor(viewWidth / 4 - 6);
+			else if (viewWidth > 300) thumbnailSize = Math.floor(viewWidth / 2 - 6);
+			else if (viewWidth > 200) thumbnailSize = Math.floor(viewWidth / 2 - 6);
+			else if (viewWidth > 100) thumbnailSize = Math.floor(viewWidth / 1 - 6);
+		}
+	}
+
+	const viewAssetHandler = (event: CustomEvent) => {
+		const { asset }: { asset: AssetResponseDto } = event.detail;
+
+		currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
+		selectedAsset = assets[currentViewAssetIndex];
+		isShowAssetViewer = true;
+		pushState(selectedAsset.id);
+	};
+
+	const selectAssetHandler = (event: CustomEvent) => {
+		const { asset }: { asset: AssetResponseDto } = event.detail;
+		let temp = new Set(selectedAssets);
+
+		if (selectedAssets.has(asset)) {
+			temp.delete(asset);
+		} else {
+			temp.add(asset);
+		}
+
+		selectedAssets = temp;
+	};
+
+	const navigateAssetForward = () => {
+		try {
+			if (currentViewAssetIndex < assets.length - 1) {
+				currentViewAssetIndex++;
+				selectedAsset = assets[currentViewAssetIndex];
+				pushState(selectedAsset.id);
+			}
+		} catch (e) {
+			handleError(e, 'Cannot navigate to the next asset');
+		}
+	};
+
+	const navigateAssetBackward = () => {
+		try {
+			if (currentViewAssetIndex > 0) {
+				currentViewAssetIndex--;
+				selectedAsset = assets[currentViewAssetIndex];
+				pushState(selectedAsset.id);
+			}
+		} catch (e) {
+			handleError(e, 'Cannot navigate to previous asset');
+		}
+	};
+
+	const pushState = (assetId: string) => {
+		// add a URL to the browser's history
+		// changes the current URL in the address bar but doesn't perform any SvelteKit navigation
+		history.pushState(null, '', `${$page.url.pathname}/photos/${assetId}`);
+	};
+
+	const closeViewer = () => {
+		isShowAssetViewer = false;
+		history.pushState(null, '', `${$page.url.pathname}`);
+	};
+</script>
+
+{#if assets.length > 0}
+	<div class="flex flex-wrap gap-1 w-full pb-20" bind:clientWidth={viewWidth}>
+		{#each assets as asset (asset.id)}
+			<ImmichThumbnail
+				{asset}
+				{thumbnailSize}
+				publicSharedKey={key}
+				format={assets.length < 7 ? ThumbnailFormat.Jpeg : ThumbnailFormat.Webp}
+				on:click={(e) => (isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e))}
+				on:select={selectAssetHandler}
+				selected={selectedAssets.has(asset)}
+			/>
+		{/each}
+	</div>
+{/if}
+
+<!-- Overlay Asset Viewer -->
+{#if isShowAssetViewer}
+	<AssetViewer
+		asset={selectedAsset}
+		publicSharedKey={key}
+		on:navigate-previous={navigateAssetBackward}
+		on:navigate-next={navigateAssetForward}
+		on:close={closeViewer}
+	/>
+{/if}

+ 1 - 1
web/src/lib/components/shared-components/notification/notification-card.svelte

@@ -90,7 +90,7 @@
 		</button>
 	</div>
 
-	<p class="whitespace-pre text-sm pl-[28px] pr-[16px]" data-testid="message">
+	<p class="whitespace-pre-wrap text-sm pl-[28px] pr-[16px]" data-testid="message">
 		{@html notificationInfo.message}
 	</p>
 </div>

+ 11 - 9
web/src/lib/utils/file-uploader.ts

@@ -12,7 +12,7 @@ import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 export const openFileUploadDialog = (
 	albumId: string | undefined = undefined,
 	sharedKey: string | undefined = undefined,
-	callback?: () => void
+	onDone?: (id: string) => void
 ) => {
 	try {
 		const fileSelector = document.createElement('input');
@@ -28,8 +28,7 @@ export const openFileUploadDialog = (
 			}
 			const files = Array.from<File>(target.files);
 
-			await fileUploadHandler(files, albumId, sharedKey);
-			callback && callback();
+			await fileUploadHandler(files, albumId, sharedKey, onDone);
 		};
 
 		fileSelector.click();
@@ -41,7 +40,8 @@ export const openFileUploadDialog = (
 export const fileUploadHandler = async (
 	files: File[],
 	albumId: string | undefined = undefined,
-	sharedKey: string | undefined = undefined
+	sharedKey: string | undefined = undefined,
+	onDone?: (id: string) => void
 ) => {
 	if (files.length > 50) {
 		notificationController.show({
@@ -54,13 +54,13 @@ export const fileUploadHandler = async (
 
 		return;
 	}
-	console.log('fileUploadHandler');
+
 	const acceptedFile = files.filter(
 		(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image'
 	);
 
 	for (const asset of acceptedFile) {
-		await fileUploader(asset, albumId, sharedKey);
+		await fileUploader(asset, albumId, sharedKey, onDone);
 	}
 };
 
@@ -68,7 +68,8 @@ export const fileUploadHandler = async (
 async function fileUploader(
 	asset: File,
 	albumId: string | undefined = undefined,
-	sharedKey: string | undefined = undefined
+	sharedKey: string | undefined = undefined,
+	onDone?: (id: string) => void
 ) {
 	const assetType = asset.type.split('/')[0].toUpperCase();
 	const temp = asset.name.split('.');
@@ -135,6 +136,7 @@ async function fileUploader(
 				if (albumId && dataId) {
 					addAssetsToAlbum(albumId, [dataId]);
 				}
+				onDone && dataId && onDone(dataId);
 				return;
 			}
 		}
@@ -154,10 +156,9 @@ async function fileUploader(
 		request.upload.onload = () => {
 			setTimeout(() => {
 				uploadAssetsStore.removeUploadAsset(deviceAssetId);
-
+				const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
 				if (albumId) {
 					try {
-						const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
 						if (res.id) {
 							addAssetsToAlbum(albumId, [res.id], sharedKey);
 						}
@@ -165,6 +166,7 @@ async function fileUploader(
 						console.error('ERROR parsing data JSON in upload onload');
 					}
 				}
+				onDone && onDone(res.id);
 			}, 1000);
 		};
 

+ 26 - 4
web/src/routes/photos/+page.svelte

@@ -6,9 +6,8 @@
 	import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
 	import AlbumSelectionModal from '$lib/components/shared-components/album-selection-modal.svelte';
 	import { goto } from '$app/navigation';
-
 	import type { PageData } from './$types';
-
+	import ShareVariantOutline from 'svelte-material-icons/ShareVariantOutline.svelte';
 	import { openFileUploadDialog } from '$lib/utils/file-uploader';
 	import {
 		assetInteractionStore,
@@ -21,16 +20,17 @@
 	import CircleIconButton from '$lib/components/shared-components/circle-icon-button.svelte';
 	import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
 	import Plus from 'svelte-material-icons/Plus.svelte';
-	import { AlbumResponseDto, api } from '@api';
+	import { AlbumResponseDto, api, SharedLinkType } from '@api';
 	import {
 		notificationController,
 		NotificationType
 	} from '$lib/components/shared-components/notification/notification';
 	import { assetStore } from '$lib/stores/assets.store';
 	import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
+	import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte';
 
 	export let data: PageData;
-
+	let isShowCreateSharedLinkModal = false;
 	const deleteSelectedAssetHandler = async () => {
 		try {
 			if (
@@ -114,6 +114,15 @@
 			assetInteractionStore.clearMultiselect();
 		});
 	};
+
+	const handleCreateSharedLink = async () => {
+		isShowCreateSharedLinkModal = true;
+	};
+
+	const handleCloseSharedLinkModal = () => {
+		assetInteractionStore.clearMultiselect();
+		isShowCreateSharedLinkModal = false;
+	};
 </script>
 
 <section>
@@ -129,6 +138,11 @@
 				</p>
 			</svelte:fragment>
 			<svelte:fragment slot="trailing">
+				<CircleIconButton
+					title="Share"
+					logo={ShareVariantOutline}
+					on:click={handleCreateSharedLink}
+				/>
 				<CircleIconButton
 					title="Download"
 					logo={CloudDownloadOutline}
@@ -164,6 +178,14 @@
 			on:close={() => (isShowAlbumPicker = false)}
 		/>
 	{/if}
+
+	{#if isShowCreateSharedLinkModal}
+		<CreateSharedLinkModal
+			sharedAssets={Array.from($selectedAssets)}
+			shareType={SharedLinkType.Individual}
+			on:close={handleCloseSharedLinkModal}
+		/>
+	{/if}
 </section>
 
 <section

+ 5 - 2
web/src/routes/share/[key]/+page.server.ts

@@ -5,7 +5,9 @@ import { getThumbnailUrl } from '$lib/utils/asset-utils';
 import { serverApi, ThumbnailFormat } from '@api';
 import type { PageServerLoad } from './$types';
 
-export const load: PageServerLoad = async ({ params }) => {
+export const load: PageServerLoad = async ({ params, parent }) => {
+	const { user } = await parent();
+
 	const { key } = params;
 
 	try {
@@ -22,7 +24,8 @@ export const load: PageServerLoad = async ({ params }) => {
 				imageUrl: assetId
 					? getThumbnailUrl(assetId, ThumbnailFormat.Webp, sharedLink.key)
 					: 'feature-panel.png'
-			}
+			},
+			user
 		};
 	} catch (e) {
 		throw error(404, {

+ 10 - 2
web/src/routes/share/[key]/+page.svelte

@@ -1,6 +1,7 @@
 <script lang="ts">
 	import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
-	import { AlbumResponseDto } from '@api';
+	import IndividualSharedViewer from '$lib/components/share-page/individual-shared-viewer.svelte';
+	import { AlbumResponseDto, SharedLinkType } from '@api';
 	import type { PageData } from './$types';
 
 	export let data: PageData;
@@ -8,13 +9,20 @@
 	const { sharedLink } = data;
 
 	let album: AlbumResponseDto | null = null;
+	let isOwned = data.user ? data.user.id === sharedLink.userId : false;
 	if (sharedLink.album) {
 		album = { ...sharedLink.album, assets: sharedLink.assets };
 	}
 </script>
 
-{#if album}
+{#if sharedLink.type == SharedLinkType.Album && album}
 	<div class="immich-scrollbar">
 		<AlbumViewer {album} {sharedLink} />
 	</div>
 {/if}
+
+{#if sharedLink.type == SharedLinkType.Individual}
+	<div class="immich-scrollbar">
+		<IndividualSharedViewer {sharedLink} {isOwned} />
+	</div>
+{/if}