mirror of
https://github.com/immich-app/immich.git
synced 2025-04-10 10:06:25 +02:00
feat(web/server) public album sharing (#1266)
This commit is contained in:
parent
fd15cdbf40
commit
10789503c1
101 changed files with 4879 additions and 347 deletions
mobile/openapi
.openapi-generator
README.mddoc
AlbumApi.mdAssetApi.mdCreateAlbumShareLinkDto.mdDownloadFilesDto.mdEditSharedLinkDto.mdShareApi.mdSharedLinkResponseDto.mdSharedLinkType.md
lib
test
server
apps/immich/src
api-v1
album
album-repository.tsalbum.controller.tsalbum.module.tsalbum.service.spec.tsalbum.service.ts
dto
response-dto
asset
share
config
decorators
middlewares
modules/immich-jwt
libs/database/src
entities
migrations
web/src
api
app.htmllib
components
admin-page/settings
album-page
asset-viewer
shared-components
control-app-bar.svelte
create-share-link-modal
dropdown-button.svelteimmich-thumbnail.sveltetheme-button.sveltesharedlinks-page
utils
routes
photos
share/[key]
sharing
18
mobile/openapi/.openapi-generator/FILES
generated
18
mobile/openapi/.openapi-generator/FILES
generated
|
@ -30,6 +30,7 @@ doc/CheckDuplicateAssetResponseDto.md
|
|||
doc/CheckExistingAssetsDto.md
|
||||
doc/CheckExistingAssetsResponseDto.md
|
||||
doc/CreateAlbumDto.md
|
||||
doc/CreateAlbumShareLinkDto.md
|
||||
doc/CreateProfileImageResponseDto.md
|
||||
doc/CreateTagDto.md
|
||||
doc/CreateUserDto.md
|
||||
|
@ -41,6 +42,8 @@ doc/DeleteAssetStatus.md
|
|||
doc/DeviceInfoApi.md
|
||||
doc/DeviceInfoResponseDto.md
|
||||
doc/DeviceTypeEnum.md
|
||||
doc/DownloadFilesDto.md
|
||||
doc/EditSharedLinkDto.md
|
||||
doc/ExifResponseDto.md
|
||||
doc/GetAssetByTimeBucketDto.md
|
||||
doc/GetAssetCountByTimeBucketDto.md
|
||||
|
@ -64,6 +67,9 @@ doc/ServerInfoResponseDto.md
|
|||
doc/ServerPingResponse.md
|
||||
doc/ServerStatsResponseDto.md
|
||||
doc/ServerVersionReponseDto.md
|
||||
doc/ShareApi.md
|
||||
doc/SharedLinkResponseDto.md
|
||||
doc/SharedLinkType.md
|
||||
doc/SignUpDto.md
|
||||
doc/SmartInfoResponseDto.md
|
||||
doc/SystemConfigApi.md
|
||||
|
@ -97,6 +103,7 @@ lib/api/device_info_api.dart
|
|||
lib/api/job_api.dart
|
||||
lib/api/o_auth_api.dart
|
||||
lib/api/server_info_api.dart
|
||||
lib/api/share_api.dart
|
||||
lib/api/system_config_api.dart
|
||||
lib/api/tag_api.dart
|
||||
lib/api/user_api.dart
|
||||
|
@ -131,6 +138,7 @@ lib/model/check_duplicate_asset_response_dto.dart
|
|||
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_profile_image_response_dto.dart
|
||||
lib/model/create_tag_dto.dart
|
||||
lib/model/create_user_dto.dart
|
||||
|
@ -141,6 +149,8 @@ lib/model/delete_asset_response_dto.dart
|
|||
lib/model/delete_asset_status.dart
|
||||
lib/model/device_info_response_dto.dart
|
||||
lib/model/device_type_enum.dart
|
||||
lib/model/download_files_dto.dart
|
||||
lib/model/edit_shared_link_dto.dart
|
||||
lib/model/exif_response_dto.dart
|
||||
lib/model/get_asset_by_time_bucket_dto.dart
|
||||
lib/model/get_asset_count_by_time_bucket_dto.dart
|
||||
|
@ -161,6 +171,8 @@ lib/model/server_info_response_dto.dart
|
|||
lib/model/server_ping_response.dart
|
||||
lib/model/server_stats_response_dto.dart
|
||||
lib/model/server_version_reponse_dto.dart
|
||||
lib/model/shared_link_response_dto.dart
|
||||
lib/model/shared_link_type.dart
|
||||
lib/model/sign_up_dto.dart
|
||||
lib/model/smart_info_response_dto.dart
|
||||
lib/model/system_config_dto.dart
|
||||
|
@ -209,6 +221,7 @@ test/check_duplicate_asset_response_dto_test.dart
|
|||
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_profile_image_response_dto_test.dart
|
||||
test/create_tag_dto_test.dart
|
||||
test/create_user_dto_test.dart
|
||||
|
@ -220,6 +233,8 @@ test/delete_asset_status_test.dart
|
|||
test/device_info_api_test.dart
|
||||
test/device_info_response_dto_test.dart
|
||||
test/device_type_enum_test.dart
|
||||
test/download_files_dto_test.dart
|
||||
test/edit_shared_link_dto_test.dart
|
||||
test/exif_response_dto_test.dart
|
||||
test/get_asset_by_time_bucket_dto_test.dart
|
||||
test/get_asset_count_by_time_bucket_dto_test.dart
|
||||
|
@ -243,6 +258,9 @@ test/server_info_response_dto_test.dart
|
|||
test/server_ping_response_test.dart
|
||||
test/server_stats_response_dto_test.dart
|
||||
test/server_version_reponse_dto_test.dart
|
||||
test/share_api_test.dart
|
||||
test/shared_link_response_dto_test.dart
|
||||
test/shared_link_type_test.dart
|
||||
test/sign_up_dto_test.dart
|
||||
test/smart_info_response_dto_test.dart
|
||||
test/system_config_api_test.dart
|
||||
|
|
12
mobile/openapi/README.md
generated
12
mobile/openapi/README.md
generated
|
@ -66,6 +66,7 @@ Class | Method | HTTP request | Description
|
|||
*AlbumApi* | [**addAssetsToAlbum**](doc//AlbumApi.md#addassetstoalbum) | **PUT** /album/{albumId}/assets |
|
||||
*AlbumApi* | [**addUsersToAlbum**](doc//AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
|
||||
*AlbumApi* | [**createAlbum**](doc//AlbumApi.md#createalbum) | **POST** /album |
|
||||
*AlbumApi* | [**createAlbumSharedLink**](doc//AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
|
||||
*AlbumApi* | [**deleteAlbum**](doc//AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
|
||||
*AlbumApi* | [**downloadArchive**](doc//AlbumApi.md#downloadarchive) | **GET** /album/{albumId}/download |
|
||||
*AlbumApi* | [**getAlbumCountByUserId**](doc//AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
|
||||
|
@ -78,6 +79,7 @@ Class | Method | HTTP request | Description
|
|||
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
*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 |
|
||||
*AssetApi* | [**downloadLibrary**](doc//AssetApi.md#downloadlibrary) | **GET** /asset/download-library |
|
||||
*AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset |
|
||||
*AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
||||
|
@ -113,6 +115,11 @@ Class | Method | HTTP request | Description
|
|||
*ServerInfoApi* | [**getServerVersion**](doc//ServerInfoApi.md#getserverversion) | **GET** /server-info/version |
|
||||
*ServerInfoApi* | [**getStats**](doc//ServerInfoApi.md#getstats) | **GET** /server-info/stats |
|
||||
*ServerInfoApi* | [**pingServer**](doc//ServerInfoApi.md#pingserver) | **GET** /server-info/ping |
|
||||
*ShareApi* | [**editSharedLink**](doc//ShareApi.md#editsharedlink) | **PATCH** /share/{id} |
|
||||
*ShareApi* | [**getAllSharedLinks**](doc//ShareApi.md#getallsharedlinks) | **GET** /share |
|
||||
*ShareApi* | [**getMySharedLink**](doc//ShareApi.md#getmysharedlink) | **GET** /share/me |
|
||||
*ShareApi* | [**getSharedLinkById**](doc//ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} |
|
||||
*ShareApi* | [**removeSharedLink**](doc//ShareApi.md#removesharedlink) | **DELETE** /share/{id} |
|
||||
*SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config |
|
||||
*SystemConfigApi* | [**getDefaults**](doc//SystemConfigApi.md#getdefaults) | **GET** /system-config/defaults |
|
||||
*SystemConfigApi* | [**getStorageTemplateOptions**](doc//SystemConfigApi.md#getstoragetemplateoptions) | **GET** /system-config/storage-template-options |
|
||||
|
@ -159,6 +166,7 @@ Class | Method | HTTP request | Description
|
|||
- [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md)
|
||||
- [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md)
|
||||
- [CreateAlbumDto](doc//CreateAlbumDto.md)
|
||||
- [CreateAlbumShareLinkDto](doc//CreateAlbumShareLinkDto.md)
|
||||
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
|
||||
- [CreateTagDto](doc//CreateTagDto.md)
|
||||
- [CreateUserDto](doc//CreateUserDto.md)
|
||||
|
@ -169,6 +177,8 @@ Class | Method | HTTP request | Description
|
|||
- [DeleteAssetStatus](doc//DeleteAssetStatus.md)
|
||||
- [DeviceInfoResponseDto](doc//DeviceInfoResponseDto.md)
|
||||
- [DeviceTypeEnum](doc//DeviceTypeEnum.md)
|
||||
- [DownloadFilesDto](doc//DownloadFilesDto.md)
|
||||
- [EditSharedLinkDto](doc//EditSharedLinkDto.md)
|
||||
- [ExifResponseDto](doc//ExifResponseDto.md)
|
||||
- [GetAssetByTimeBucketDto](doc//GetAssetByTimeBucketDto.md)
|
||||
- [GetAssetCountByTimeBucketDto](doc//GetAssetCountByTimeBucketDto.md)
|
||||
|
@ -189,6 +199,8 @@ Class | Method | HTTP request | Description
|
|||
- [ServerPingResponse](doc//ServerPingResponse.md)
|
||||
- [ServerStatsResponseDto](doc//ServerStatsResponseDto.md)
|
||||
- [ServerVersionReponseDto](doc//ServerVersionReponseDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
- [SharedLinkType](doc//SharedLinkType.md)
|
||||
- [SignUpDto](doc//SignUpDto.md)
|
||||
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
|
||||
- [SystemConfigDto](doc//SystemConfigDto.md)
|
||||
|
|
48
mobile/openapi/doc/AlbumApi.md
generated
48
mobile/openapi/doc/AlbumApi.md
generated
|
@ -12,6 +12,7 @@ Method | HTTP request | Description
|
|||
[**addAssetsToAlbum**](AlbumApi.md#addassetstoalbum) | **PUT** /album/{albumId}/assets |
|
||||
[**addUsersToAlbum**](AlbumApi.md#adduserstoalbum) | **PUT** /album/{albumId}/users |
|
||||
[**createAlbum**](AlbumApi.md#createalbum) | **POST** /album |
|
||||
[**createAlbumSharedLink**](AlbumApi.md#createalbumsharedlink) | **POST** /album/create-shared-link |
|
||||
[**deleteAlbum**](AlbumApi.md#deletealbum) | **DELETE** /album/{albumId} |
|
||||
[**downloadArchive**](AlbumApi.md#downloadarchive) | **GET** /album/{albumId}/download |
|
||||
[**getAlbumCountByUserId**](AlbumApi.md#getalbumcountbyuserid) | **GET** /album/count-by-user-id |
|
||||
|
@ -167,6 +168,53 @@ 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)
|
||||
|
||||
# **createAlbumSharedLink**
|
||||
> SharedLinkResponseDto createAlbumSharedLink(createAlbumShareLinkDto)
|
||||
|
||||
|
||||
|
||||
### 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 = AlbumApi();
|
||||
final createAlbumShareLinkDto = CreateAlbumShareLinkDto(); // CreateAlbumShareLinkDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.createAlbumSharedLink(createAlbumShareLinkDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AlbumApi->createAlbumSharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**createAlbumShareLinkDto** | [**CreateAlbumShareLinkDto**](CreateAlbumShareLinkDto.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)
|
||||
|
||||
# **deleteAlbum**
|
||||
> deleteAlbum(albumId)
|
||||
|
||||
|
|
48
mobile/openapi/doc/AssetApi.md
generated
48
mobile/openapi/doc/AssetApi.md
generated
|
@ -13,6 +13,7 @@ Method | HTTP request | Description
|
|||
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
[**deleteAsset**](AssetApi.md#deleteasset) | **DELETE** /asset |
|
||||
[**downloadFile**](AssetApi.md#downloadfile) | **GET** /asset/download/{assetId} |
|
||||
[**downloadFiles**](AssetApi.md#downloadfiles) | **POST** /asset/download-files |
|
||||
[**downloadLibrary**](AssetApi.md#downloadlibrary) | **GET** /asset/download-library |
|
||||
[**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset |
|
||||
[**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{assetId} |
|
||||
|
@ -226,6 +227,53 @@ 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)
|
||||
|
||||
# **downloadFiles**
|
||||
> Object downloadFiles(downloadFilesDto)
|
||||
|
||||
|
||||
|
||||
### 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 downloadFilesDto = DownloadFilesDto(); // DownloadFilesDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.downloadFiles(downloadFilesDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->downloadFiles: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**downloadFilesDto** | [**DownloadFilesDto**](DownloadFilesDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**Object**](Object.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)
|
||||
|
||||
# **downloadLibrary**
|
||||
> Object downloadLibrary(skip)
|
||||
|
||||
|
|
18
mobile/openapi/doc/CreateAlbumShareLinkDto.md
generated
Normal file
18
mobile/openapi/doc/CreateAlbumShareLinkDto.md
generated
Normal file
|
@ -0,0 +1,18 @@
|
|||
# openapi.model.CreateAlbumShareLinkDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**albumId** | **String** | |
|
||||
**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)
|
||||
|
||||
|
15
mobile/openapi/doc/DownloadFilesDto.md
generated
Normal file
15
mobile/openapi/doc/DownloadFilesDto.md
generated
Normal file
|
@ -0,0 +1,15 @@
|
|||
# openapi.model.DownloadFilesDto
|
||||
|
||||
## 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)
|
||||
|
||||
|
18
mobile/openapi/doc/EditSharedLinkDto.md
generated
Normal file
18
mobile/openapi/doc/EditSharedLinkDto.md
generated
Normal file
|
@ -0,0 +1,18 @@
|
|||
# openapi.model.EditSharedLinkDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**description** | **String** | | [optional]
|
||||
**expiredAt** | **String** | | [optional]
|
||||
**allowUpload** | **bool** | | [optional]
|
||||
**isEditExpireTime** | **bool** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
217
mobile/openapi/doc/ShareApi.md
generated
Normal file
217
mobile/openapi/doc/ShareApi.md
generated
Normal file
|
@ -0,0 +1,217 @@
|
|||
# openapi.api.ShareApi
|
||||
|
||||
## Load the API package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**editSharedLink**](ShareApi.md#editsharedlink) | **PATCH** /share/{id} |
|
||||
[**getAllSharedLinks**](ShareApi.md#getallsharedlinks) | **GET** /share |
|
||||
[**getMySharedLink**](ShareApi.md#getmysharedlink) | **GET** /share/me |
|
||||
[**getSharedLinkById**](ShareApi.md#getsharedlinkbyid) | **GET** /share/{id} |
|
||||
[**removeSharedLink**](ShareApi.md#removesharedlink) | **DELETE** /share/{id} |
|
||||
|
||||
|
||||
# **editSharedLink**
|
||||
> SharedLinkResponseDto editSharedLink(id, editSharedLinkDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = ShareApi();
|
||||
final id = id_example; // String |
|
||||
final editSharedLinkDto = EditSharedLinkDto(); // EditSharedLinkDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.editSharedLink(id, editSharedLinkDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ShareApi->editSharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
**editSharedLinkDto** | [**EditSharedLinkDto**](EditSharedLinkDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### 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)
|
||||
|
||||
# **getAllSharedLinks**
|
||||
> List<SharedLinkResponseDto> getAllSharedLinks()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = ShareApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getAllSharedLinks();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ShareApi->getAllSharedLinks: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<SharedLinkResponseDto>**](SharedLinkResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **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)
|
||||
|
||||
# **getMySharedLink**
|
||||
> SharedLinkResponseDto getMySharedLink()
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = ShareApi();
|
||||
|
||||
try {
|
||||
final result = api_instance.getMySharedLink();
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ShareApi->getMySharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
This endpoint does not need any parameter.
|
||||
|
||||
### Return type
|
||||
|
||||
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **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)
|
||||
|
||||
# **getSharedLinkById**
|
||||
> SharedLinkResponseDto getSharedLinkById(id)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = ShareApi();
|
||||
final id = id_example; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.getSharedLinkById(id);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ShareApi->getSharedLinkById: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **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)
|
||||
|
||||
# **removeSharedLink**
|
||||
> String removeSharedLink(id)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final api_instance = ShareApi();
|
||||
final id = id_example; // String |
|
||||
|
||||
try {
|
||||
final result = api_instance.removeSharedLink(id);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling ShareApi->removeSharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**id** | **String**| |
|
||||
|
||||
### Return type
|
||||
|
||||
**String**
|
||||
|
||||
### Authorization
|
||||
|
||||
No authorization required
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: Not defined
|
||||
- **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)
|
||||
|
24
mobile/openapi/doc/SharedLinkResponseDto.md
generated
Normal file
24
mobile/openapi/doc/SharedLinkResponseDto.md
generated
Normal file
|
@ -0,0 +1,24 @@
|
|||
# openapi.model.SharedLinkResponseDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**type** | [**SharedLinkType**](SharedLinkType.md) | |
|
||||
**id** | **String** | |
|
||||
**description** | **String** | | [optional]
|
||||
**userId** | **String** | |
|
||||
**key** | **String** | |
|
||||
**createdAt** | **String** | |
|
||||
**expiresAt** | **String** | |
|
||||
**assets** | **List<String>** | | [default to const []]
|
||||
**album** | [**AlbumResponseDto**](AlbumResponseDto.md) | | [optional]
|
||||
**allowUpload** | **bool** | |
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
14
mobile/openapi/doc/SharedLinkType.md
generated
Normal file
14
mobile/openapi/doc/SharedLinkType.md
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
# openapi.model.SharedLinkType
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
6
mobile/openapi/lib/api.dart
generated
6
mobile/openapi/lib/api.dart
generated
|
@ -35,6 +35,7 @@ part 'api/device_info_api.dart';
|
|||
part 'api/job_api.dart';
|
||||
part 'api/o_auth_api.dart';
|
||||
part 'api/server_info_api.dart';
|
||||
part 'api/share_api.dart';
|
||||
part 'api/system_config_api.dart';
|
||||
part 'api/tag_api.dart';
|
||||
part 'api/user_api.dart';
|
||||
|
@ -62,6 +63,7 @@ part 'model/check_duplicate_asset_response_dto.dart';
|
|||
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_profile_image_response_dto.dart';
|
||||
part 'model/create_tag_dto.dart';
|
||||
part 'model/create_user_dto.dart';
|
||||
|
@ -72,6 +74,8 @@ part 'model/delete_asset_response_dto.dart';
|
|||
part 'model/delete_asset_status.dart';
|
||||
part 'model/device_info_response_dto.dart';
|
||||
part 'model/device_type_enum.dart';
|
||||
part 'model/download_files_dto.dart';
|
||||
part 'model/edit_shared_link_dto.dart';
|
||||
part 'model/exif_response_dto.dart';
|
||||
part 'model/get_asset_by_time_bucket_dto.dart';
|
||||
part 'model/get_asset_count_by_time_bucket_dto.dart';
|
||||
|
@ -92,6 +96,8 @@ part 'model/server_info_response_dto.dart';
|
|||
part 'model/server_ping_response.dart';
|
||||
part 'model/server_stats_response_dto.dart';
|
||||
part 'model/server_version_reponse_dto.dart';
|
||||
part 'model/shared_link_response_dto.dart';
|
||||
part 'model/shared_link_type.dart';
|
||||
part 'model/sign_up_dto.dart';
|
||||
part 'model/smart_info_response_dto.dart';
|
||||
part 'model/system_config_dto.dart';
|
||||
|
|
47
mobile/openapi/lib/api/album_api.dart
generated
47
mobile/openapi/lib/api/album_api.dart
generated
|
@ -167,6 +167,53 @@ class AlbumApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /album/create-shared-link' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
|
||||
Future<Response> createAlbumSharedLinkWithHttpInfo(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/album/create-shared-link';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = createAlbumShareLinkDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [CreateAlbumShareLinkDto] createAlbumShareLinkDto (required):
|
||||
Future<SharedLinkResponseDto?> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto,) async {
|
||||
final response = await createAlbumSharedLinkWithHttpInfo(createAlbumShareLinkDto,);
|
||||
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;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /album/{albumId}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
|
47
mobile/openapi/lib/api/asset_api.dart
generated
47
mobile/openapi/lib/api/asset_api.dart
generated
|
@ -233,6 +233,53 @@ class AssetApi {
|
|||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'POST /asset/download-files' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [DownloadFilesDto] downloadFilesDto (required):
|
||||
Future<Response> downloadFilesWithHttpInfo(DownloadFilesDto downloadFilesDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/download-files';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = downloadFilesDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [DownloadFilesDto] downloadFilesDto (required):
|
||||
Future<Object?> downloadFiles(DownloadFilesDto downloadFilesDto,) async {
|
||||
final response = await downloadFilesWithHttpInfo(downloadFilesDto,);
|
||||
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), 'Object',) as Object;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /asset/download-library' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
|
251
mobile/openapi/lib/api/share_api.dart
generated
Normal file
251
mobile/openapi/lib/api/share_api.dart
generated
Normal file
|
@ -0,0 +1,251 @@
|
|||
//
|
||||
// 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 ShareApi {
|
||||
ShareApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient;
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
/// Performs an HTTP 'PATCH /share/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [EditSharedLinkDto] editSharedLinkDto (required):
|
||||
Future<Response> editSharedLinkWithHttpInfo(String id, EditSharedLinkDto editSharedLinkDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/share/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = editSharedLinkDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
///
|
||||
/// * [EditSharedLinkDto] editSharedLinkDto (required):
|
||||
Future<SharedLinkResponseDto?> editSharedLink(String id, EditSharedLinkDto editSharedLinkDto,) async {
|
||||
final response = await editSharedLinkWithHttpInfo(id, editSharedLinkDto,);
|
||||
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;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /share' operation and returns the [Response].
|
||||
Future<Response> getAllSharedLinksWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/share';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<SharedLinkResponseDto>?> getAllSharedLinks() async {
|
||||
final response = await getAllSharedLinksWithHttpInfo();
|
||||
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) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<SharedLinkResponseDto>') as List)
|
||||
.cast<SharedLinkResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /share/me' operation and returns the [Response].
|
||||
Future<Response> getMySharedLinkWithHttpInfo() async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/share/me';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
Future<SharedLinkResponseDto?> getMySharedLink() async {
|
||||
final response = await getMySharedLinkWithHttpInfo();
|
||||
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;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /share/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> getSharedLinkByIdWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/share/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'GET',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<SharedLinkResponseDto?> getSharedLinkById(String id,) async {
|
||||
final response = await getSharedLinkByIdWithHttpInfo(id,);
|
||||
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;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'DELETE /share/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<Response> removeSharedLinkWithHttpInfo(String id,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/share/{id}'
|
||||
.replaceAll('{id}', id);
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>[];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'DELETE',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [String] id (required):
|
||||
Future<String?> removeSharedLink(String id,) async {
|
||||
final response = await removeSharedLinkWithHttpInfo(id,);
|
||||
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), 'String',) as String;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
10
mobile/openapi/lib/api_client.dart
generated
10
mobile/openapi/lib/api_client.dart
generated
|
@ -238,6 +238,8 @@ class ApiClient {
|
|||
return CheckExistingAssetsResponseDto.fromJson(value);
|
||||
case 'CreateAlbumDto':
|
||||
return CreateAlbumDto.fromJson(value);
|
||||
case 'CreateAlbumShareLinkDto':
|
||||
return CreateAlbumShareLinkDto.fromJson(value);
|
||||
case 'CreateProfileImageResponseDto':
|
||||
return CreateProfileImageResponseDto.fromJson(value);
|
||||
case 'CreateTagDto':
|
||||
|
@ -258,6 +260,10 @@ class ApiClient {
|
|||
return DeviceInfoResponseDto.fromJson(value);
|
||||
case 'DeviceTypeEnum':
|
||||
return DeviceTypeEnumTypeTransformer().decode(value);
|
||||
case 'DownloadFilesDto':
|
||||
return DownloadFilesDto.fromJson(value);
|
||||
case 'EditSharedLinkDto':
|
||||
return EditSharedLinkDto.fromJson(value);
|
||||
case 'ExifResponseDto':
|
||||
return ExifResponseDto.fromJson(value);
|
||||
case 'GetAssetByTimeBucketDto':
|
||||
|
@ -298,6 +304,10 @@ class ApiClient {
|
|||
return ServerStatsResponseDto.fromJson(value);
|
||||
case 'ServerVersionReponseDto':
|
||||
return ServerVersionReponseDto.fromJson(value);
|
||||
case 'SharedLinkResponseDto':
|
||||
return SharedLinkResponseDto.fromJson(value);
|
||||
case 'SharedLinkType':
|
||||
return SharedLinkTypeTypeTransformer().decode(value);
|
||||
case 'SignUpDto':
|
||||
return SignUpDto.fromJson(value);
|
||||
case 'SmartInfoResponseDto':
|
||||
|
|
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
|
@ -70,6 +70,9 @@ String parameterToString(dynamic value) {
|
|||
if (value is JobId) {
|
||||
return JobIdTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is SharedLinkType) {
|
||||
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
||||
}
|
||||
if (value is TagTypeEnum) {
|
||||
return TagTypeEnumTypeTransformer().encode(value).toString();
|
||||
}
|
||||
|
|
162
mobile/openapi/lib/model/create_album_share_link_dto.dart
generated
Normal file
162
mobile/openapi/lib/model/create_album_share_link_dto.dart
generated
Normal file
|
@ -0,0 +1,162 @@
|
|||
//
|
||||
// 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 CreateAlbumShareLinkDto {
|
||||
/// Returns a new [CreateAlbumShareLinkDto] instance.
|
||||
CreateAlbumShareLinkDto({
|
||||
required this.albumId,
|
||||
this.expiredAt,
|
||||
this.allowUpload,
|
||||
this.description,
|
||||
});
|
||||
|
||||
String albumId;
|
||||
|
||||
///
|
||||
/// 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 CreateAlbumShareLinkDto &&
|
||||
other.albumId == albumId &&
|
||||
other.expiredAt == expiredAt &&
|
||||
other.allowUpload == allowUpload &&
|
||||
other.description == description;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(albumId.hashCode) +
|
||||
(expiredAt == null ? 0 : expiredAt!.hashCode) +
|
||||
(allowUpload == null ? 0 : allowUpload!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'CreateAlbumShareLinkDto[albumId=$albumId, expiredAt=$expiredAt, allowUpload=$allowUpload, description=$description]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'albumId'] = albumId;
|
||||
if (expiredAt != null) {
|
||||
_json[r'expiredAt'] = expiredAt;
|
||||
} else {
|
||||
_json[r'expiredAt'] = null;
|
||||
}
|
||||
if (allowUpload != null) {
|
||||
_json[r'allowUpload'] = allowUpload;
|
||||
} else {
|
||||
_json[r'allowUpload'] = null;
|
||||
}
|
||||
if (description != null) {
|
||||
_json[r'description'] = description;
|
||||
} else {
|
||||
_json[r'description'] = null;
|
||||
}
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [CreateAlbumShareLinkDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static CreateAlbumShareLinkDto? 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 "CreateAlbumShareLinkDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "CreateAlbumShareLinkDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return CreateAlbumShareLinkDto(
|
||||
albumId: mapValueOfType<String>(json, r'albumId')!,
|
||||
expiredAt: mapValueOfType<String>(json, r'expiredAt'),
|
||||
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<CreateAlbumShareLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <CreateAlbumShareLinkDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = CreateAlbumShareLinkDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, CreateAlbumShareLinkDto> mapFromJson(dynamic json) {
|
||||
final map = <String, CreateAlbumShareLinkDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CreateAlbumShareLinkDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of CreateAlbumShareLinkDto-objects as value to a dart map
|
||||
static Map<String, List<CreateAlbumShareLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<CreateAlbumShareLinkDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = CreateAlbumShareLinkDto.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>{
|
||||
'albumId',
|
||||
};
|
||||
}
|
||||
|
113
mobile/openapi/lib/model/download_files_dto.dart
generated
Normal file
113
mobile/openapi/lib/model/download_files_dto.dart
generated
Normal file
|
@ -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 DownloadFilesDto {
|
||||
/// Returns a new [DownloadFilesDto] instance.
|
||||
DownloadFilesDto({
|
||||
this.assetIds = const [],
|
||||
});
|
||||
|
||||
List<String> assetIds;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is DownloadFilesDto &&
|
||||
other.assetIds == assetIds;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetIds.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'DownloadFilesDto[assetIds=$assetIds]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'assetIds'] = assetIds;
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [DownloadFilesDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static DownloadFilesDto? 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 "DownloadFilesDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "DownloadFilesDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return DownloadFilesDto(
|
||||
assetIds: json[r'assetIds'] is List
|
||||
? (json[r'assetIds'] as List).cast<String>()
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<DownloadFilesDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <DownloadFilesDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = DownloadFilesDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, DownloadFilesDto> mapFromJson(dynamic json) {
|
||||
final map = <String, DownloadFilesDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = DownloadFilesDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of DownloadFilesDto-objects as value to a dart map
|
||||
static Map<String, List<DownloadFilesDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<DownloadFilesDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = DownloadFilesDto.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',
|
||||
};
|
||||
}
|
||||
|
171
mobile/openapi/lib/model/edit_shared_link_dto.dart
generated
Normal file
171
mobile/openapi/lib/model/edit_shared_link_dto.dart
generated
Normal file
|
@ -0,0 +1,171 @@
|
|||
//
|
||||
// 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 EditSharedLinkDto {
|
||||
/// Returns a new [EditSharedLinkDto] instance.
|
||||
EditSharedLinkDto({
|
||||
this.description,
|
||||
this.expiredAt,
|
||||
this.allowUpload,
|
||||
this.isEditExpireTime,
|
||||
});
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
bool? isEditExpireTime;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is EditSharedLinkDto &&
|
||||
other.description == description &&
|
||||
other.expiredAt == expiredAt &&
|
||||
other.allowUpload == allowUpload &&
|
||||
other.isEditExpireTime == isEditExpireTime;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(expiredAt == null ? 0 : expiredAt!.hashCode) +
|
||||
(allowUpload == null ? 0 : allowUpload!.hashCode) +
|
||||
(isEditExpireTime == null ? 0 : isEditExpireTime!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'EditSharedLinkDto[description=$description, expiredAt=$expiredAt, allowUpload=$allowUpload, isEditExpireTime=$isEditExpireTime]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
if (description != null) {
|
||||
_json[r'description'] = description;
|
||||
} else {
|
||||
_json[r'description'] = null;
|
||||
}
|
||||
if (expiredAt != null) {
|
||||
_json[r'expiredAt'] = expiredAt;
|
||||
} else {
|
||||
_json[r'expiredAt'] = null;
|
||||
}
|
||||
if (allowUpload != null) {
|
||||
_json[r'allowUpload'] = allowUpload;
|
||||
} else {
|
||||
_json[r'allowUpload'] = null;
|
||||
}
|
||||
if (isEditExpireTime != null) {
|
||||
_json[r'isEditExpireTime'] = isEditExpireTime;
|
||||
} else {
|
||||
_json[r'isEditExpireTime'] = null;
|
||||
}
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [EditSharedLinkDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static EditSharedLinkDto? 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 "EditSharedLinkDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "EditSharedLinkDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return EditSharedLinkDto(
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
expiredAt: mapValueOfType<String>(json, r'expiredAt'),
|
||||
allowUpload: mapValueOfType<bool>(json, r'allowUpload'),
|
||||
isEditExpireTime: mapValueOfType<bool>(json, r'isEditExpireTime'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<EditSharedLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <EditSharedLinkDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = EditSharedLinkDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, EditSharedLinkDto> mapFromJson(dynamic json) {
|
||||
final map = <String, EditSharedLinkDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = EditSharedLinkDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of EditSharedLinkDto-objects as value to a dart map
|
||||
static Map<String, List<EditSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<EditSharedLinkDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = EditSharedLinkDto.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>{
|
||||
};
|
||||
}
|
||||
|
207
mobile/openapi/lib/model/shared_link_response_dto.dart
generated
Normal file
207
mobile/openapi/lib/model/shared_link_response_dto.dart
generated
Normal file
|
@ -0,0 +1,207 @@
|
|||
//
|
||||
// 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 SharedLinkResponseDto {
|
||||
/// Returns a new [SharedLinkResponseDto] instance.
|
||||
SharedLinkResponseDto({
|
||||
required this.type,
|
||||
required this.id,
|
||||
this.description,
|
||||
required this.userId,
|
||||
required this.key,
|
||||
required this.createdAt,
|
||||
required this.expiresAt,
|
||||
this.assets = const [],
|
||||
this.album,
|
||||
required this.allowUpload,
|
||||
});
|
||||
|
||||
SharedLinkType type;
|
||||
|
||||
String id;
|
||||
|
||||
///
|
||||
/// 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;
|
||||
|
||||
String userId;
|
||||
|
||||
String key;
|
||||
|
||||
String createdAt;
|
||||
|
||||
String? expiresAt;
|
||||
|
||||
List<String> assets;
|
||||
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
AlbumResponseDto? album;
|
||||
|
||||
bool allowUpload;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkResponseDto &&
|
||||
other.type == type &&
|
||||
other.id == id &&
|
||||
other.description == description &&
|
||||
other.userId == userId &&
|
||||
other.key == key &&
|
||||
other.createdAt == createdAt &&
|
||||
other.expiresAt == expiresAt &&
|
||||
other.assets == assets &&
|
||||
other.album == album &&
|
||||
other.allowUpload == allowUpload;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(type.hashCode) +
|
||||
(id.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(userId.hashCode) +
|
||||
(key.hashCode) +
|
||||
(createdAt.hashCode) +
|
||||
(expiresAt == null ? 0 : expiresAt!.hashCode) +
|
||||
(assets.hashCode) +
|
||||
(album == null ? 0 : album!.hashCode) +
|
||||
(allowUpload.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinkResponseDto[type=$type, id=$id, description=$description, userId=$userId, key=$key, createdAt=$createdAt, expiresAt=$expiresAt, assets=$assets, album=$album, allowUpload=$allowUpload]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final _json = <String, dynamic>{};
|
||||
_json[r'type'] = type;
|
||||
_json[r'id'] = id;
|
||||
if (description != null) {
|
||||
_json[r'description'] = description;
|
||||
} else {
|
||||
_json[r'description'] = null;
|
||||
}
|
||||
_json[r'userId'] = userId;
|
||||
_json[r'key'] = key;
|
||||
_json[r'createdAt'] = createdAt;
|
||||
if (expiresAt != null) {
|
||||
_json[r'expiresAt'] = expiresAt;
|
||||
} else {
|
||||
_json[r'expiresAt'] = null;
|
||||
}
|
||||
_json[r'assets'] = assets;
|
||||
if (album != null) {
|
||||
_json[r'album'] = album;
|
||||
} else {
|
||||
_json[r'album'] = null;
|
||||
}
|
||||
_json[r'allowUpload'] = allowUpload;
|
||||
return _json;
|
||||
}
|
||||
|
||||
/// Returns a new [SharedLinkResponseDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SharedLinkResponseDto? 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 "SharedLinkResponseDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "SharedLinkResponseDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return SharedLinkResponseDto(
|
||||
type: SharedLinkType.fromJson(json[r'type'])!,
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
userId: mapValueOfType<String>(json, r'userId')!,
|
||||
key: mapValueOfType<String>(json, r'key')!,
|
||||
createdAt: mapValueOfType<String>(json, r'createdAt')!,
|
||||
expiresAt: mapValueOfType<String>(json, r'expiresAt'),
|
||||
assets: json[r'assets'] is List
|
||||
? (json[r'assets'] as List).cast<String>()
|
||||
: const [],
|
||||
album: AlbumResponseDto.fromJson(json[r'album']),
|
||||
allowUpload: mapValueOfType<bool>(json, r'allowUpload')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SharedLinkResponseDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharedLinkResponseDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharedLinkResponseDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SharedLinkResponseDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SharedLinkResponseDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SharedLinkResponseDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SharedLinkResponseDto-objects as value to a dart map
|
||||
static Map<String, List<SharedLinkResponseDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SharedLinkResponseDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SharedLinkResponseDto.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>{
|
||||
'type',
|
||||
'id',
|
||||
'userId',
|
||||
'key',
|
||||
'createdAt',
|
||||
'expiresAt',
|
||||
'assets',
|
||||
'allowUpload',
|
||||
};
|
||||
}
|
||||
|
85
mobile/openapi/lib/model/shared_link_type.dart
generated
Normal file
85
mobile/openapi/lib/model/shared_link_type.dart
generated
Normal file
|
@ -0,0 +1,85 @@
|
|||
//
|
||||
// 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 SharedLinkType {
|
||||
/// Instantiate a new enum with the provided [value].
|
||||
const SharedLinkType._(this.value);
|
||||
|
||||
/// The underlying value of this enum member.
|
||||
final String value;
|
||||
|
||||
@override
|
||||
String toString() => value;
|
||||
|
||||
String toJson() => value;
|
||||
|
||||
static const ALBUM = SharedLinkType._(r'ALBUM');
|
||||
static const INDIVIDUAL = SharedLinkType._(r'INDIVIDUAL');
|
||||
|
||||
/// List of all possible values in this [enum][SharedLinkType].
|
||||
static const values = <SharedLinkType>[
|
||||
ALBUM,
|
||||
INDIVIDUAL,
|
||||
];
|
||||
|
||||
static SharedLinkType? fromJson(dynamic value) => SharedLinkTypeTypeTransformer().decode(value);
|
||||
|
||||
static List<SharedLinkType>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharedLinkType>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharedLinkType.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transformation class that can [encode] an instance of [SharedLinkType] to String,
|
||||
/// and [decode] dynamic data back to [SharedLinkType].
|
||||
class SharedLinkTypeTypeTransformer {
|
||||
factory SharedLinkTypeTypeTransformer() => _instance ??= const SharedLinkTypeTypeTransformer._();
|
||||
|
||||
const SharedLinkTypeTypeTransformer._();
|
||||
|
||||
String encode(SharedLinkType data) => data.value;
|
||||
|
||||
/// Decodes a [dynamic value][data] to a SharedLinkType.
|
||||
///
|
||||
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||
///
|
||||
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||
/// and users are still using an old app with the old code.
|
||||
SharedLinkType? decode(dynamic data, {bool allowNull = true}) {
|
||||
if (data != null) {
|
||||
switch (data.toString()) {
|
||||
case r'ALBUM': return SharedLinkType.ALBUM;
|
||||
case r'INDIVIDUAL': return SharedLinkType.INDIVIDUAL;
|
||||
default:
|
||||
if (!allowNull) {
|
||||
throw ArgumentError('Unknown enum value to decode: $data');
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Singleton [SharedLinkTypeTypeTransformer] instance.
|
||||
static SharedLinkTypeTypeTransformer? _instance;
|
||||
}
|
||||
|
5
mobile/openapi/test/album_api_test.dart
generated
5
mobile/openapi/test/album_api_test.dart
generated
|
@ -32,6 +32,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
//Future<SharedLinkResponseDto> createAlbumSharedLink(CreateAlbumShareLinkDto createAlbumShareLinkDto) async
|
||||
test('test createAlbumSharedLink', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future deleteAlbum(String albumId) async
|
||||
test('test deleteAlbum', () async {
|
||||
// TODO
|
||||
|
|
5
mobile/openapi/test/asset_api_test.dart
generated
5
mobile/openapi/test/asset_api_test.dart
generated
|
@ -41,6 +41,11 @@ void main() {
|
|||
// TODO
|
||||
});
|
||||
|
||||
//Future<Object> downloadFiles(DownloadFilesDto downloadFilesDto) async
|
||||
test('test downloadFiles', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<Object> downloadLibrary({ num skip }) async
|
||||
test('test downloadLibrary', () async {
|
||||
// TODO
|
||||
|
|
42
mobile/openapi/test/create_album_share_link_dto_test.dart
generated
Normal file
42
mobile/openapi/test/create_album_share_link_dto_test.dart
generated
Normal file
|
@ -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 CreateAlbumShareLinkDto
|
||||
void main() {
|
||||
// final instance = CreateAlbumShareLinkDto();
|
||||
|
||||
group('test CreateAlbumShareLinkDto', () {
|
||||
// String albumId
|
||||
test('to test the property `albumId`', () 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
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
27
mobile/openapi/test/download_files_dto_test.dart
generated
Normal file
27
mobile/openapi/test/download_files_dto_test.dart
generated
Normal file
|
@ -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 DownloadFilesDto
|
||||
void main() {
|
||||
// final instance = DownloadFilesDto();
|
||||
|
||||
group('test DownloadFilesDto', () {
|
||||
// List<String> assetIds (default value: const [])
|
||||
test('to test the property `assetIds`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
42
mobile/openapi/test/edit_shared_link_dto_test.dart
generated
Normal file
42
mobile/openapi/test/edit_shared_link_dto_test.dart
generated
Normal file
|
@ -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 EditSharedLinkDto
|
||||
void main() {
|
||||
// final instance = EditSharedLinkDto();
|
||||
|
||||
group('test EditSharedLinkDto', () {
|
||||
// String description
|
||||
test('to test the property `description`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String expiredAt
|
||||
test('to test the property `expiredAt`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool allowUpload
|
||||
test('to test the property `allowUpload`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool isEditExpireTime
|
||||
test('to test the property `isEditExpireTime`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
46
mobile/openapi/test/share_api_test.dart
generated
Normal file
46
mobile/openapi/test/share_api_test.dart
generated
Normal file
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// 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 ShareApi
|
||||
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
|
||||
});
|
||||
|
||||
});
|
||||
}
|
72
mobile/openapi/test/shared_link_response_dto_test.dart
generated
Normal file
72
mobile/openapi/test/shared_link_response_dto_test.dart
generated
Normal file
|
@ -0,0 +1,72 @@
|
|||
//
|
||||
// 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 SharedLinkResponseDto
|
||||
void main() {
|
||||
// final instance = SharedLinkResponseDto();
|
||||
|
||||
group('test SharedLinkResponseDto', () {
|
||||
// SharedLinkType type
|
||||
test('to test the property `type`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String description
|
||||
test('to test the property `description`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String userId
|
||||
test('to test the property `userId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String key
|
||||
test('to test the property `key`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String createdAt
|
||||
test('to test the property `createdAt`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String expiresAt
|
||||
test('to test the property `expiresAt`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// List<String> assets (default value: const [])
|
||||
test('to test the property `assets`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// AlbumResponseDto album
|
||||
test('to test the property `album`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// bool allowUpload
|
||||
test('to test the property `allowUpload`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
21
mobile/openapi/test/shared_link_type_test.dart
generated
Normal file
21
mobile/openapi/test/shared_link_type_test.dart
generated
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// 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 SharedLinkType
|
||||
void main() {
|
||||
|
||||
group('test SharedLinkType', () {
|
||||
|
||||
});
|
||||
|
||||
}
|
12
notes.md
12
notes.md
|
@ -1,10 +1,6 @@
|
|||
# User defined storage structure
|
||||
## Public sharing
|
||||
|
||||
# Folder structure
|
||||
* Year is the top level
|
||||
* Different parsing sequence will be the second level
|
||||
### Albums
|
||||
|
||||
# Filename
|
||||
* Filename will always be appended by a unique ID. Maybe use https://github.com/ai/nanoid
|
||||
* Example: `notes.md` -> `notes-1234567890.md`
|
||||
* Filename will be unique in the same folder
|
||||
- [ ] Add asset to shared link when new asset is added to shared album
|
||||
- [ ] Prevent public user to delete asset from shared album
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { AlbumEntity, AssetAlbumEntity, UserAlbumEntity } from '@app/database';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { In, Repository, SelectQueryBuilder, DataSource, Brackets } from 'typeorm';
|
||||
import { In, Repository, SelectQueryBuilder, DataSource, Brackets, Not, IsNull } from 'typeorm';
|
||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||
import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
|
@ -14,6 +14,7 @@ import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
|||
export interface IAlbumRepository {
|
||||
create(ownerId: string, createAlbumDto: CreateAlbumDto): Promise<AlbumEntity>;
|
||||
getList(ownerId: string, getAlbumsDto: GetAlbumsDto): Promise<AlbumEntity[]>;
|
||||
getPublicSharingList(ownerId: string): Promise<AlbumEntity[]>;
|
||||
get(albumId: string): Promise<AlbumEntity | undefined>;
|
||||
delete(album: AlbumEntity): Promise<void>;
|
||||
addSharedUsers(album: AlbumEntity, addUsersDto: AddUsersDto): Promise<AlbumEntity>;
|
||||
|
@ -43,6 +44,21 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
private dataSource: DataSource,
|
||||
) {}
|
||||
|
||||
async getPublicSharingList(ownerId: string): Promise<AlbumEntity[]> {
|
||||
return this.albumRepository.find({
|
||||
relations: {
|
||||
sharedLinks: true,
|
||||
assets: true,
|
||||
},
|
||||
where: {
|
||||
ownerId,
|
||||
sharedLinks: {
|
||||
id: Not(IsNull()),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getCountByUserId(userId: string): Promise<AlbumCountResponseDto> {
|
||||
const ownedAlbums = await this.albumRepository.find({ where: { ownerId: userId }, relations: ['sharedUsers'] });
|
||||
|
||||
|
@ -161,6 +177,9 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC');
|
||||
|
||||
// Get information of shared links in albums
|
||||
query = query.leftJoinAndSelect('album.sharedLinks', 'sharedLink');
|
||||
|
||||
const albums = await query.getMany();
|
||||
|
||||
albums.sort((a, b) => new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf());
|
||||
|
@ -203,6 +222,7 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
.leftJoinAndSelect('album.assets', 'assets')
|
||||
.leftJoinAndSelect('assets.assetInfo', 'assetInfo')
|
||||
.leftJoinAndSelect('assetInfo.exifInfo', 'exifInfo')
|
||||
.leftJoinAndSelect('album.sharedLinks', 'sharedLinks')
|
||||
.orderBy('"assetInfo"."createdAt"::timestamptz', 'ASC')
|
||||
.getOne();
|
||||
|
||||
|
|
|
@ -33,25 +33,29 @@ import {
|
|||
IMMICH_CONTENT_LENGTH_HINT,
|
||||
} from '../../constants/download.constant';
|
||||
import { DownloadDto } from '../asset/dto/download-library.dto';
|
||||
import { CreateAlbumShareLinkDto as CreateAlbumSharedLinkDto } from './dto/create-album-shared-link.dto';
|
||||
|
||||
// TODO might be worth creating a AlbumParamsDto that validates `albumId` instead of using the pipe.
|
||||
@Authenticated()
|
||||
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('Album')
|
||||
@Controller('album')
|
||||
export class AlbumController {
|
||||
constructor(private readonly albumService: AlbumService) {}
|
||||
|
||||
@Authenticated()
|
||||
@Get('count-by-user-id')
|
||||
async getAlbumCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AlbumCountResponseDto> {
|
||||
return this.albumService.getAlbumCountByUserId(authUser);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post()
|
||||
async createAlbum(@GetAuthUser() authUser: AuthUserDto, @Body(ValidationPipe) createAlbumDto: CreateAlbumDto) {
|
||||
return this.albumService.create(authUser, createAlbumDto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Put('/:albumId/users')
|
||||
async addUsersToAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -61,6 +65,7 @@ export class AlbumController {
|
|||
return this.albumService.addUsersToAlbum(authUser, addUsersDto, albumId);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Put('/:albumId/assets')
|
||||
async addAssetsToAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -70,6 +75,7 @@ export class AlbumController {
|
|||
return this.albumService.addAssetsToAlbum(authUser, addAssetsDto, albumId);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get()
|
||||
async getAllAlbums(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -78,6 +84,7 @@ export class AlbumController {
|
|||
return this.albumService.getAllAlbums(authUser, query);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/:albumId')
|
||||
async getAlbumInfo(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -86,6 +93,7 @@ export class AlbumController {
|
|||
return this.albumService.getAlbumInfo(authUser, albumId);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Delete('/:albumId/assets')
|
||||
async removeAssetFromAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -95,6 +103,7 @@ export class AlbumController {
|
|||
return this.albumService.removeAssetsFromAlbum(authUser, removeAssetsDto, albumId);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Delete('/:albumId')
|
||||
async deleteAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -103,6 +112,7 @@ export class AlbumController {
|
|||
return this.albumService.deleteAlbum(authUser, albumId);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Delete('/:albumId/user/:userId')
|
||||
async removeUserFromAlbum(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -112,6 +122,7 @@ export class AlbumController {
|
|||
return this.albumService.removeUserFromAlbum(authUser, albumId, userId);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Patch('/:albumId')
|
||||
async updateAlbumInfo(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -121,6 +132,7 @@ export class AlbumController {
|
|||
return this.albumService.updateAlbumInfo(authUser, updateAlbumInfoDto, albumId);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/:albumId/download')
|
||||
async downloadArchive(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -139,4 +151,13 @@ export class AlbumController {
|
|||
res.setHeader(IMMICH_ARCHIVE_COMPLETE, `${complete}`);
|
||||
return stream;
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post('/create-shared-link')
|
||||
async createAlbumSharedLink(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) createAlbumShareLinkDto: CreateAlbumSharedLinkDto,
|
||||
) {
|
||||
return this.albumService.createAlbumSharedLink(authUser, createAlbumShareLinkDto);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { AlbumRepository, IAlbumRepository } from './album-repository';
|
|||
import { DownloadModule } from '../../modules/download/download.module';
|
||||
import { AssetModule } from '../asset/asset.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { ShareModule } from '../share/share.module';
|
||||
|
||||
const ALBUM_REPOSITORY_PROVIDER = {
|
||||
provide: IAlbumRepository,
|
||||
|
@ -19,6 +20,7 @@ const ALBUM_REPOSITORY_PROVIDER = {
|
|||
DownloadModule,
|
||||
UserModule,
|
||||
forwardRef(() => AssetModule),
|
||||
ShareModule,
|
||||
],
|
||||
controllers: [AlbumController],
|
||||
providers: [AlbumService, ALBUM_REPOSITORY_PROVIDER],
|
||||
|
|
|
@ -3,15 +3,15 @@ import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|||
import { BadRequestException, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { AlbumEntity } from '@app/database';
|
||||
import { AlbumResponseDto } from './response-dto/album-response.dto';
|
||||
import { IAssetRepository } from '../asset/asset-repository';
|
||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||
import { IAlbumRepository } from './album-repository';
|
||||
import { DownloadService } from '../../modules/download/download.service';
|
||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
||||
|
||||
describe('Album service', () => {
|
||||
let sut: AlbumService;
|
||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
||||
|
||||
const authUser: AuthUserDto = Object.freeze({
|
||||
|
@ -33,7 +33,7 @@ describe('Album service', () => {
|
|||
albumEntity.sharedUsers = [];
|
||||
albumEntity.assets = [];
|
||||
albumEntity.albumThumbnailAssetId = null;
|
||||
|
||||
albumEntity.sharedLinks = [];
|
||||
return albumEntity;
|
||||
};
|
||||
|
||||
|
@ -94,6 +94,7 @@ describe('Album service', () => {
|
|||
},
|
||||
},
|
||||
];
|
||||
albumEntity.sharedLinks = [];
|
||||
|
||||
return albumEntity;
|
||||
};
|
||||
|
@ -113,6 +114,7 @@ describe('Album service', () => {
|
|||
|
||||
beforeAll(() => {
|
||||
albumRepositoryMock = {
|
||||
getPublicSharingList: jest.fn(),
|
||||
addAssets: jest.fn(),
|
||||
addSharedUsers: jest.fn(),
|
||||
create: jest.fn(),
|
||||
|
@ -127,31 +129,20 @@ describe('Album service', () => {
|
|||
getSharedWithUserAlbumCount: jest.fn(),
|
||||
};
|
||||
|
||||
assetRepositoryMock = {
|
||||
sharedLinkRepositoryMock = {
|
||||
create: jest.fn(),
|
||||
update: jest.fn(),
|
||||
getAllByUserId: jest.fn(),
|
||||
getAllByDeviceId: jest.fn(),
|
||||
getAssetCountByTimeBucket: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getById: jest.fn(),
|
||||
getDetectedObjectsByUserId: jest.fn(),
|
||||
getLocationsByUserId: jest.fn(),
|
||||
getSearchPropertiesByUserId: jest.fn(),
|
||||
getAssetByTimeBucket: jest.fn(),
|
||||
getAssetByChecksum: jest.fn(),
|
||||
getAssetCountByUserId: jest.fn(),
|
||||
getAssetWithNoEXIF: jest.fn(),
|
||||
getAssetWithNoThumbnail: jest.fn(),
|
||||
getAssetWithNoSmartInfo: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
countByIdAndUser: jest.fn(),
|
||||
getByKey: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
downloadServiceMock = {
|
||||
downloadArchive: jest.fn(),
|
||||
};
|
||||
|
||||
sut = new AlbumService(albumRepositoryMock, assetRepositoryMock, downloadServiceMock as DownloadService);
|
||||
sut = new AlbumService(albumRepositoryMock, sharedLinkRepositoryMock, downloadServiceMock as DownloadService);
|
||||
});
|
||||
|
||||
it('creates album', async () => {
|
||||
|
@ -175,10 +166,8 @@ describe('Album service', () => {
|
|||
albumRepositoryMock.getList.mockImplementation(() => Promise.resolve(albums));
|
||||
|
||||
const result = await sut.getAllAlbums(authUser, {});
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].id).toEqual(ownedAlbum.id);
|
||||
expect(result[1].id).toEqual(ownedSharedAlbum.id);
|
||||
expect(result[2].id).toEqual(sharedWithMeAlbum.id);
|
||||
});
|
||||
|
||||
it('gets an owned album', async () => {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { BadRequestException, Inject, Injectable, NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
import { BadRequestException, Inject, Injectable, NotFoundException, ForbiddenException, Logger } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
import { AlbumEntity } from '@app/database';
|
||||
import { AlbumEntity, SharedLinkType } from '@app/database';
|
||||
import { AddUsersDto } from './dto/add-users.dto';
|
||||
import { RemoveAssetsDto } from './dto/remove-assets.dto';
|
||||
import { UpdateAlbumDto } from './dto/update-album.dto';
|
||||
|
@ -9,19 +9,28 @@ import { GetAlbumsDto } from './dto/get-albums.dto';
|
|||
import { AlbumResponseDto, mapAlbum, mapAlbumExcludeAssetInfo } from './response-dto/album-response.dto';
|
||||
import { IAlbumRepository } from './album-repository';
|
||||
import { AlbumCountResponseDto } from './response-dto/album-count-response.dto';
|
||||
import { IAssetRepository } from '../asset/asset-repository';
|
||||
import { AddAssetsResponseDto } from './response-dto/add-assets-response.dto';
|
||||
import { AddAssetsDto } from './dto/add-assets.dto';
|
||||
import { DownloadService } from '../../modules/download/download.service';
|
||||
import { DownloadDto } from '../asset/dto/download-library.dto';
|
||||
import { ShareCore } from '../share/share.core';
|
||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
||||
import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from '../share/response-dto/shared-link-response.dto';
|
||||
import { CreateAlbumShareLinkDto } from './dto/create-album-shared-link.dto';
|
||||
import _ from 'lodash';
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
readonly logger = new Logger(AlbumService.name);
|
||||
private shareCore: ShareCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
||||
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||
private downloadService: DownloadService,
|
||||
) {}
|
||||
) {
|
||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
||||
}
|
||||
|
||||
private async _getAlbum({
|
||||
authUser,
|
||||
|
@ -63,8 +72,14 @@ export class AlbumService {
|
|||
albums = await this._albumRepository.getListByAssetId(authUser.id, getAlbumsDto.assetId);
|
||||
} else {
|
||||
albums = await this._albumRepository.getList(authUser.id, getAlbumsDto);
|
||||
if (getAlbumsDto.shared) {
|
||||
const publicSharingAlbums = await this._albumRepository.getPublicSharingList(authUser.id);
|
||||
albums = [...albums, ...publicSharingAlbums];
|
||||
}
|
||||
}
|
||||
|
||||
albums = _.uniqBy(albums, (album) => album.id);
|
||||
|
||||
for (const album of albums) {
|
||||
await this._checkValidThumbnail(album);
|
||||
}
|
||||
|
@ -85,6 +100,11 @@ export class AlbumService {
|
|||
|
||||
async deleteAlbum(authUser: AuthUserDto, albumId: string): Promise<void> {
|
||||
const album = await this._getAlbum({ authUser, albumId });
|
||||
|
||||
for (const sharedLink of album.sharedLinks) {
|
||||
await this.shareCore.removeSharedLink(sharedLink.id, authUser.id);
|
||||
}
|
||||
|
||||
await this._albumRepository.delete(album);
|
||||
}
|
||||
|
||||
|
@ -125,6 +145,11 @@ export class AlbumService {
|
|||
addAssetsDto: AddAssetsDto,
|
||||
albumId: string,
|
||||
): Promise<AddAssetsResponseDto> {
|
||||
if (authUser.isPublicUser && !authUser.isAllowUpload) {
|
||||
this.logger.warn('Deny public user attempt to add asset to album');
|
||||
throw new ForbiddenException('Public user is not allowed to upload');
|
||||
}
|
||||
|
||||
const album = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||
const result = await this._albumRepository.addAssets(album, addAssetsDto);
|
||||
const newAlbum = await this._getAlbum({ authUser, albumId, validateIsOwner: false });
|
||||
|
@ -174,4 +199,19 @@ export class AlbumService {
|
|||
album.albumThumbnailAssetId = dto.albumThumbnailAssetId || null;
|
||||
}
|
||||
}
|
||||
|
||||
async createAlbumSharedLink(authUser: AuthUserDto, dto: CreateAlbumShareLinkDto): Promise<SharedLinkResponseDto> {
|
||||
const album = await this._getAlbum({ authUser, albumId: dto.albumId });
|
||||
|
||||
const sharedLink = await this.shareCore.createSharedLink(authUser.id, {
|
||||
sharedType: SharedLinkType.ALBUM,
|
||||
expiredAt: dto.expiredAt,
|
||||
allowUpload: dto.allowUpload,
|
||||
album: album,
|
||||
assets: [],
|
||||
description: dto.description,
|
||||
});
|
||||
|
||||
return mapSharedLinkToResponseDto(sharedLink);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class CreateAlbumShareLinkDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
albumId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
expiredAt?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
allowUpload?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
}
|
|
@ -33,7 +33,7 @@ export function mapAlbum(entity: AlbumEntity): AlbumResponseDto {
|
|||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
sharedUsers,
|
||||
shared: sharedUsers.length > 0,
|
||||
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
|
||||
assets: entity.assets?.map((assetAlbum) => mapAsset(assetAlbum.assetInfo)) || [],
|
||||
assetCount: entity.assets?.length || 0,
|
||||
};
|
||||
|
@ -55,7 +55,7 @@ export function mapAlbumExcludeAssetInfo(entity: AlbumEntity): AlbumResponseDto
|
|||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
sharedUsers,
|
||||
shared: sharedUsers.length > 0,
|
||||
shared: sharedUsers.length > 0 || entity.sharedLinks?.length > 0,
|
||||
assets: [],
|
||||
assetCount: entity.assets?.length || 0,
|
||||
};
|
||||
|
|
|
@ -226,7 +226,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
where: {
|
||||
id: assetId,
|
||||
},
|
||||
relations: ['exifInfo', 'tags'],
|
||||
relations: ['exifInfo', 'tags', 'sharedLinks'],
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -49,14 +49,15 @@ import {
|
|||
IMMICH_ARCHIVE_FILE_COUNT,
|
||||
IMMICH_CONTENT_LENGTH_HINT,
|
||||
} from '../../constants/download.constant';
|
||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||
|
||||
@Authenticated()
|
||||
@ApiBearerAuth()
|
||||
@ApiTags('Asset')
|
||||
@Controller('asset')
|
||||
export class AssetController {
|
||||
constructor(private assetService: AssetService, private backgroundTaskService: BackgroundTaskService) {}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Post('upload')
|
||||
@UseInterceptors(
|
||||
FileFieldsInterceptor(
|
||||
|
@ -84,6 +85,7 @@ export class AssetController {
|
|||
return this.assetService.handleUploadedAsset(authUser, createAssetDto, res, originalAssetData, livePhotoAssetData);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/download/:assetId')
|
||||
async downloadFile(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -95,6 +97,23 @@ export class AssetController {
|
|||
return this.assetService.downloadFile(query, assetId, res);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Post('/download-files')
|
||||
async downloadFiles(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Response({ passthrough: true }) res: Res,
|
||||
@Body(new ValidationPipe()) dto: DownloadFilesDto,
|
||||
): Promise<any> {
|
||||
await this.assetService.checkAssetsAccess(authUser, [...dto.assetIds]);
|
||||
const { stream, fileName, fileSize, fileCount, complete } = await this.assetService.downloadFiles(dto);
|
||||
res.attachment(fileName);
|
||||
res.setHeader(IMMICH_CONTENT_LENGTH_HINT, fileSize);
|
||||
res.setHeader(IMMICH_ARCHIVE_FILE_COUNT, fileCount);
|
||||
res.setHeader(IMMICH_ARCHIVE_COMPLETE, `${complete}`);
|
||||
return stream;
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/download-library')
|
||||
async downloadLibrary(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -109,6 +128,7 @@ export class AssetController {
|
|||
return stream;
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/file/:assetId')
|
||||
@Header('Cache-Control', 'max-age=31536000')
|
||||
async serveFile(
|
||||
|
@ -122,6 +142,7 @@ export class AssetController {
|
|||
return this.assetService.serveFile(assetId, query, res, headers);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/thumbnail/:assetId')
|
||||
@Header('Cache-Control', 'max-age=31536000')
|
||||
async getAssetThumbnail(
|
||||
|
@ -135,21 +156,25 @@ export class AssetController {
|
|||
return this.assetService.getAssetThumbnail(assetId, query, res, headers);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get('/curated-objects')
|
||||
async getCuratedObjects(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedObjectsResponseDto[]> {
|
||||
return this.assetService.getCuratedObject(authUser);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get('/curated-locations')
|
||||
async getCuratedLocations(@GetAuthUser() authUser: AuthUserDto): Promise<CuratedLocationsResponseDto[]> {
|
||||
return this.assetService.getCuratedLocation(authUser);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get('/search-terms')
|
||||
async getAssetSearchTerms(@GetAuthUser() authUser: AuthUserDto): Promise<string[]> {
|
||||
return this.assetService.getAssetSearchTerm(authUser);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post('/search')
|
||||
async searchAsset(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -158,6 +183,7 @@ export class AssetController {
|
|||
return this.assetService.searchAsset(authUser, searchAssetDto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post('/count-by-time-bucket')
|
||||
async getAssetCountByTimeBucket(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -166,6 +192,7 @@ export class AssetController {
|
|||
return this.assetService.getAssetCountByTimeBucket(authUser, getAssetCountByTimeGroupDto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get('/count-by-user-id')
|
||||
async getAssetCountByUserId(@GetAuthUser() authUser: AuthUserDto): Promise<AssetCountByUserIdResponseDto> {
|
||||
return this.assetService.getAssetCountByUserId(authUser);
|
||||
|
@ -174,6 +201,7 @@ export class AssetController {
|
|||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
@Authenticated()
|
||||
@Get('/')
|
||||
@ApiHeader({
|
||||
name: 'if-none-match',
|
||||
|
@ -186,6 +214,7 @@ export class AssetController {
|
|||
return assets;
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Post('/time-bucket')
|
||||
async getAssetByTimeBucket(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -193,9 +222,11 @@ export class AssetController {
|
|||
): Promise<AssetResponseDto[]> {
|
||||
return await this.assetService.getAssetByTimeBucket(authUser, getAssetByTimeBucketDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all asset of a device that are in the database, ID only.
|
||||
*/
|
||||
@Authenticated()
|
||||
@Get('/:deviceId')
|
||||
async getUserAssetsByDeviceId(@GetAuthUser() authUser: AuthUserDto, @Param('deviceId') deviceId: string) {
|
||||
return await this.assetService.getUserAssetsByDeviceId(authUser, deviceId);
|
||||
|
@ -204,6 +235,7 @@ export class AssetController {
|
|||
/**
|
||||
* Get a single asset's information
|
||||
*/
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('/assetById/:assetId')
|
||||
async getAssetById(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -216,6 +248,7 @@ export class AssetController {
|
|||
/**
|
||||
* Update an asset
|
||||
*/
|
||||
@Authenticated()
|
||||
@Put('/:assetId')
|
||||
async updateAsset(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -226,6 +259,7 @@ export class AssetController {
|
|||
return await this.assetService.updateAsset(authUser, assetId, dto);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Delete('/')
|
||||
async deleteAsset(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
|
@ -265,6 +299,7 @@ export class AssetController {
|
|||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
*/
|
||||
@Authenticated({ isShared: true })
|
||||
@Post('/check')
|
||||
@HttpCode(200)
|
||||
async checkDuplicateAsset(
|
||||
|
@ -277,6 +312,7 @@ export class AssetController {
|
|||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Authenticated()
|
||||
@Post('/exist')
|
||||
@HttpCode(200)
|
||||
async checkExistingAssets(
|
||||
|
|
|
@ -14,6 +14,7 @@ import { AlbumModule } from '../album/album.module';
|
|||
import { UserModule } from '../user/user.module';
|
||||
import { StorageModule } from '@app/storage';
|
||||
import { immichSharedQueues } from '@app/job/constants/bull-queue-registration.constant';
|
||||
import { ShareModule } from '../share/share.module';
|
||||
|
||||
const ASSET_REPOSITORY_PROVIDER = {
|
||||
provide: IAssetRepository,
|
||||
|
@ -32,6 +33,7 @@ const ASSET_REPOSITORY_PROVIDER = {
|
|||
StorageModule,
|
||||
forwardRef(() => AlbumModule),
|
||||
BullModule.registerQueue(...immichSharedQueues),
|
||||
ShareModule,
|
||||
],
|
||||
controllers: [AssetController],
|
||||
providers: [AssetService, BackgroundTaskService, ASSET_REPOSITORY_PROVIDER],
|
||||
|
|
|
@ -13,6 +13,7 @@ import { IAssetUploadedJob, IVideoTranscodeJob } from '@app/job';
|
|||
import { Queue } from 'bull';
|
||||
import { IAlbumRepository } from '../album/album-repository';
|
||||
import { StorageService } from '@app/storage';
|
||||
import { ISharedLinkRepository } from '../share/shared-link.repository';
|
||||
|
||||
describe('AssetService', () => {
|
||||
let sui: AssetService;
|
||||
|
@ -24,6 +25,7 @@ describe('AssetService', () => {
|
|||
let assetUploadedQueueMock: jest.Mocked<Queue<IAssetUploadedJob>>;
|
||||
let videoConversionQueueMock: jest.Mocked<Queue<IVideoTranscodeJob>>;
|
||||
let storageSeriveMock: jest.Mocked<StorageService>;
|
||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||
const authUser: AuthUserDto = Object.freeze({
|
||||
id: 'user_id_1',
|
||||
email: 'auth@test.com',
|
||||
|
@ -128,12 +130,22 @@ describe('AssetService', () => {
|
|||
getAssetWithNoSmartInfo: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
countByIdAndUser: jest.fn(),
|
||||
getSharePermission: jest.fn(),
|
||||
};
|
||||
|
||||
downloadServiceMock = {
|
||||
downloadArchive: jest.fn(),
|
||||
};
|
||||
|
||||
sharedLinkRepositoryMock = {
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
getById: jest.fn(),
|
||||
getByKey: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
save: jest.fn(),
|
||||
};
|
||||
|
||||
sui = new AssetService(
|
||||
assetRepositoryMock,
|
||||
albumRepositoryMock,
|
||||
|
@ -143,6 +155,7 @@ describe('AssetService', () => {
|
|||
videoConversionQueueMock,
|
||||
downloadServiceMock as DownloadService,
|
||||
storageSeriveMock,
|
||||
sharedLinkRepositoryMock,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -56,11 +56,17 @@ import { DownloadService } from '../../modules/download/download.service';
|
|||
import { DownloadDto } from './dto/download-library.dto';
|
||||
import { IAlbumRepository } from '../album/album-repository';
|
||||
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';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@Injectable()
|
||||
export class AssetService {
|
||||
readonly logger = new Logger(AssetService.name);
|
||||
private shareCore: ShareCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
||||
|
||||
|
@ -80,7 +86,10 @@ export class AssetService {
|
|||
private downloadService: DownloadService,
|
||||
|
||||
private storageService: StorageService,
|
||||
) {}
|
||||
@Inject(ISharedLinkRepository) private sharedLinkRepository: ISharedLinkRepository,
|
||||
) {
|
||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
||||
}
|
||||
|
||||
public async handleUploadedAsset(
|
||||
authUser: AuthUserDto,
|
||||
|
@ -253,6 +262,24 @@ export class AssetService {
|
|||
return this.downloadService.downloadArchive(dto.name || `library`, assets);
|
||||
}
|
||||
|
||||
public async downloadFiles(dto: DownloadFilesDto) {
|
||||
const assetToDownload = [];
|
||||
|
||||
for (const assetId of dto.assetIds) {
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
assetToDownload.push(asset);
|
||||
|
||||
// Get live photo asset
|
||||
if (asset.livePhotoVideoId) {
|
||||
const livePhotoAsset = await this._assetRepository.getById(asset.livePhotoVideoId);
|
||||
assetToDownload.push(livePhotoAsset);
|
||||
}
|
||||
}
|
||||
|
||||
const now = new Date().toISOString();
|
||||
return this.downloadService.downloadArchive(`immich-${now}`, assetToDownload);
|
||||
}
|
||||
|
||||
public async downloadFile(query: ServeFileDto, assetId: string, res: Res) {
|
||||
try {
|
||||
let fileReadStream = null;
|
||||
|
@ -649,7 +676,15 @@ export class AssetService {
|
|||
|
||||
async checkAssetsAccess(authUser: AuthUserDto, assetIds: string[], mustBeOwner = false) {
|
||||
for (const assetId of assetIds) {
|
||||
// Step 1: Check if user owns asset
|
||||
// Step 1: Check if asset is part of a public shared
|
||||
if (authUser.sharedLinkId) {
|
||||
const canAccess = await this.shareCore.hasAssetAccess(authUser.sharedLinkId, assetId);
|
||||
if (!canAccess) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Check if user owns asset
|
||||
if ((await this._assetRepository.countByIdAndUser(assetId, authUser.id)) == 1) {
|
||||
continue;
|
||||
}
|
||||
|
@ -660,8 +695,6 @@ export class AssetService {
|
|||
if ((await this._albumRepository.getSharedWithUserAlbumCount(authUser.id, assetId)) > 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//TODO: Step 3: Check if asset is part of a public album
|
||||
}
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class DownloadFilesDto {
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({
|
||||
isArray: true,
|
||||
type: String,
|
||||
title: 'Array of asset ids to be downloaded',
|
||||
})
|
||||
assetIds!: string[];
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { AlbumEntity, AssetEntity } from '@app/database';
|
||||
import { SharedLinkType } from '@app/database/entities/shared-link.entity';
|
||||
|
||||
export class CreateSharedLinkDto {
|
||||
description?: string;
|
||||
expiredAt?: string;
|
||||
sharedType!: SharedLinkType;
|
||||
assets!: AssetEntity[];
|
||||
album?: AlbumEntity;
|
||||
allowUpload?: boolean;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
||||
|
||||
export class EditSharedLinkDto {
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
expiredAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
allowUpload?: boolean;
|
||||
|
||||
@IsNotEmpty()
|
||||
isEditExpireTime?: boolean;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import { SharedLinkEntity, SharedLinkType } from '@app/database';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import _ from 'lodash';
|
||||
import { AlbumResponseDto, mapAlbumExcludeAssetInfo } from '../../album/response-dto/album-response.dto';
|
||||
import { AssetResponseDto, mapAsset } from '../../asset/response-dto/asset-response.dto';
|
||||
|
||||
export class SharedLinkResponseDto {
|
||||
id!: string;
|
||||
description?: string;
|
||||
userId!: string;
|
||||
key!: string;
|
||||
|
||||
@ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType })
|
||||
type!: SharedLinkType;
|
||||
createdAt!: string;
|
||||
expiresAt!: string | null;
|
||||
assets!: AssetResponseDto[];
|
||||
album?: AlbumResponseDto;
|
||||
allowUpload!: boolean;
|
||||
}
|
||||
|
||||
export function mapSharedLinkToResponseDto(sharedLink: SharedLinkEntity): SharedLinkResponseDto {
|
||||
const linkAssets = sharedLink.assets || [];
|
||||
const albumAssets = (sharedLink?.album?.assets || []).map((albumAsset) => albumAsset.assetInfo);
|
||||
|
||||
const assets = _.uniqBy([...linkAssets, ...albumAssets], (asset) => asset.id);
|
||||
|
||||
return {
|
||||
id: sharedLink.id,
|
||||
description: sharedLink.description,
|
||||
userId: sharedLink.userId,
|
||||
key: sharedLink.key.toString('hex'),
|
||||
type: sharedLink.type,
|
||||
createdAt: sharedLink.createdAt,
|
||||
expiresAt: sharedLink.expiresAt,
|
||||
assets: assets.map(mapAsset),
|
||||
album: sharedLink.album ? mapAlbumExcludeAssetInfo(sharedLink.album) : undefined,
|
||||
allowUpload: sharedLink.allowUpload,
|
||||
};
|
||||
}
|
46
server/apps/immich/src/api-v1/share/share.controller.ts
Normal file
46
server/apps/immich/src/api-v1/share/share.controller.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Body, Controller, Delete, Get, Param, Patch, ValidationPipe } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
|
||||
import { SharedLinkResponseDto } from './response-dto/shared-link-response.dto';
|
||||
import { ShareService } from './share.service';
|
||||
|
||||
@ApiTags('share')
|
||||
@Controller('share')
|
||||
export class ShareController {
|
||||
constructor(private readonly shareService: ShareService) {}
|
||||
@Authenticated()
|
||||
@Get()
|
||||
getAllSharedLinks(@GetAuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.shareService.getAll(authUser);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Get('me')
|
||||
getMySharedLink(@GetAuthUser() authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
||||
return this.shareService.getMine(authUser);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Get(':id')
|
||||
getSharedLinkById(@Param('id') id: string): Promise<SharedLinkResponseDto> {
|
||||
return this.shareService.getById(id);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Delete(':id')
|
||||
removeSharedLink(@Param('id') id: string, @GetAuthUser() authUser: AuthUserDto): Promise<string> {
|
||||
return this.shareService.remove(id, authUser.id);
|
||||
}
|
||||
|
||||
@Authenticated()
|
||||
@Patch(':id')
|
||||
editSharedLink(
|
||||
@Param('id') id: string,
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(new ValidationPipe()) dto: EditSharedLinkDto,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
return this.shareService.edit(id, authUser, dto);
|
||||
}
|
||||
}
|
99
server/apps/immich/src/api-v1/share/share.core.ts
Normal file
99
server/apps/immich/src/api-v1/share/share.core.ts
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { SharedLinkEntity } from '@app/database/entities/shared-link.entity';
|
||||
import { CreateSharedLinkDto } from './dto/create-shared-link.dto';
|
||||
import { ISharedLinkRepository } from './shared-link.repository';
|
||||
import crypto from 'node:crypto';
|
||||
import { BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common';
|
||||
import { AssetEntity } from '@app/database';
|
||||
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
|
||||
|
||||
export class ShareCore {
|
||||
readonly logger = new Logger(ShareCore.name);
|
||||
|
||||
constructor(private sharedLinkRepository: ISharedLinkRepository) {}
|
||||
|
||||
async createSharedLink(userId: string, dto: CreateSharedLinkDto): Promise<SharedLinkEntity> {
|
||||
try {
|
||||
const sharedLink = new SharedLinkEntity();
|
||||
|
||||
sharedLink.key = Buffer.from(crypto.randomBytes(50));
|
||||
sharedLink.description = dto.description;
|
||||
sharedLink.userId = userId;
|
||||
sharedLink.createdAt = new Date().toISOString();
|
||||
sharedLink.expiresAt = dto.expiredAt ?? null;
|
||||
sharedLink.type = dto.sharedType;
|
||||
sharedLink.assets = dto.assets;
|
||||
sharedLink.album = dto.album;
|
||||
sharedLink.allowUpload = dto.allowUpload ?? false;
|
||||
|
||||
return this.sharedLinkRepository.create(sharedLink);
|
||||
} catch (error: any) {
|
||||
this.logger.error(error, error.stack);
|
||||
throw new InternalServerErrorException('failed to create shared link');
|
||||
}
|
||||
}
|
||||
|
||||
async getSharedLinks(userId: string): Promise<SharedLinkEntity[]> {
|
||||
return this.sharedLinkRepository.get(userId);
|
||||
}
|
||||
|
||||
async removeSharedLink(id: string, userId: string): Promise<SharedLinkEntity> {
|
||||
const link = await this.sharedLinkRepository.getByIdAndUserId(id, userId);
|
||||
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
return await this.sharedLinkRepository.remove(link);
|
||||
}
|
||||
|
||||
async getSharedLinkById(id: string): Promise<SharedLinkEntity> {
|
||||
const link = await this.sharedLinkRepository.getById(id);
|
||||
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
async getSharedLinkByKey(key: string): Promise<SharedLinkEntity> {
|
||||
const link = await this.sharedLinkRepository.getByKey(key);
|
||||
|
||||
if (!link) {
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
return link;
|
||||
}
|
||||
|
||||
async updateAssetsInSharedLink(sharedLinkId: string, assets: AssetEntity[]) {
|
||||
const link = await this.getSharedLinkById(sharedLinkId);
|
||||
|
||||
link.assets = assets;
|
||||
|
||||
return await this.sharedLinkRepository.save(link);
|
||||
}
|
||||
|
||||
async updateSharedLink(id: string, userId: string, dto: EditSharedLinkDto): Promise<SharedLinkEntity> {
|
||||
const link = await this.sharedLinkRepository.getByIdAndUserId(id, userId);
|
||||
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
link.description = dto.description ?? link.description;
|
||||
link.allowUpload = dto.allowUpload ?? link.allowUpload;
|
||||
|
||||
if (dto.isEditExpireTime && dto.expiredAt) {
|
||||
link.expiresAt = dto.expiredAt;
|
||||
} else if (dto.isEditExpireTime && !dto.expiredAt) {
|
||||
link.expiresAt = null;
|
||||
}
|
||||
|
||||
return await this.sharedLinkRepository.save(link);
|
||||
}
|
||||
|
||||
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
|
||||
return this.sharedLinkRepository.hasAssetAccess(id, assetId);
|
||||
}
|
||||
}
|
19
server/apps/immich/src/api-v1/share/share.module.ts
Normal file
19
server/apps/immich/src/api-v1/share/share.module.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ShareService } from './share.service';
|
||||
import { ShareController } from './share.controller';
|
||||
import { SharedLinkEntity } from '@app/database/entities/shared-link.entity';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { SharedLinkRepository, ISharedLinkRepository } from './shared-link.repository';
|
||||
|
||||
const SHARED_LINK_REPOSITORY_PROVIDER = {
|
||||
provide: ISharedLinkRepository,
|
||||
useClass: SharedLinkRepository,
|
||||
};
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([SharedLinkEntity])],
|
||||
controllers: [ShareController],
|
||||
providers: [ShareService, SHARED_LINK_REPOSITORY_PROVIDER],
|
||||
exports: [SHARED_LINK_REPOSITORY_PROVIDER, ShareService],
|
||||
})
|
||||
export class ShareModule {}
|
54
server/apps/immich/src/api-v1/share/share.service.ts
Normal file
54
server/apps/immich/src/api-v1/share/share.service.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { ForbiddenException, Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
||||
import { EditSharedLinkDto } from './dto/edit-shared-link.dto';
|
||||
import { mapSharedLinkToResponseDto, SharedLinkResponseDto } from './response-dto/shared-link-response.dto';
|
||||
import { ShareCore } from './share.core';
|
||||
import { ISharedLinkRepository } from './shared-link.repository';
|
||||
|
||||
@Injectable()
|
||||
export class ShareService {
|
||||
readonly logger = new Logger(ShareService.name);
|
||||
private shareCore: ShareCore;
|
||||
|
||||
constructor(
|
||||
@Inject(ISharedLinkRepository)
|
||||
sharedLinkRepository: ISharedLinkRepository,
|
||||
) {
|
||||
this.shareCore = new ShareCore(sharedLinkRepository);
|
||||
}
|
||||
async getAll(authUser: AuthUserDto): Promise<SharedLinkResponseDto[]> {
|
||||
const links = await this.shareCore.getSharedLinks(authUser.id);
|
||||
return links.map(mapSharedLinkToResponseDto);
|
||||
}
|
||||
|
||||
async getMine(authUser: AuthUserDto): Promise<SharedLinkResponseDto> {
|
||||
if (!authUser.isPublicUser || !authUser.sharedLinkId) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const link = await this.shareCore.getSharedLinkById(authUser.sharedLinkId);
|
||||
|
||||
return mapSharedLinkToResponseDto(link);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<SharedLinkResponseDto> {
|
||||
const link = await this.shareCore.getSharedLinkById(id);
|
||||
return mapSharedLinkToResponseDto(link);
|
||||
}
|
||||
|
||||
async remove(id: string, userId: string): Promise<string> {
|
||||
await this.shareCore.removeSharedLink(id, userId);
|
||||
return id;
|
||||
}
|
||||
|
||||
async getByKey(key: string): Promise<SharedLinkResponseDto> {
|
||||
const link = await this.shareCore.getSharedLinkByKey(key);
|
||||
return mapSharedLinkToResponseDto(link);
|
||||
}
|
||||
|
||||
async edit(id: string, authUser: AuthUserDto, dto: EditSharedLinkDto) {
|
||||
const link = await this.shareCore.updateSharedLink(id, authUser.id, dto);
|
||||
|
||||
return mapSharedLinkToResponseDto(link);
|
||||
}
|
||||
}
|
123
server/apps/immich/src/api-v1/share/shared-link.repository.ts
Normal file
123
server/apps/immich/src/api-v1/share/shared-link.repository.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { SharedLinkEntity } from '@app/database/entities/shared-link.entity';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
export interface ISharedLinkRepository {
|
||||
get(userId: string): Promise<SharedLinkEntity[]>;
|
||||
getById(id: string): Promise<SharedLinkEntity | null>;
|
||||
getByIdAndUserId(id: string, userId: string): Promise<SharedLinkEntity | null>;
|
||||
getByKey(key: string): Promise<SharedLinkEntity | null>;
|
||||
create(payload: SharedLinkEntity): Promise<SharedLinkEntity>;
|
||||
remove(entity: SharedLinkEntity): Promise<SharedLinkEntity>;
|
||||
save(entity: SharedLinkEntity): Promise<SharedLinkEntity>;
|
||||
hasAssetAccess(id: string, assetId: string): Promise<boolean>;
|
||||
}
|
||||
|
||||
export const ISharedLinkRepository = 'ISharedLinkRepository';
|
||||
|
||||
export class SharedLinkRepository implements ISharedLinkRepository {
|
||||
readonly logger = new Logger(SharedLinkRepository.name);
|
||||
constructor(
|
||||
@InjectRepository(SharedLinkEntity)
|
||||
private readonly sharedLinkRepository: Repository<SharedLinkEntity>,
|
||||
) {}
|
||||
async getByIdAndUserId(id: string, userId: string): Promise<SharedLinkEntity | null> {
|
||||
return await this.sharedLinkRepository.findOne({
|
||||
where: {
|
||||
userId: userId,
|
||||
id: id,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async get(userId: string): Promise<SharedLinkEntity[]> {
|
||||
return await this.sharedLinkRepository.find({
|
||||
where: {
|
||||
userId: userId,
|
||||
},
|
||||
relations: ['assets', 'album'],
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async create(payload: SharedLinkEntity): Promise<SharedLinkEntity> {
|
||||
return await this.sharedLinkRepository.save(payload);
|
||||
}
|
||||
|
||||
async getById(id: string): Promise<SharedLinkEntity | null> {
|
||||
return await this.sharedLinkRepository.findOne({
|
||||
where: {
|
||||
id: id,
|
||||
},
|
||||
relations: {
|
||||
assets: true,
|
||||
album: {
|
||||
assets: {
|
||||
assetInfo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByKey(key: string): Promise<SharedLinkEntity | null> {
|
||||
return await this.sharedLinkRepository.findOne({
|
||||
where: {
|
||||
key: Buffer.from(key, 'hex'),
|
||||
},
|
||||
relations: {
|
||||
assets: true,
|
||||
album: {
|
||||
assets: {
|
||||
assetInfo: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async remove(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
|
||||
return await this.sharedLinkRepository.remove(entity);
|
||||
}
|
||||
|
||||
async save(entity: SharedLinkEntity): Promise<SharedLinkEntity> {
|
||||
return await this.sharedLinkRepository.save(entity);
|
||||
}
|
||||
|
||||
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
|
||||
const count1 = await this.sharedLinkRepository.count({
|
||||
where: {
|
||||
id,
|
||||
assets: {
|
||||
id: assetId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const count2 = await this.sharedLinkRepository.count({
|
||||
where: {
|
||||
id,
|
||||
album: {
|
||||
assets: {
|
||||
assetId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return Boolean(count1 + count2);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import { JobModule } from './api-v1/job/job.module';
|
|||
import { SystemConfigModule } from './api-v1/system-config/system-config.module';
|
||||
import { OAuthModule } from './api-v1/oauth/oauth.module';
|
||||
import { TagModule } from './api-v1/tag/tag.module';
|
||||
import { ShareModule } from './api-v1/share/share.module';
|
||||
import { APIKeyModule } from './api-v1/api-key/api-key.module';
|
||||
|
||||
@Module({
|
||||
|
@ -58,6 +59,8 @@ import { APIKeyModule } from './api-v1/api-key/api-key.module';
|
|||
SystemConfigModule,
|
||||
|
||||
TagModule,
|
||||
|
||||
ShareModule,
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [],
|
||||
|
|
|
@ -7,6 +7,7 @@ import { existsSync, mkdirSync } from 'fs';
|
|||
import { diskStorage } from 'multer';
|
||||
import { extname, join } from 'path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { AuthUserDto } from '../decorators/auth-user.decorator';
|
||||
import { patchFormData } from '../utils/path-form-data.util';
|
||||
|
||||
const logger = new Logger('AssetUploadConfig');
|
||||
|
@ -42,6 +43,12 @@ function destination(req: Request, file: Express.Multer.File, cb: any) {
|
|||
return cb(new UnauthorizedException());
|
||||
}
|
||||
|
||||
const user = req.user as AuthUserDto;
|
||||
|
||||
if (user.isPublicUser && !user.isAllowUpload) {
|
||||
return cb(new UnauthorizedException());
|
||||
}
|
||||
|
||||
const basePath = APP_UPLOAD_LOCATION;
|
||||
const sanitizedDeviceId = sanitize(String(req.body['deviceId']));
|
||||
const originalUploadFolder = join(basePath, req.user.id, 'original', sanitizedDeviceId);
|
||||
|
|
|
@ -1,23 +1,15 @@
|
|||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { UserEntity } from '@app/database';
|
||||
// import { AuthUserDto } from './dto/auth-user.dto';
|
||||
|
||||
export class AuthUserDto {
|
||||
id!: string;
|
||||
email!: string;
|
||||
isAdmin!: boolean;
|
||||
isPublicUser?: boolean;
|
||||
sharedLinkId?: string;
|
||||
isAllowUpload?: boolean;
|
||||
}
|
||||
|
||||
export const GetAuthUser = createParamDecorator((data, ctx: ExecutionContext): AuthUserDto => {
|
||||
const req = ctx.switchToHttp().getRequest<{ user: UserEntity }>();
|
||||
|
||||
const { id, email, isAdmin } = req.user;
|
||||
|
||||
const authUser: AuthUserDto = {
|
||||
id: id.toString(),
|
||||
email,
|
||||
isAdmin,
|
||||
};
|
||||
|
||||
return authUser;
|
||||
return ctx.switchToHttp().getRequest<{ user: AuthUserDto }>().user;
|
||||
});
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { AdminRolesGuard } from '../middlewares/admin-role-guard.middleware';
|
||||
import { RouteNotSharedGuard } from '../middlewares/route-not-shared-guard.middleware';
|
||||
import { AuthGuard } from '../modules/immich-jwt/guards/auth.guard';
|
||||
|
||||
interface AuthenticatedOptions {
|
||||
admin?: boolean;
|
||||
isShared?: boolean;
|
||||
}
|
||||
|
||||
export const Authenticated = (options?: AuthenticatedOptions) => {
|
||||
const guards: Parameters<typeof UseGuards> = [AuthGuard];
|
||||
|
||||
options = options || {};
|
||||
|
||||
if (options.admin) {
|
||||
guards.push(AdminRolesGuard);
|
||||
}
|
||||
|
||||
if (!options.isShared) {
|
||||
guards.push(RouteNotSharedGuard);
|
||||
}
|
||||
|
||||
return UseGuards(...guards);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AuthUserDto } from '../decorators/auth-user.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RouteNotSharedGuard implements CanActivate {
|
||||
logger = new Logger(RouteNotSharedGuard.name);
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const user = request.user as AuthUserDto;
|
||||
|
||||
// Inverse logic - I know it is weird
|
||||
if (user.isPublicUser) {
|
||||
this.logger.warn(`Denied attempt to access non-shared route: ${request.path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { AuthGuard as PassportAuthGuard } from '@nestjs/passport';
|
||||
import { API_KEY_STRATEGY } from '../strategies/api-key.strategy';
|
||||
import { JWT_STRATEGY } from '../strategies/jwt.strategy';
|
||||
import { PUBLIC_SHARE_STRATEGY } from '../strategies/public-share.strategy';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard extends PassportAuthGuard([JWT_STRATEGY, API_KEY_STRATEGY]) {}
|
||||
export class AuthGuard extends PassportAuthGuard([PUBLIC_SHARE_STRATEGY, JWT_STRATEGY, API_KEY_STRATEGY]) {}
|
||||
|
|
|
@ -7,10 +7,12 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||
import { UserEntity } from '@app/database';
|
||||
import { APIKeyModule } from '../../api-v1/api-key/api-key.module';
|
||||
import { APIKeyStrategy } from './strategies/api-key.strategy';
|
||||
import { ShareModule } from '../../api-v1/share/share.module';
|
||||
import { PublicShareStrategy } from './strategies/public-share.strategy';
|
||||
|
||||
@Module({
|
||||
imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity]), APIKeyModule],
|
||||
providers: [ImmichJwtService, JwtStrategy, APIKeyStrategy],
|
||||
imports: [JwtModule.register(jwtConfig), TypeOrmModule.forFeature([UserEntity]), APIKeyModule, ShareModule],
|
||||
providers: [ImmichJwtService, JwtStrategy, APIKeyStrategy, PublicShareStrategy],
|
||||
exports: [ImmichJwtService],
|
||||
})
|
||||
export class ImmichJwtModule {}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { AuthUserDto } from 'apps/immich/src/decorators/auth-user.decorator';
|
||||
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
|
||||
import { APIKeyService } from '../../../api-v1/api-key/api-key.service';
|
||||
|
||||
|
@ -15,7 +16,16 @@ export class APIKeyStrategy extends PassportStrategy(Strategy, API_KEY_STRATEGY)
|
|||
super(options);
|
||||
}
|
||||
|
||||
async validate(token: string) {
|
||||
return this.apiKeyService.validate(token);
|
||||
async validate(token: string): Promise<AuthUserDto> {
|
||||
const user = await this.apiKeyService.validate(token);
|
||||
|
||||
const authUser = new AuthUserDto();
|
||||
authUser.id = user.id;
|
||||
authUser.email = user.email;
|
||||
authUser.isAdmin = user.isAdmin;
|
||||
authUser.isPublicUser = false;
|
||||
authUser.isAllowUpload = true;
|
||||
|
||||
return authUser;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { JwtPayloadDto } from '../../../api-v1/auth/dto/jwt-payload.dto';
|
|||
import { UserEntity } from '@app/database';
|
||||
import { jwtSecret } from '../../../constants/jwt.constant';
|
||||
import { ImmichJwtService } from '../immich-jwt.service';
|
||||
import { AuthUserDto } from 'apps/immich/src/decorators/auth-user.decorator';
|
||||
|
||||
export const JWT_STRATEGY = 'jwt';
|
||||
|
||||
|
@ -27,7 +28,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) {
|
|||
} as StrategyOptions);
|
||||
}
|
||||
|
||||
async validate(payload: JwtPayloadDto) {
|
||||
async validate(payload: JwtPayloadDto): Promise<AuthUserDto> {
|
||||
const { userId } = payload;
|
||||
const user = await this.usersRepository.findOne({ where: { id: userId } });
|
||||
|
||||
|
@ -35,6 +36,13 @@ export class JwtStrategy extends PassportStrategy(Strategy, JWT_STRATEGY) {
|
|||
throw new UnauthorizedException('Failure to validate JWT payload');
|
||||
}
|
||||
|
||||
return user;
|
||||
const authUser = new AuthUserDto();
|
||||
authUser.id = user.id;
|
||||
authUser.email = user.email;
|
||||
authUser.isAdmin = user.isAdmin;
|
||||
authUser.isPublicUser = false;
|
||||
authUser.isAllowUpload = true;
|
||||
|
||||
return authUser;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import { UserEntity } from '@app/database';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { ShareService } from '../../../api-v1/share/share.service';
|
||||
import { IStrategyOptions, Strategy } from 'passport-http-header-strategy';
|
||||
import { Repository } from 'typeorm';
|
||||
import { AuthUserDto } from 'apps/immich/src/decorators/auth-user.decorator';
|
||||
|
||||
export const PUBLIC_SHARE_STRATEGY = 'public-share';
|
||||
|
||||
const options: IStrategyOptions = {
|
||||
header: 'x-immich-share-key',
|
||||
param: 'key',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PublicShareStrategy extends PassportStrategy(Strategy, PUBLIC_SHARE_STRATEGY) {
|
||||
constructor(
|
||||
private shareService: ShareService,
|
||||
@InjectRepository(UserEntity)
|
||||
private usersRepository: Repository<UserEntity>,
|
||||
) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
async validate(key: string): Promise<AuthUserDto> {
|
||||
const validatedLink = await this.shareService.getByKey(key);
|
||||
|
||||
if (validatedLink.expiresAt) {
|
||||
const now = new Date().getTime();
|
||||
const expiresAt = new Date(validatedLink.expiresAt).getTime();
|
||||
|
||||
if (now > expiresAt) {
|
||||
throw new UnauthorizedException('Expired link');
|
||||
}
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findOne({ where: { id: validatedLink.userId } });
|
||||
|
||||
if (!user) {
|
||||
throw new UnauthorizedException('Failure to validate public share payload');
|
||||
}
|
||||
|
||||
let publicUser = new AuthUserDto();
|
||||
publicUser = user;
|
||||
publicUser.isPublicUser = true;
|
||||
publicUser.sharedLinkId = validatedLink.id;
|
||||
publicUser.isAllowUpload = validatedLink.allowUpload;
|
||||
|
||||
return publicUser;
|
||||
}
|
||||
}
|
|
@ -473,6 +473,147 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/share": {
|
||||
"get": {
|
||||
"operationId": "getAllSharedLinks",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"share"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/share/me": {
|
||||
"get": {
|
||||
"operationId": "getMySharedLink",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"share"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/share/{id}": {
|
||||
"get": {
|
||||
"operationId": "getSharedLinkById",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"share"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "removeSharedLink",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"share"
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "editSharedLink",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EditSharedLinkDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"share"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/upload": {
|
||||
"post": {
|
||||
"operationId": "uploadFile",
|
||||
|
@ -563,6 +704,42 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/asset/download-files": {
|
||||
"post": {
|
||||
"operationId": "downloadFiles",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DownloadFilesDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Asset"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/download-library": {
|
||||
"get": {
|
||||
"operationId": "downloadLibrary",
|
||||
|
@ -1616,6 +1793,42 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/album/create-shared-link": {
|
||||
"post": {
|
||||
"operationId": "createAlbumSharedLink",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateAlbumShareLinkDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Album"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/tag": {
|
||||
"post": {
|
||||
"operationId": "create",
|
||||
|
@ -2666,99 +2879,11 @@
|
|||
"name"
|
||||
]
|
||||
},
|
||||
"AssetFileUploadDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetData": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetData"
|
||||
]
|
||||
},
|
||||
"AssetFileUploadResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"ThumbnailFormat": {
|
||||
"SharedLinkType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"JPEG",
|
||||
"WEBP"
|
||||
]
|
||||
},
|
||||
"CuratedObjectsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"resizePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceAssetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"object",
|
||||
"resizePath",
|
||||
"deviceAssetId",
|
||||
"deviceId"
|
||||
]
|
||||
},
|
||||
"CuratedLocationsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"resizePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceAssetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"city",
|
||||
"resizePath",
|
||||
"deviceAssetId",
|
||||
"deviceId"
|
||||
]
|
||||
},
|
||||
"SearchAssetDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"searchTerm": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"searchTerm"
|
||||
"ALBUM",
|
||||
"INDIVIDUAL"
|
||||
]
|
||||
},
|
||||
"AssetTypeEnum": {
|
||||
|
@ -3019,6 +3144,232 @@
|
|||
"tags"
|
||||
]
|
||||
},
|
||||
"AlbumResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"albumName": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"albumThumbnailAssetId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"shared": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sharedUsers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserResponseDto"
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetCount",
|
||||
"id",
|
||||
"ownerId",
|
||||
"albumName",
|
||||
"createdAt",
|
||||
"albumThumbnailAssetId",
|
||||
"shared",
|
||||
"sharedUsers",
|
||||
"assets"
|
||||
]
|
||||
},
|
||||
"SharedLinkResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/SharedLinkType"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"userId": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiresAt": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
}
|
||||
},
|
||||
"album": {
|
||||
"$ref": "#/components/schemas/AlbumResponseDto"
|
||||
},
|
||||
"allowUpload": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"id",
|
||||
"userId",
|
||||
"key",
|
||||
"createdAt",
|
||||
"expiresAt",
|
||||
"assets",
|
||||
"allowUpload"
|
||||
]
|
||||
},
|
||||
"EditSharedLinkDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiredAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"allowUpload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isEditExpireTime": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AssetFileUploadDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetData": {
|
||||
"type": "string",
|
||||
"format": "binary"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetData"
|
||||
]
|
||||
},
|
||||
"AssetFileUploadResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"DownloadFilesDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
"title": "Array of asset ids to be downloaded",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetIds"
|
||||
]
|
||||
},
|
||||
"ThumbnailFormat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"JPEG",
|
||||
"WEBP"
|
||||
]
|
||||
},
|
||||
"CuratedObjectsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"object": {
|
||||
"type": "string"
|
||||
},
|
||||
"resizePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceAssetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"object",
|
||||
"resizePath",
|
||||
"deviceAssetId",
|
||||
"deviceId"
|
||||
]
|
||||
},
|
||||
"CuratedLocationsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"city": {
|
||||
"type": "string"
|
||||
},
|
||||
"resizePath": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceAssetId": {
|
||||
"type": "string"
|
||||
},
|
||||
"deviceId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"city",
|
||||
"resizePath",
|
||||
"deviceAssetId",
|
||||
"deviceId"
|
||||
]
|
||||
},
|
||||
"SearchAssetDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"searchTerm": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"searchTerm"
|
||||
]
|
||||
},
|
||||
"TimeGroupEnum": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
|
@ -3287,56 +3638,6 @@
|
|||
"albumName"
|
||||
]
|
||||
},
|
||||
"AlbumResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetCount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"type": "string"
|
||||
},
|
||||
"albumName": {
|
||||
"type": "string"
|
||||
},
|
||||
"createdAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"albumThumbnailAssetId": {
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
},
|
||||
"shared": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"sharedUsers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/UserResponseDto"
|
||||
}
|
||||
},
|
||||
"assets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/AssetResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetCount",
|
||||
"id",
|
||||
"ownerId",
|
||||
"albumName",
|
||||
"createdAt",
|
||||
"albumThumbnailAssetId",
|
||||
"shared",
|
||||
"sharedUsers",
|
||||
"assets"
|
||||
]
|
||||
},
|
||||
"AddUsersDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -3411,6 +3712,26 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"CreateAlbumShareLinkDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"albumId": {
|
||||
"type": "string"
|
||||
},
|
||||
"expiredAt": {
|
||||
"type": "string"
|
||||
},
|
||||
"allowUpload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"albumId"
|
||||
]
|
||||
},
|
||||
"CreateTagDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { AssetAlbumEntity } from './asset-album.entity';
|
||||
import { SharedLinkEntity } from './shared-link.entity';
|
||||
import { UserAlbumEntity } from './user-album.entity';
|
||||
|
||||
@Entity('albums')
|
||||
|
@ -24,4 +25,7 @@ export class AlbumEntity {
|
|||
|
||||
@OneToMany(() => AssetAlbumEntity, (assetAlbumEntity) => assetAlbumEntity.albumInfo)
|
||||
assets?: AssetAlbumEntity[];
|
||||
|
||||
@OneToMany(() => SharedLinkEntity, (link) => link.album)
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Column, Entity, Index, JoinTable, ManyToMany, OneToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { ExifEntity } from './exif.entity';
|
||||
import { SharedLinkEntity } from './shared-link.entity';
|
||||
import { SmartInfoEntity } from './smart-info.entity';
|
||||
import { TagEntity } from './tag.entity';
|
||||
|
||||
|
@ -68,6 +69,10 @@ export class AssetEntity {
|
|||
@ManyToMany(() => TagEntity, (tag) => tag.assets, { cascade: true })
|
||||
@JoinTable({ name: 'tag_asset' })
|
||||
tags!: TagEntity[];
|
||||
|
||||
@ManyToMany(() => SharedLinkEntity, (link) => link.assets, { cascade: true })
|
||||
@JoinTable({ name: 'shared_link__asset' })
|
||||
sharedLinks!: SharedLinkEntity[];
|
||||
}
|
||||
|
||||
export enum AssetType {
|
||||
|
|
|
@ -9,3 +9,4 @@ export * from './system-config.entity';
|
|||
export * from './tag.entity';
|
||||
export * from './user-album.entity';
|
||||
export * from './user.entity';
|
||||
export * from './shared-link.entity';
|
||||
|
|
50
server/libs/database/src/entities/shared-link.entity.ts
Normal file
50
server/libs/database/src/entities/shared-link.entity.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Column, Entity, Index, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm';
|
||||
import { AlbumEntity } from './album.entity';
|
||||
import { AssetEntity } from './asset.entity';
|
||||
|
||||
@Entity('shared_links')
|
||||
@Unique('UQ_sharedlink_key', ['key'])
|
||||
export class SharedLinkEntity {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id!: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@Column()
|
||||
userId!: string;
|
||||
|
||||
@Index('IDX_sharedlink_key')
|
||||
@Column({ type: 'bytea' })
|
||||
key!: Buffer; // use to access the inidividual asset
|
||||
|
||||
@Column()
|
||||
type!: SharedLinkType;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
createdAt!: string;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
expiresAt!: string | null;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
allowUpload!: boolean;
|
||||
|
||||
@ManyToMany(() => AssetEntity, (asset) => asset.sharedLinks)
|
||||
assets!: AssetEntity[];
|
||||
|
||||
@ManyToOne(() => AlbumEntity, (album) => album.sharedLinks)
|
||||
album?: AlbumEntity;
|
||||
}
|
||||
|
||||
export enum SharedLinkType {
|
||||
ALBUM = 'ALBUM',
|
||||
|
||||
/**
|
||||
* Individual asset
|
||||
* or group of assets that are not in an album
|
||||
*/
|
||||
INDIVIDUAL = 'INDIVIDUAL',
|
||||
}
|
||||
|
||||
// npm run typeorm -- migration:generate ./libs/database/src/AddSharedLinkTable -d libs/database/src/config/database.config.ts
|
|
@ -0,0 +1,28 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddSharedLinkTable1673150490490 implements MigrationInterface {
|
||||
name = 'AddSharedLinkTable1673150490490'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" character varying NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "expiresAt" TIMESTAMP WITH TIME ZONE, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key"), CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_sharedlink_key" ON "shared_links" ("key") `);
|
||||
await queryRunner.query(`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL, CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId"))`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_5b7decce6c8d3db9593d6111a6" ON "shared_link__asset" ("assetsId") `);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId") `);
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`);
|
||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_c9fab4aa97ffd1b034f3d6581a"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_5b7decce6c8d3db9593d6111a6"`);
|
||||
await queryRunner.query(`DROP TABLE "shared_link__asset"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_key"`);
|
||||
await queryRunner.query(`DROP TABLE "shared_links"`);
|
||||
}
|
||||
|
||||
}
|
20
server/package-lock.json
generated
20
server/package-lock.json
generated
|
@ -47,6 +47,7 @@
|
|||
"nest-commander": "^3.3.0",
|
||||
"openid-client": "^5.2.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-http-header-strategy": "^1.1.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pg": "^8.7.1",
|
||||
|
@ -8619,6 +8620,17 @@
|
|||
"url": "https://github.com/sponsors/jaredhanson"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-custom": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
|
||||
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
|
||||
"dependencies": {
|
||||
"passport-strategy": "1.x.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/passport-http-header-strategy": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz",
|
||||
|
@ -17927,6 +17939,14 @@
|
|||
"utils-merge": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"passport-custom": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/passport-custom/-/passport-custom-1.1.1.tgz",
|
||||
"integrity": "sha512-/2m7jUGxmCYvoqenLB9UrmkCgPt64h8ZtV+UtuQklZ/Tn1NpKBeOorCYkB/8lMRoiZ5hUrCoMmDtxCS/d38mlg==",
|
||||
"requires": {
|
||||
"passport-strategy": "1.x.x"
|
||||
}
|
||||
},
|
||||
"passport-http-header-strategy": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/passport-http-header-strategy/-/passport-http-header-strategy-1.1.0.tgz",
|
||||
|
|
|
@ -70,6 +70,7 @@
|
|||
"nest-commander": "^3.3.0",
|
||||
"openid-client": "^5.2.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-custom": "^1.1.1",
|
||||
"passport-http-header-strategy": "^1.1.0",
|
||||
"passport-jwt": "^4.0.0",
|
||||
"pg": "^8.7.1",
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
JobApi,
|
||||
OAuthApi,
|
||||
ServerInfoApi,
|
||||
ShareApi,
|
||||
SystemConfigApi,
|
||||
UserApi
|
||||
} from './open-api';
|
||||
|
@ -24,6 +25,7 @@ class ImmichApi {
|
|||
public jobApi: JobApi;
|
||||
public keyApi: APIKeyApi;
|
||||
public systemConfigApi: SystemConfigApi;
|
||||
public shareApi: ShareApi;
|
||||
|
||||
private config = new Configuration({ basePath: '/api' });
|
||||
|
||||
|
@ -38,6 +40,7 @@ class ImmichApi {
|
|||
this.jobApi = new JobApi(this.config);
|
||||
this.keyApi = new APIKeyApi(this.config);
|
||||
this.systemConfigApi = new SystemConfigApi(this.config);
|
||||
this.shareApi = new ShareApi(this.config);
|
||||
}
|
||||
|
||||
public setAccessToken(accessToken: string) {
|
||||
|
|
642
web/src/api/open-api/api.ts
generated
642
web/src/api/open-api/api.ts
generated
|
@ -671,6 +671,37 @@ export interface CreateAlbumDto {
|
|||
*/
|
||||
'assetIds'?: Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface CreateAlbumShareLinkDto
|
||||
*/
|
||||
export interface CreateAlbumShareLinkDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CreateAlbumShareLinkDto
|
||||
*/
|
||||
'albumId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CreateAlbumShareLinkDto
|
||||
*/
|
||||
'expiredAt'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof CreateAlbumShareLinkDto
|
||||
*/
|
||||
'allowUpload'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof CreateAlbumShareLinkDto
|
||||
*/
|
||||
'description'?: string;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -918,6 +949,50 @@ export const DeviceTypeEnum = {
|
|||
export type DeviceTypeEnum = typeof DeviceTypeEnum[keyof typeof DeviceTypeEnum];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface DownloadFilesDto
|
||||
*/
|
||||
export interface DownloadFilesDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof DownloadFilesDto
|
||||
*/
|
||||
'assetIds': Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface EditSharedLinkDto
|
||||
*/
|
||||
export interface EditSharedLinkDto {
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof EditSharedLinkDto
|
||||
*/
|
||||
'description'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof EditSharedLinkDto
|
||||
*/
|
||||
'expiredAt'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof EditSharedLinkDto
|
||||
*/
|
||||
'allowUpload'?: boolean;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof EditSharedLinkDto
|
||||
*/
|
||||
'isEditExpireTime'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -1477,6 +1552,87 @@ export interface ServerVersionReponseDto {
|
|||
*/
|
||||
'build': number;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface SharedLinkResponseDto
|
||||
*/
|
||||
export interface SharedLinkResponseDto {
|
||||
/**
|
||||
*
|
||||
* @type {SharedLinkType}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'type': SharedLinkType;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'description'?: string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'userId': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'key': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'createdAt': string;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'expiresAt': string | null;
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'assets': Array<string>;
|
||||
/**
|
||||
*
|
||||
* @type {AlbumResponseDto}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'album'?: AlbumResponseDto;
|
||||
/**
|
||||
*
|
||||
* @type {boolean}
|
||||
* @memberof SharedLinkResponseDto
|
||||
*/
|
||||
'allowUpload': boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @enum {string}
|
||||
*/
|
||||
|
||||
export const SharedLinkType = {
|
||||
Album: 'ALBUM',
|
||||
Individual: 'INDIVIDUAL'
|
||||
} as const;
|
||||
|
||||
export type SharedLinkType = typeof SharedLinkType[keyof typeof SharedLinkType];
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
|
@ -2554,6 +2710,45 @@ export const AlbumApiAxiosParamCreator = function (configuration?: Configuration
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAlbumSharedLink: async (createAlbumShareLinkDto: CreateAlbumShareLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'createAlbumShareLinkDto' is not null or undefined
|
||||
assertParamExists('createAlbumSharedLink', 'createAlbumShareLinkDto', createAlbumShareLinkDto)
|
||||
const localVarPath = `/album/create-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(createAlbumShareLinkDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
|
@ -2915,6 +3110,16 @@ export const AlbumApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbum(createAlbumDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.createAlbumSharedLink(createAlbumShareLinkDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
|
@ -3038,6 +3243,15 @@ export const AlbumApiFactory = function (configuration?: Configuration, basePath
|
|||
createAlbum(createAlbumDto: CreateAlbumDto, options?: any): AxiosPromise<AlbumResponseDto> {
|
||||
return localVarFp.createAlbum(createAlbumDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.createAlbumSharedLink(createAlbumShareLinkDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
|
@ -3159,6 +3373,17 @@ export class AlbumApi extends BaseAPI {
|
|||
return AlbumApiFp(this.configuration).createAlbum(createAlbumDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {CreateAlbumShareLinkDto} createAlbumShareLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AlbumApi
|
||||
*/
|
||||
public createAlbumSharedLink(createAlbumShareLinkDto: CreateAlbumShareLinkDto, options?: AxiosRequestConfig) {
|
||||
return AlbumApiFp(this.configuration).createAlbumSharedLink(createAlbumShareLinkDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} albumId
|
||||
|
@ -3423,6 +3648,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
|||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {DownloadFilesDto} downloadFilesDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
downloadFiles: async (downloadFilesDto: DownloadFilesDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'downloadFilesDto' is not null or undefined
|
||||
assertParamExists('downloadFiles', 'downloadFilesDto', downloadFilesDto)
|
||||
const localVarPath = `/asset/download-files`;
|
||||
// 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(downloadFilesDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} [skip]
|
||||
|
@ -4050,6 +4314,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
|||
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFile(assetId, isThumb, isWeb, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {DownloadFilesDto} downloadFilesDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async downloadFiles(downloadFilesDto: DownloadFilesDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<object>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.downloadFiles(downloadFilesDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} [skip]
|
||||
|
@ -4248,6 +4522,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
|||
downloadFile(assetId: string, isThumb?: boolean, isWeb?: boolean, options?: any): AxiosPromise<object> {
|
||||
return localVarFp.downloadFile(assetId, isThumb, isWeb, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {DownloadFilesDto} downloadFilesDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
downloadFiles(downloadFilesDto: DownloadFilesDto, options?: any): AxiosPromise<object> {
|
||||
return localVarFp.downloadFiles(downloadFilesDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {number} [skip]
|
||||
|
@ -4439,6 +4722,17 @@ export class AssetApi extends BaseAPI {
|
|||
return AssetApiFp(this.configuration).downloadFile(assetId, isThumb, isWeb, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {DownloadFilesDto} downloadFilesDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public downloadFiles(downloadFilesDto: DownloadFilesDto, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).downloadFiles(downloadFilesDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} [skip]
|
||||
|
@ -6052,6 +6346,354 @@ export class ServerInfoApi extends BaseAPI {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* ShareApi - axios parameter creator
|
||||
* @export
|
||||
*/
|
||||
export const ShareApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {EditSharedLinkDto} editSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
editSharedLink: async (id: string, editSharedLinkDto: EditSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('editSharedLink', 'id', id)
|
||||
// verify required parameter 'editSharedLinkDto' is not null or undefined
|
||||
assertParamExists('editSharedLink', 'editSharedLinkDto', editSharedLinkDto)
|
||||
const localVarPath = `/share/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// 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;
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(editSharedLinkDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAllSharedLinks: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/share`;
|
||||
// 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: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getMySharedLink: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
const localVarPath = `/share/me`;
|
||||
// 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: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSharedLinkById: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('getSharedLinkById', 'id', id)
|
||||
const localVarPath = `/share/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// 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: 'GET', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
removeSharedLink: async (id: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'id' is not null or undefined
|
||||
assertParamExists('removeSharedLink', 'id', id)
|
||||
const localVarPath = `/share/{id}`
|
||||
.replace(`{${"id"}}`, encodeURIComponent(String(id)));
|
||||
// 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: 'DELETE', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ShareApi - functional programming interface
|
||||
* @export
|
||||
*/
|
||||
export const ShareApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = ShareApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {EditSharedLinkDto} editSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.editSharedLink(id, editSharedLinkDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getAllSharedLinks(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<SharedLinkResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getAllSharedLinks(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getMySharedLink(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getMySharedLink(options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async getSharedLinkById(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getSharedLinkById(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async removeSharedLink(id: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<string>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.removeSharedLink(id, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* ShareApi - factory interface
|
||||
* @export
|
||||
*/
|
||||
export const ShareApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = ShareApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {EditSharedLinkDto} editSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.editSharedLink(id, editSharedLinkDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getAllSharedLinks(options?: any): AxiosPromise<Array<SharedLinkResponseDto>> {
|
||||
return localVarFp.getAllSharedLinks(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getMySharedLink(options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.getMySharedLink(options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
getSharedLinkById(id: string, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.getSharedLinkById(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
removeSharedLink(id: string, options?: any): AxiosPromise<string> {
|
||||
return localVarFp.removeSharedLink(id, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* ShareApi - object-oriented interface
|
||||
* @export
|
||||
* @class ShareApi
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class ShareApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {EditSharedLinkDto} editSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ShareApi
|
||||
*/
|
||||
public editSharedLink(id: string, editSharedLinkDto: EditSharedLinkDto, options?: AxiosRequestConfig) {
|
||||
return ShareApiFp(this.configuration).editSharedLink(id, editSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ShareApi
|
||||
*/
|
||||
public getAllSharedLinks(options?: AxiosRequestConfig) {
|
||||
return ShareApiFp(this.configuration).getAllSharedLinks(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ShareApi
|
||||
*/
|
||||
public getMySharedLink(options?: AxiosRequestConfig) {
|
||||
return ShareApiFp(this.configuration).getMySharedLink(options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ShareApi
|
||||
*/
|
||||
public getSharedLinkById(id: string, options?: AxiosRequestConfig) {
|
||||
return ShareApiFp(this.configuration).getSharedLinkById(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof ShareApi
|
||||
*/
|
||||
public removeSharedLink(id: string, options?: AxiosRequestConfig) {
|
||||
return ShareApiFp(this.configuration).removeSharedLink(id, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* SystemConfigApi - axios parameter creator
|
||||
* @export
|
||||
|
|
|
@ -4,13 +4,14 @@ import { UserResponseDto } from './open-api';
|
|||
|
||||
const _basePath = '/api';
|
||||
|
||||
export function getFileUrl(assetId: string, isThumb?: boolean, isWeb?: boolean) {
|
||||
export function getFileUrl(assetId: string, isThumb?: boolean, isWeb?: boolean, key?: string) {
|
||||
const urlObj = new URL(`${window.location.origin}${_basePath}/asset/file/${assetId}`);
|
||||
|
||||
if (isThumb !== undefined && isThumb !== null)
|
||||
urlObj.searchParams.append('isThumb', `${isThumb}`);
|
||||
if (isWeb !== undefined && isWeb !== null) urlObj.searchParams.append('isWeb', `${isWeb}`);
|
||||
|
||||
if (key !== undefined && key !== null) urlObj.searchParams.append('key', key);
|
||||
return urlObj.href;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
<body class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
<div>%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
>.
|
||||
</p>
|
||||
|
||||
<SettingSwitch title="Enable" bind:checked={oauthConfig.enabled} />
|
||||
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
<div class="w-full">
|
||||
<div class={`flex place-items-center gap-1 h-[26px]`}>
|
||||
<label class={`immich-form-label text-xs`} for={label}>{label}</label>
|
||||
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
|
||||
{#if required}
|
||||
<div class="text-red-400">*</div>
|
||||
{/if}
|
||||
|
|
|
@ -8,13 +8,13 @@
|
|||
<div class="flex justify-between place-items-center">
|
||||
<div>
|
||||
<h2 class="immich-form-label text-sm">
|
||||
{title.toUpperCase()}
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<label class="relative inline-block w-[36px] h-[10px]" {disabled}>
|
||||
<label class="relative inline-block w-[36px] h-[10px]">
|
||||
<input
|
||||
class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
|
||||
type="checkbox"
|
||||
|
|
|
@ -93,6 +93,7 @@ describe('AlbumCard component', () => {
|
|||
expect(apiMock.assetApi.getAssetThumbnail).toHaveBeenCalledWith(
|
||||
'thumbnailIdOne',
|
||||
ThumbnailFormat.Jpeg,
|
||||
'',
|
||||
{ responseType: 'blob' }
|
||||
);
|
||||
expect(createObjectURLMock).toHaveBeenCalledWith(thumbnailBlob);
|
||||
|
|
|
@ -1,7 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { AlbumResponseDto, api, AssetResponseDto, ThumbnailFormat, UserResponseDto } from '@api';
|
||||
import {
|
||||
AlbumResponseDto,
|
||||
api,
|
||||
AssetResponseDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkType,
|
||||
ThumbnailFormat,
|
||||
UserResponseDto
|
||||
} from '@api';
|
||||
import { onMount } from 'svelte';
|
||||
import ArrowLeft from 'svelte-material-icons/ArrowLeft.svelte';
|
||||
import Plus from 'svelte-material-icons/Plus.svelte';
|
||||
|
@ -23,20 +31,30 @@
|
|||
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
|
||||
import ThumbnailSelection from './thumbnail-selection.svelte';
|
||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
import { browser } from '$app/environment';
|
||||
import { albumAssetSelectionStore } from '$lib/stores/album-asset-selection.store';
|
||||
import CreateSharedLinkModal from '../shared-components/create-share-link-modal/create-shared-link-modal.svelte';
|
||||
import ThemeButton from '../shared-components/theme-button.svelte';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
import { bulkDownload } from '$lib/utils/asset-utils';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let sharedLink: SharedLinkResponseDto | undefined = undefined;
|
||||
|
||||
const { isAlbumAssetSelectionOpen } = albumAssetSelectionStore;
|
||||
|
||||
let isShowAssetViewer = false;
|
||||
|
||||
let isShowAssetSelection = false;
|
||||
|
||||
let isShowShareLinkModal = false;
|
||||
|
||||
$: $isAlbumAssetSelectionOpen = isShowAssetSelection;
|
||||
$: {
|
||||
if (browser) {
|
||||
|
@ -65,6 +83,7 @@
|
|||
let titleInput: HTMLInputElement;
|
||||
let contextMenuPosition = { x: 0, y: 0 };
|
||||
|
||||
$: isPublicShared = sharedLink;
|
||||
$: isOwned = currentUser?.id == album.ownerId;
|
||||
|
||||
let multiSelectAsset: Set<AssetResponseDto> = new Set();
|
||||
|
@ -82,7 +101,11 @@
|
|||
if (album.assets?.length < 6) {
|
||||
thumbnailSize = Math.floor(viewWidth / album.assetCount - album.assetCount);
|
||||
} else {
|
||||
thumbnailSize = Math.floor(viewWidth / 6 - 6);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -219,9 +242,17 @@
|
|||
const createAlbumHandler = async (event: CustomEvent) => {
|
||||
const { assets }: { assets: AssetResponseDto[] } = event.detail;
|
||||
try {
|
||||
const { data } = await api.albumApi.addAssetsToAlbum(album.id, {
|
||||
assetIds: assets.map((a) => a.id)
|
||||
});
|
||||
const { data } = await api.albumApi.addAssetsToAlbum(
|
||||
album.id,
|
||||
{
|
||||
assetIds: assets.map((a) => a.id)
|
||||
},
|
||||
{
|
||||
params: {
|
||||
key: sharedLink?.key
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (data.album) {
|
||||
album = data.album;
|
||||
|
@ -316,6 +347,9 @@
|
|||
album.id,
|
||||
skip || undefined,
|
||||
{
|
||||
params: {
|
||||
key: sharedLink?.key
|
||||
},
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: function (progressEvent) {
|
||||
const request = this as XMLHttpRequest;
|
||||
|
@ -397,6 +431,23 @@
|
|||
|
||||
isShowThumbnailSelection = false;
|
||||
};
|
||||
|
||||
const onSharedLinkClickHandler = () => {
|
||||
isShowShareUserSelection = false;
|
||||
isShowShareLinkModal = true;
|
||||
};
|
||||
|
||||
const handleDownloadSelectedAssets = async () => {
|
||||
await bulkDownload(
|
||||
album.albumName,
|
||||
Array.from(multiSelectAsset),
|
||||
() => {
|
||||
isMultiSelectionMode = false;
|
||||
clearMultiSelectAssetAssetHandler();
|
||||
},
|
||||
sharedLink?.key
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="bg-immich-bg dark:bg-immich-dark-bg">
|
||||
|
@ -413,6 +464,11 @@
|
|||
</p>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
<CircleIconButton
|
||||
title="Download"
|
||||
on:click={handleDownloadSelectedAssets}
|
||||
logo={CloudDownloadOutline}
|
||||
/>
|
||||
{#if isOwned}
|
||||
<CircleIconButton
|
||||
title="Remove from album"
|
||||
|
@ -426,14 +482,45 @@
|
|||
|
||||
<!-- Default app bar -->
|
||||
{#if !isMultiSelectionMode}
|
||||
<ControlAppBar on:close-button-click={() => goto(backUrl)} backIcon={ArrowLeft}>
|
||||
<ControlAppBar
|
||||
on:close-button-click={() => goto(backUrl)}
|
||||
backIcon={ArrowLeft}
|
||||
showBackButton={(!isPublicShared && isOwned) ||
|
||||
(!isPublicShared && !isOwned) ||
|
||||
(isPublicShared && isOwned)}
|
||||
>
|
||||
<svelte:fragment slot="leading">
|
||||
{#if isPublicShared && !isOwned}
|
||||
<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>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
{#if album.assetCount > 0}
|
||||
<CircleIconButton
|
||||
title="Add Photos"
|
||||
on:click={() => (isShowAssetSelection = true)}
|
||||
logo={FileImagePlusOutline}
|
||||
/>
|
||||
{#if !sharedLink}
|
||||
<CircleIconButton
|
||||
title="Add Photos"
|
||||
on:click={() => (isShowAssetSelection = true)}
|
||||
logo={FileImagePlusOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if sharedLink?.allowUpload}
|
||||
<CircleIconButton
|
||||
title="Add Photos"
|
||||
on:click={() => openFileUploadDialog(album.id, sharedLink?.key)}
|
||||
logo={FileImagePlusOutline}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Share and remove album -->
|
||||
{#if isOwned}
|
||||
|
@ -451,11 +538,17 @@
|
|||
logo={FolderDownloadOutline}
|
||||
/>
|
||||
|
||||
<CircleIconButton
|
||||
title="Album options"
|
||||
on:click={(event) => showAlbumOptionsMenu(event)}
|
||||
logo={DotsVertical}
|
||||
/>
|
||||
{#if !isPublicShared}
|
||||
<CircleIconButton
|
||||
title="Album options"
|
||||
on:click={(event) => showAlbumOptionsMenu(event)}
|
||||
logo={DotsVertical}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isPublicShared}
|
||||
<ThemeButton />
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isCreatingSharedAlbum && album.sharedUsers.length == 0}
|
||||
|
@ -470,7 +563,7 @@
|
|||
</ControlAppBar>
|
||||
{/if}
|
||||
|
||||
<section class="m-auto my-[160px] w-[60%]">
|
||||
<section class="flex flex-col my-[160px] px-6 sm:px-12 md:px-24 lg:px-40">
|
||||
<input
|
||||
on:keydown={(e) => {
|
||||
if (e.key == 'Enter') {
|
||||
|
@ -492,7 +585,6 @@
|
|||
{#if album.assetCount > 0}
|
||||
<p class="my-4 text-sm text-gray-500 font-medium">{getDateRange()}</p>
|
||||
{/if}
|
||||
|
||||
{#if album.shared}
|
||||
<div class="my-6 flex">
|
||||
{#each album.sharedUsers as user}
|
||||
|
@ -521,6 +613,7 @@
|
|||
<ImmichThumbnail
|
||||
{asset}
|
||||
{thumbnailSize}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
format={ThumbnailFormat.Jpeg}
|
||||
on:click={(e) =>
|
||||
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
|
||||
|
@ -531,6 +624,7 @@
|
|||
<ImmichThumbnail
|
||||
{asset}
|
||||
{thumbnailSize}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
on:click={(e) =>
|
||||
isMultiSelectionMode ? selectAssetHandler(e) : viewAssetHandler(e)}
|
||||
on:select={selectAssetHandler}
|
||||
|
@ -564,6 +658,7 @@
|
|||
{#if isShowAssetViewer}
|
||||
<AssetViewer
|
||||
asset={selectedAsset}
|
||||
publicSharedKey={sharedLink?.key}
|
||||
on:navigate-previous={navigateAssetBackward}
|
||||
on:navigate-next={navigateAssetForward}
|
||||
on:close={closeViewer}
|
||||
|
@ -581,12 +676,21 @@
|
|||
|
||||
{#if isShowShareUserSelection}
|
||||
<UserSelectionModal
|
||||
{album}
|
||||
on:close={() => (isShowShareUserSelection = false)}
|
||||
on:add-user={addUserHandler}
|
||||
on:sharedlinkclick={onSharedLinkClickHandler}
|
||||
sharedUsersInAlbum={new Set(album.sharedUsers)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if isShowShareLinkModal}
|
||||
<CreateSharedLinkModal
|
||||
on:close={() => (isShowShareLinkModal = false)}
|
||||
shareType={SharedLinkType.Album}
|
||||
{album}
|
||||
/>
|
||||
{/if}
|
||||
{#if isShowShareInfoModal}
|
||||
<ShareInfoModal
|
||||
on:close={() => (isShowShareInfoModal = false)}
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
<svelte:fragment slot="trailing">
|
||||
<button
|
||||
on:click={() =>
|
||||
openFileUploadDialog(albumId, () => {
|
||||
openFileUploadDialog(albumId, '', () => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
dispatch('go-back');
|
||||
})}
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { api, UserResponseDto } from '@api';
|
||||
import { AlbumResponseDto, api, SharedLinkResponseDto, UserResponseDto } from '@api';
|
||||
import BaseModal from '../shared-components/base-modal.svelte';
|
||||
import CircleAvatar from '../shared-components/circle-avatar.svelte';
|
||||
import Link from 'svelte-material-icons/Link.svelte';
|
||||
import ShareCircle from 'svelte-material-icons/ShareCircle.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let album: AlbumResponseDto;
|
||||
export let sharedUsersInAlbum: Set<UserResponseDto>;
|
||||
let users: UserResponseDto[] = [];
|
||||
let selectedUsers: UserResponseDto[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let sharedLinks: SharedLinkResponseDto[] = [];
|
||||
onMount(async () => {
|
||||
await getSharedLinks();
|
||||
const { data } = await api.userApi.getAllUsers(false);
|
||||
|
||||
// remove soft deleted users
|
||||
|
@ -22,6 +27,12 @@
|
|||
});
|
||||
});
|
||||
|
||||
const getSharedLinks = async () => {
|
||||
const { data } = await api.shareApi.getAllSharedLinks();
|
||||
|
||||
sharedLinks = data.filter((link) => link.album?.id === album.id);
|
||||
};
|
||||
|
||||
const selectUser = (user: UserResponseDto) => {
|
||||
if (selectedUsers.includes(user)) {
|
||||
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
|
||||
|
@ -33,6 +44,10 @@
|
|||
const deselectUser = (user: UserResponseDto) => {
|
||||
selectedUsers = selectedUsers.filter((selectedUser) => selectedUser.id !== user.id);
|
||||
};
|
||||
|
||||
const onSharedLinkClick = () => {
|
||||
dispatch('sharedlinkclick');
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal on:close={() => dispatch('close')}>
|
||||
|
@ -93,7 +108,7 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm px-5">
|
||||
<p class="text-sm p-5">
|
||||
Looks like you have shared this album with all users or you don't have any user to share
|
||||
with.
|
||||
</p>
|
||||
|
@ -109,4 +124,25 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div id="shared-buttons" class="flex my-4 justify-around place-items-center place-content-center">
|
||||
<button
|
||||
class="flex flex-col gap-2 place-items-center place-content-center hover:cursor-pointer"
|
||||
on:click={onSharedLinkClick}
|
||||
>
|
||||
<Link size={24} />
|
||||
<p class="text-sm">Create link</p>
|
||||
</button>
|
||||
|
||||
{#if sharedLinks.length}
|
||||
<button
|
||||
class="flex flex-col gap-2 place-items-center place-content-center hover:cursor-pointer"
|
||||
on:click={() => goto('/sharing/sharedlinks')}
|
||||
>
|
||||
<ShareCircle size={24} />
|
||||
<p class="text-sm">View links</p>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</BaseModal>
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
export let showMotionPlayButton: boolean;
|
||||
export let isMotionPhotoPlaying = false;
|
||||
|
||||
const isOwner = asset.ownerId === $page.data.user.id;
|
||||
const isOwner = asset.ownerId === $page.data.user?.id;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
|
@ -94,12 +94,15 @@
|
|||
title="Favorite"
|
||||
/>
|
||||
{/if}
|
||||
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
||||
<CircleIconButton
|
||||
logo={DotsVertical}
|
||||
on:click={(event) => showOptionsMenu(event)}
|
||||
title="More"
|
||||
/>
|
||||
|
||||
{#if isOwner}
|
||||
<CircleIconButton logo={DeleteOutline} on:click={() => dispatch('delete')} title="Delete" />
|
||||
<CircleIconButton
|
||||
logo={DotsVertical}
|
||||
on:click={(event) => showOptionsMenu(event)}
|
||||
title="More"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -10,12 +10,7 @@
|
|||
import { downloadAssets } from '$lib/stores/download';
|
||||
import VideoViewer from './video-viewer.svelte';
|
||||
import AlbumSelectionModal from '../shared-components/album-selection-modal.svelte';
|
||||
import {
|
||||
api,
|
||||
AssetResponseDto,
|
||||
AssetTypeEnum,
|
||||
AlbumResponseDto
|
||||
} from '@api';
|
||||
import { api, AssetResponseDto, AssetTypeEnum, AlbumResponseDto } from '@api';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
|
@ -25,6 +20,9 @@
|
|||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||
|
||||
export let asset: AssetResponseDto;
|
||||
export let publicSharedKey = '';
|
||||
export let showNavigation = true;
|
||||
|
||||
$: {
|
||||
appearsInAlbums = [];
|
||||
|
||||
|
@ -91,12 +89,12 @@
|
|||
|
||||
const handleDownload = () => {
|
||||
if (asset.livePhotoVideoId) {
|
||||
downloadFile(asset.livePhotoVideoId, true);
|
||||
downloadFile(asset.id, false);
|
||||
downloadFile(asset.livePhotoVideoId, true, publicSharedKey);
|
||||
downloadFile(asset.id, false, publicSharedKey);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadFile(asset.id, false);
|
||||
downloadFile(asset.id, false, publicSharedKey);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -111,7 +109,7 @@
|
|||
};
|
||||
};
|
||||
|
||||
const downloadFile = async (assetId: string, isLivePhoto: boolean) => {
|
||||
const downloadFile = async (assetId: string, isLivePhoto: boolean, key: string) => {
|
||||
try {
|
||||
const { filenameWithoutExtension } = getTemplateFilename();
|
||||
|
||||
|
@ -126,6 +124,9 @@
|
|||
$downloadAssets[imageFileName] = 0;
|
||||
|
||||
const { data, status } = await api.assetApi.downloadFile(assetId, false, false, {
|
||||
params: {
|
||||
key
|
||||
},
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: (progressEvent) => {
|
||||
if (progressEvent.lengthComputable) {
|
||||
|
@ -251,69 +252,74 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 mb-[60px] ${
|
||||
asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
|
||||
}`}
|
||||
on:mouseenter={() => {
|
||||
halfLeftHover = true;
|
||||
halfRightHover = false;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
halfLeftHover = false;
|
||||
}}
|
||||
on:click={navigateAssetBackward}
|
||||
on:keydown={navigateAssetBackward}
|
||||
>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
|
||||
class:navigation-button-hover={halfLeftHover}
|
||||
{#if showNavigation}
|
||||
<div
|
||||
class={`row-start-2 row-span-end col-start-1 col-span-2 flex place-items-center hover:cursor-pointer w-3/4 mb-[60px] ${
|
||||
asset.type === AssetTypeEnum.Video ? '' : 'z-[999]'
|
||||
}`}
|
||||
on:mouseenter={() => {
|
||||
halfLeftHover = true;
|
||||
halfRightHover = false;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
halfLeftHover = false;
|
||||
}}
|
||||
on:click={navigateAssetBackward}
|
||||
on:keydown={navigateAssetBackward}
|
||||
>
|
||||
<ChevronLeft size="36" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 z-[1000] text-gray-500 mx-4"
|
||||
class:navigation-button-hover={halfLeftHover}
|
||||
on:click={navigateAssetBackward}
|
||||
>
|
||||
<ChevronLeft size="36" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="row-start-1 row-span-full col-start-1 col-span-4">
|
||||
{#key asset.id}
|
||||
{#if asset.type === AssetTypeEnum.Image}
|
||||
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
||||
<VideoViewer
|
||||
{publicSharedKey}
|
||||
assetId={asset.livePhotoVideoId}
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||
/>
|
||||
{:else}
|
||||
<PhotoViewer assetId={asset.id} on:close={closeViewer} />
|
||||
<PhotoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />
|
||||
{/if}
|
||||
{:else}
|
||||
<VideoViewer assetId={asset.id} on:close={closeViewer} />
|
||||
<VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end mb-[60px] ${
|
||||
asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
|
||||
}`}
|
||||
on:click={navigateAssetForward}
|
||||
on:keydown={navigateAssetForward}
|
||||
on:mouseenter={() => {
|
||||
halfLeftHover = false;
|
||||
halfRightHover = true;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
halfRightHover = false;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
|
||||
class:navigation-button-hover={halfRightHover}
|
||||
{#if showNavigation}
|
||||
<div
|
||||
class={`row-start-2 row-span-full col-start-3 col-span-2 flex justify-end place-items-center hover:cursor-pointer w-3/4 justify-self-end mb-[60px] ${
|
||||
asset.type === AssetTypeEnum.Video ? '' : 'z-[500]'
|
||||
}`}
|
||||
on:click={navigateAssetForward}
|
||||
on:keydown={navigateAssetForward}
|
||||
on:mouseenter={() => {
|
||||
halfLeftHover = false;
|
||||
halfRightHover = true;
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
halfRightHover = false;
|
||||
}}
|
||||
>
|
||||
<ChevronRight size="36" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="rounded-full p-3 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
|
||||
class:navigation-button-hover={halfRightHover}
|
||||
on:click={navigateAssetForward}
|
||||
>
|
||||
<ChevronRight size="36" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if isShowDetail}
|
||||
<div
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let assetId: string;
|
||||
export let publicSharedKey = '';
|
||||
|
||||
let assetInfo: AssetResponseDto;
|
||||
let assetData: string;
|
||||
|
@ -18,7 +19,11 @@
|
|||
let copyImageToClipboard: (src: string) => Promise<Blob>;
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.assetApi.getAssetById(assetId);
|
||||
const { data } = await api.assetApi.getAssetById(assetId, {
|
||||
params: {
|
||||
key: publicSharedKey
|
||||
}
|
||||
});
|
||||
assetInfo = data;
|
||||
|
||||
//Import hack :( see https://github.com/vadimkorr/svelte-carousel/issues/27#issuecomment-851022295
|
||||
|
@ -29,6 +34,9 @@
|
|||
const loadAssetData = async () => {
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(assetInfo.id, false, true, {
|
||||
params: {
|
||||
key: publicSharedKey
|
||||
},
|
||||
responseType: 'blob'
|
||||
});
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { api, AssetResponseDto, getFileUrl } from '@api';
|
||||
|
||||
export let assetId: string;
|
||||
|
||||
export let publicSharedKey = '';
|
||||
let asset: AssetResponseDto;
|
||||
|
||||
let videoPlayerNode: HTMLVideoElement;
|
||||
|
@ -15,7 +15,11 @@
|
|||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(async () => {
|
||||
const { data: assetInfo } = await api.assetApi.getAssetById(assetId);
|
||||
const { data: assetInfo } = await api.assetApi.getAssetById(assetId, {
|
||||
params: {
|
||||
key: publicSharedKey
|
||||
}
|
||||
});
|
||||
|
||||
await loadVideoData(assetInfo);
|
||||
|
||||
|
@ -25,7 +29,7 @@
|
|||
const loadVideoData = async (assetInfo: AssetResponseDto) => {
|
||||
isVideoLoading = true;
|
||||
|
||||
videoUrl = getFileUrl(assetInfo.id, false, true);
|
||||
videoUrl = getFileUrl(assetInfo.id, false, true, publicSharedKey);
|
||||
|
||||
return assetInfo;
|
||||
};
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let showBackButton = true;
|
||||
export let backIcon = Close;
|
||||
export let tailwindClasses = '';
|
||||
|
||||
|
@ -42,14 +44,15 @@
|
|||
class={`flex justify-between ${appBarBorder} rounded-lg p-2 mx-2 mt-2 transition-all place-items-center ${tailwindClasses} dark:bg-immich-dark-gray`}
|
||||
>
|
||||
<div class="flex place-items-center gap-6 dark:text-immich-dark-fg">
|
||||
<CircleIconButton
|
||||
on:click={() => dispatch('close-button-click')}
|
||||
logo={backIcon}
|
||||
backgroundColor={'transparent'}
|
||||
hoverColor={'#e2e7e9'}
|
||||
size={'24'}
|
||||
/>
|
||||
|
||||
{#if showBackButton}
|
||||
<CircleIconButton
|
||||
on:click={() => dispatch('close-button-click')}
|
||||
logo={backIcon}
|
||||
backgroundColor={'transparent'}
|
||||
hoverColor={'#e2e7e9'}
|
||||
size={'24'}
|
||||
/>
|
||||
{/if}
|
||||
<slot name="leading" />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,243 @@
|
|||
<script lang="ts">
|
||||
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 { notificationController, NotificationType } from '../notification/notification';
|
||||
import { ImmichDropDownOption } from '../dropdown-button.svelte';
|
||||
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
|
||||
import DropdownButton from '../dropdown-button.svelte';
|
||||
import SettingInputField, {
|
||||
SettingInputFieldType
|
||||
} from '$lib/components/admin-page/settings/setting-input-field.svelte';
|
||||
|
||||
export let shareType: SharedLinkType;
|
||||
export let album: AlbumResponseDto | undefined;
|
||||
export let editingLink: SharedLinkResponseDto | undefined = undefined;
|
||||
|
||||
let isLoading = false;
|
||||
let isShowSharedLink = false;
|
||||
let expirationTime = '';
|
||||
let isAllowUpload = false;
|
||||
let sharedLink = '';
|
||||
let description = '';
|
||||
let shouldChangeExpirationTime = false;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const expiredDateOption: ImmichDropDownOption = {
|
||||
default: 'Never',
|
||||
options: ['Never', '30 minutes', '1 hour', '6 hours', '1 day', '7 days', '30 days']
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (editingLink) {
|
||||
if (editingLink.description) {
|
||||
description = editingLink.description;
|
||||
}
|
||||
isAllowUpload = editingLink.allowUpload;
|
||||
}
|
||||
});
|
||||
|
||||
const createAlbumSharedLink = async () => {
|
||||
if (album) {
|
||||
isLoading = true;
|
||||
try {
|
||||
const expirationTime = getExpirationTimeInMillisecond();
|
||||
const currentTime = new Date().getTime();
|
||||
const expirationDate = expirationTime
|
||||
? new Date(currentTime + expirationTime).toISOString()
|
||||
: undefined;
|
||||
|
||||
const { data } = await api.albumApi.createAlbumSharedLink({
|
||||
albumId: album.id,
|
||||
expiredAt: expirationDate,
|
||||
allowUpload: isAllowUpload,
|
||||
description: description
|
||||
});
|
||||
|
||||
buildSharedLink(data);
|
||||
isLoading = false;
|
||||
isShowSharedLink = true;
|
||||
} catch (e) {
|
||||
console.error('[createAlbumSharedLink] Error: ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Failed to create shared link'
|
||||
});
|
||||
isLoading = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const buildSharedLink = (createdLink: SharedLinkResponseDto) => {
|
||||
sharedLink = `${window.location.origin}/share/${createdLink.key}`;
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(sharedLink);
|
||||
notificationController.show({
|
||||
message: 'Copied to clipboard!',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getExpirationTimeInMillisecond = () => {
|
||||
switch (expirationTime) {
|
||||
case '30 minutes':
|
||||
return 30 * 60 * 1000;
|
||||
case '1 hour':
|
||||
return 60 * 60 * 1000;
|
||||
case '6 hours':
|
||||
return 6 * 60 * 60 * 1000;
|
||||
case '1 day':
|
||||
return 24 * 60 * 60 * 1000;
|
||||
case '7 days':
|
||||
return 7 * 24 * 60 * 60 * 1000;
|
||||
case '30 days':
|
||||
return 30 * 24 * 60 * 60 * 1000;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditLink = async () => {
|
||||
if (editingLink) {
|
||||
try {
|
||||
const expirationTime = getExpirationTimeInMillisecond();
|
||||
const currentTime = new Date().getTime();
|
||||
let expirationDate = expirationTime
|
||||
? new Date(currentTime + expirationTime).toISOString()
|
||||
: undefined;
|
||||
|
||||
if (expirationTime === 0) {
|
||||
expirationDate = undefined;
|
||||
}
|
||||
|
||||
await api.shareApi.editSharedLink(editingLink.id, {
|
||||
description: description,
|
||||
expiredAt: expirationDate,
|
||||
allowUpload: isAllowUpload,
|
||||
isEditExpireTime: shouldChangeExpirationTime
|
||||
});
|
||||
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
message: 'Edited'
|
||||
});
|
||||
|
||||
dispatch('close');
|
||||
} catch (e) {
|
||||
console.error('[handleEditLink]', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Failed to edit shared link'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseModal on:close={() => dispatch('close')}>
|
||||
<svelte:fragment slot="title">
|
||||
<span class="flex gap-2 place-items-center">
|
||||
<Link size={24} />
|
||||
{#if editingLink}
|
||||
<p class="font-medium text-immich-fg dark:text-immich-dark-fg">Edit link</p>
|
||||
{:else}
|
||||
<p class="font-medium text-immich-fg dark:text-immich-dark-fg">Create link to share</p>
|
||||
{/if}
|
||||
</span>
|
||||
</svelte:fragment>
|
||||
|
||||
<section class="mx-6 mb-6">
|
||||
{#if shareType == SharedLinkType.Album}
|
||||
{#if !editingLink}
|
||||
<div>Let anyone with the link see photos and people in this album.</div>
|
||||
{:else}
|
||||
<div class="text-sm">
|
||||
Public album | <span class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>{editingLink.album?.albumName}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 mb-2">
|
||||
<p class="text-xs">LINK OPTIONS</p>
|
||||
</div>
|
||||
<div class="p-4 bg-gray-100 dark:bg-black/40 rounded-lg">
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Description"
|
||||
bind:value={description}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingSwitch bind:checked={isAllowUpload} title={'Allow public user to upload'} />
|
||||
|
||||
<div class="text-sm mt-4">
|
||||
{#if editingLink}
|
||||
<p class="my-2 immich-form-label">
|
||||
<SettingSwitch
|
||||
bind:checked={shouldChangeExpirationTime}
|
||||
title={'Change expiration time'}
|
||||
/>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="my-2 immich-form-label">Expire after</p>
|
||||
{/if}
|
||||
|
||||
<DropdownButton
|
||||
options={expiredDateOption}
|
||||
bind:selected={expirationTime}
|
||||
disabled={editingLink && !shouldChangeExpirationTime}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<hr />
|
||||
|
||||
<section class="m-6">
|
||||
{#if !isShowSharedLink}
|
||||
{#if editingLink}
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={handleEditLink}
|
||||
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"
|
||||
>
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={createAlbumSharedLink}
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if isShowSharedLink}
|
||||
<div class="flex w-full gap-4">
|
||||
<input class="immich-form-input w-full" bind:value={sharedLink} />
|
||||
|
||||
<button
|
||||
on:click={() => handleCopy()}
|
||||
class="flex-1 transition-colors bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 dark:hover:bg-immich-dark-primary/80 dark:text-immich-dark-gray px-6 py-2 text-white rounded-full shadow-md w-full font-medium"
|
||||
>Copy</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
</BaseModal>
|
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts" context="module">
|
||||
export type ImmichDropDownOption = {
|
||||
default: string;
|
||||
options: string[];
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let options: ImmichDropDownOption;
|
||||
export let selected: string;
|
||||
export let disabled = false;
|
||||
|
||||
onMount(() => {
|
||||
selected = options.default;
|
||||
});
|
||||
|
||||
export let isOpen = false;
|
||||
const toggle = () => (isOpen = !isOpen);
|
||||
</script>
|
||||
|
||||
<div id="immich-dropdown" class="relative">
|
||||
<button
|
||||
{disabled}
|
||||
on:click={toggle}
|
||||
aria-expanded={isOpen}
|
||||
class="bg-gray-200 w-full flex p-2 rounded-lg dark:bg-gray-600 place-items-center justify-between disabled:cursor-not-allowed dark:disabled:bg-gray-300 disabled:bg-gray-600 "
|
||||
>
|
||||
<div>
|
||||
{selected}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<svg
|
||||
style="tran"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<div class="flex flex-col mt-2 absolute w-full">
|
||||
{#each options.options as option}
|
||||
<button
|
||||
on:click={() => {
|
||||
selected = option;
|
||||
isOpen = false;
|
||||
}}
|
||||
class="bg-gray-200 dark:bg-gray-500 dark:hover:bg-gray-700 w-full flex p-2 hover:bg-gray-300 transition-all "
|
||||
>
|
||||
{option}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
transition: transform 0.2s ease-in;
|
||||
}
|
||||
|
||||
[aria-expanded='true'] svg {
|
||||
transform: rotate(0.5turn);
|
||||
}
|
||||
</style>
|
|
@ -18,6 +18,9 @@
|
|||
export let format: ThumbnailFormat = ThumbnailFormat.Webp;
|
||||
export let selected = false;
|
||||
export let disabled = false;
|
||||
export let publicSharedKey = '';
|
||||
export let isRoundedCorner = false;
|
||||
|
||||
let imageData: string;
|
||||
|
||||
let mouseOver = false;
|
||||
|
@ -35,10 +38,9 @@
|
|||
isThumbnailVideoPlaying = false;
|
||||
|
||||
if (isLivePhoto && asset.livePhotoVideoId) {
|
||||
console.log('get file url');
|
||||
videoUrl = getFileUrl(asset.livePhotoVideoId, false, true);
|
||||
videoUrl = getFileUrl(asset.livePhotoVideoId, false, true, publicSharedKey);
|
||||
} else {
|
||||
videoUrl = getFileUrl(asset.id, false, true);
|
||||
videoUrl = getFileUrl(asset.id, false, true, publicSharedKey);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -118,6 +120,8 @@
|
|||
return 'border-[20px] border-immich-primary/20';
|
||||
} else if (disabled) {
|
||||
return 'border-[20px] border-gray-300';
|
||||
} else if (isRoundedCorner) {
|
||||
return 'rounded-[20px]';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
|
@ -244,7 +248,7 @@
|
|||
style:width={`${thumbnailSize}px`}
|
||||
style:height={`${thumbnailSize}px`}
|
||||
in:fade={{ duration: 150 }}
|
||||
src={`/api/asset/thumbnail/${asset.id}?format=${format}`}
|
||||
src={`/api/asset/thumbnail/${asset.id}?format=${format}&key=${publicSharedKey}`}
|
||||
alt={asset.id}
|
||||
class={`object-cover ${getSize()} transition-all z-0 ${getThumbnailBorderStyle()}`}
|
||||
loading="lazy"
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
on:click={toggleTheme}
|
||||
id="theme-toggle"
|
||||
type="button"
|
||||
class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5"
|
||||
class="text-gray-500 dark:text-immich-dark-primary hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none rounded-full text-sm p-2.5"
|
||||
>
|
||||
<svg
|
||||
id="theme-toggle-dark-icon"
|
||||
|
|
142
web/src/lib/components/sharedlinks-page/shared-link-card.svelte
Normal file
142
web/src/lib/components/sharedlinks-page/shared-link-card.svelte
Normal file
|
@ -0,0 +1,142 @@
|
|||
<script lang="ts">
|
||||
import { api, AssetResponseDto, SharedLinkResponseDto, SharedLinkType } from '@api';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import OpenInNew from 'svelte-material-icons/OpenInNew.svelte';
|
||||
import Delete from 'svelte-material-icons/TrashCanOutline.svelte';
|
||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||
import CircleEditOutline from 'svelte-material-icons/CircleEditOutline.svelte';
|
||||
import * as luxon from 'luxon';
|
||||
import CircleIconButton from '../shared-components/circle-icon-button.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let link: SharedLinkResponseDto;
|
||||
|
||||
let expirationCountdown: luxon.DurationObjectUnits;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const getAssetInfo = async (): Promise<AssetResponseDto> => {
|
||||
let assetId = '';
|
||||
|
||||
if (link.album?.albumThumbnailAssetId) {
|
||||
assetId = link.album.albumThumbnailAssetId;
|
||||
} else if (link.assets.length > 0) {
|
||||
assetId = link.assets[0];
|
||||
}
|
||||
|
||||
const { data } = await api.assetApi.getAssetById(assetId);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
const getCountDownExpirationDate = () => {
|
||||
if (!link.expiresAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expiresAtDate = luxon.DateTime.fromISO(new Date(link.expiresAt).toISOString());
|
||||
const now = luxon.DateTime.now();
|
||||
|
||||
expirationCountdown = expiresAtDate
|
||||
.diff(now, ['days', 'hours', 'minutes', 'seconds'])
|
||||
.toObject();
|
||||
|
||||
if (expirationCountdown.days && expirationCountdown.days > 0) {
|
||||
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'days' });
|
||||
} else if (expirationCountdown.hours && expirationCountdown.hours > 0) {
|
||||
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'hours' });
|
||||
} else if (expirationCountdown.minutes && expirationCountdown.minutes > 0) {
|
||||
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'minutes' });
|
||||
} else if (expirationCountdown.seconds && expirationCountdown.seconds > 0) {
|
||||
return expiresAtDate.toRelativeCalendar({ base: now, locale: 'en-US', unit: 'seconds' });
|
||||
}
|
||||
};
|
||||
|
||||
const isExpired = (expiresAt: string) => {
|
||||
const now = new Date().getTime();
|
||||
const expiration = new Date(expiresAt).getTime();
|
||||
|
||||
return now > expiration;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full flex gap-4 dark:text-immich-gray transition-all border-b border-gray-200 dark:border-gray-600 hover:border-immich-primary dark:hover:border-immich-dark-primary py-4"
|
||||
>
|
||||
<div>
|
||||
{#await getAssetInfo()}
|
||||
<LoadingSpinner />
|
||||
{:then asset}
|
||||
<img
|
||||
id={asset.id}
|
||||
src={`/api/asset/thumbnail/${asset.id}?format=WEBP`}
|
||||
alt={asset.id}
|
||||
class="object-cover w-[100px] h-[100px] rounded-lg"
|
||||
loading="lazy"
|
||||
/>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between">
|
||||
<div class="info-top">
|
||||
<div class="text-xs font-mono font-semibold text-gray-500 dark:text-gray-400">
|
||||
{#if link.expiresAt}
|
||||
{#if isExpired(link.expiresAt)}
|
||||
<p class="text-red-600 dark:text-red-400 font-bold">Expired</p>
|
||||
{:else}
|
||||
<p>
|
||||
Expires {getCountDownExpirationDate()}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>Expires ∞</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-sm">
|
||||
<div
|
||||
class="flex gap-2 place-items-center text-immich-primary dark:text-immich-dark-primary"
|
||||
>
|
||||
{#if link.type === SharedLinkType.Album}
|
||||
<p>
|
||||
{link.album?.albumName.toUpperCase()}
|
||||
</p>
|
||||
{:else if link.type === SharedLinkType.Individual}
|
||||
<p>INDIVIDUAL SHARE</p>
|
||||
{/if}
|
||||
|
||||
{#if !link.expiresAt || !isExpired(link.expiresAt)}
|
||||
<div
|
||||
class="hover:cursor-pointer"
|
||||
title="Go to share page"
|
||||
on:click={() => goto(`/share/${link.key}`)}
|
||||
on:keydown={() => goto(`/share/${link.key}`)}
|
||||
>
|
||||
<OpenInNew />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-sm">{link.description ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-bottom">
|
||||
{#if link.allowUpload}
|
||||
<div
|
||||
class="text-xs px-2 py-1 bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-immich-dark-gray flex place-items-center place-content-center rounded-full w-[100px]"
|
||||
>
|
||||
Allow upload
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-auto flex flex-col place-content-center place-items-end text-right">
|
||||
<div class="flex">
|
||||
<CircleIconButton logo={Delete} on:click={() => dispatch('delete')} />
|
||||
<CircleIconButton logo={CircleEditOutline} on:click={() => dispatch('edit')} />
|
||||
<CircleIconButton logo={ContentCopy} on:click={() => dispatch('copy')} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,21 +1,106 @@
|
|||
import { api, AddAssetsResponseDto } from '@api';
|
||||
import { api, AddAssetsResponseDto, AssetResponseDto } from '@api';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { downloadAssets } from '$lib/stores/download';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const addAssetsToAlbum = async (
|
||||
albumId: string,
|
||||
assetIds: Array<string>
|
||||
assetIds: Array<string>,
|
||||
key: string | undefined = undefined
|
||||
): Promise<AddAssetsResponseDto> =>
|
||||
api.albumApi.addAssetsToAlbum(albumId, { assetIds }).then(({ data: dto }) => {
|
||||
if (dto.successfullyAdded > 0) {
|
||||
// This might be 0 if the user tries to add an asset that is already in the album
|
||||
notificationController.show({
|
||||
message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
api.albumApi
|
||||
.addAssetsToAlbum(albumId, { assetIds }, { params: { key } })
|
||||
.then(({ data: dto }) => {
|
||||
if (dto.successfullyAdded > 0) {
|
||||
// This might be 0 if the user tries to add an asset that is already in the album
|
||||
notificationController.show({
|
||||
message: `Added ${dto.successfullyAdded} to ${dto.album?.albumName}`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
|
||||
return dto;
|
||||
});
|
||||
return dto;
|
||||
});
|
||||
|
||||
export async function bulkDownload(
|
||||
fileName: string,
|
||||
assets: AssetResponseDto[],
|
||||
onDone: () => void,
|
||||
key?: string
|
||||
) {
|
||||
const assetIds = assets.map((asset) => asset.id);
|
||||
|
||||
try {
|
||||
let skip = 0;
|
||||
let count = 0;
|
||||
let done = false;
|
||||
|
||||
while (!done) {
|
||||
count++;
|
||||
|
||||
const downloadFileName = fileName + `${count === 1 ? '' : count}.zip`;
|
||||
downloadAssets.set({ [downloadFileName]: 0 });
|
||||
|
||||
let total = 0;
|
||||
|
||||
const { data, status, headers } = await api.assetApi.downloadFiles(
|
||||
{ assetIds },
|
||||
{
|
||||
params: { key },
|
||||
responseType: 'blob',
|
||||
onDownloadProgress: function (progressEvent) {
|
||||
const request = this as XMLHttpRequest;
|
||||
if (!total) {
|
||||
total = Number(request.getResponseHeader('X-Immich-Content-Length-Hint')) || 0;
|
||||
}
|
||||
|
||||
if (total) {
|
||||
const current = progressEvent.loaded;
|
||||
downloadAssets.set({ [downloadFileName]: Math.floor((current / total) * 100) });
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const isNotComplete = headers['x-immich-archive-complete'] === 'false';
|
||||
const fileCount = Number(headers['x-immich-archive-file-count']) || 0;
|
||||
if (isNotComplete && fileCount > 0) {
|
||||
skip += fileCount;
|
||||
} else {
|
||||
onDone();
|
||||
done = true;
|
||||
}
|
||||
|
||||
if (!(data instanceof Blob)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === 201) {
|
||||
const fileUrl = URL.createObjectURL(data);
|
||||
const anchor = document.createElement('a');
|
||||
anchor.href = fileUrl;
|
||||
anchor.download = downloadFileName;
|
||||
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
|
||||
// Remove item from download list
|
||||
setTimeout(() => {
|
||||
downloadAssets.set({});
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error downloading file ', e);
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
message: 'Error downloading file, check console for more details.'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
|||
|
||||
export const openFileUploadDialog = (
|
||||
albumId: string | undefined = undefined,
|
||||
sharedKey: string | undefined = undefined,
|
||||
callback?: () => void
|
||||
) => {
|
||||
try {
|
||||
|
@ -27,7 +28,7 @@ export const openFileUploadDialog = (
|
|||
}
|
||||
const files = Array.from<File>(target.files);
|
||||
|
||||
await fileUploadHandler(files, albumId);
|
||||
await fileUploadHandler(files, albumId, sharedKey);
|
||||
callback && callback();
|
||||
};
|
||||
|
||||
|
@ -37,7 +38,11 @@ export const openFileUploadDialog = (
|
|||
}
|
||||
};
|
||||
|
||||
export const fileUploadHandler = async (files: File[], albumId: string | undefined = undefined) => {
|
||||
export const fileUploadHandler = async (
|
||||
files: File[],
|
||||
albumId: string | undefined = undefined,
|
||||
sharedKey: string | undefined = undefined
|
||||
) => {
|
||||
if (files.length > 50) {
|
||||
notificationController.show({
|
||||
type: NotificationType.Error,
|
||||
|
@ -49,18 +54,22 @@ export const fileUploadHandler = async (files: File[], albumId: string | undefin
|
|||
|
||||
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);
|
||||
await fileUploader(asset, albumId, sharedKey);
|
||||
}
|
||||
};
|
||||
|
||||
//TODO: should probably use the @api SDK
|
||||
async function fileUploader(asset: File, albumId: string | undefined = undefined) {
|
||||
async function fileUploader(
|
||||
asset: File,
|
||||
albumId: string | undefined = undefined,
|
||||
sharedKey: string | undefined = undefined
|
||||
) {
|
||||
const assetType = asset.type.split('/')[0].toUpperCase();
|
||||
const temp = asset.name.split('.');
|
||||
const fileExtension = temp[temp.length - 1];
|
||||
|
@ -108,10 +117,17 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
|
|||
formData.append('assetData', asset);
|
||||
|
||||
// Check if asset upload on server before performing upload
|
||||
const { data, status } = await api.assetApi.checkDuplicateAsset({
|
||||
deviceAssetId: String(deviceAssetId),
|
||||
deviceId: 'WEB'
|
||||
});
|
||||
const { data, status } = await api.assetApi.checkDuplicateAsset(
|
||||
{
|
||||
deviceAssetId: String(deviceAssetId),
|
||||
deviceId: 'WEB'
|
||||
},
|
||||
{
|
||||
params: {
|
||||
key: sharedKey
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (status === 200) {
|
||||
if (data.isExist) {
|
||||
|
@ -124,7 +140,6 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
|
|||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
|
||||
request.upload.onloadstart = () => {
|
||||
const newUploadAsset: UploadAsset = {
|
||||
id: deviceAssetId,
|
||||
|
@ -144,7 +159,7 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
|
|||
try {
|
||||
const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
|
||||
if (res.id) {
|
||||
addAssetsToAlbum(albumId, [res.id]);
|
||||
addAssetsToAlbum(albumId, [res.id], sharedKey);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ERROR parsing data JSON in upload onload');
|
||||
|
@ -171,7 +186,7 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
|
|||
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
|
||||
};
|
||||
|
||||
request.open('POST', `/api/asset/upload`);
|
||||
request.open('POST', `/api/asset/upload?key=${sharedKey ?? ''}`);
|
||||
|
||||
request.send(formData);
|
||||
} catch (e) {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
} from '$lib/stores/asset-interaction.store';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import Close from 'svelte-material-icons/Close.svelte';
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
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';
|
||||
|
@ -26,7 +27,7 @@
|
|||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { assetStore } from '$lib/stores/assets.store';
|
||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
|
||||
import { addAssetsToAlbum, bulkDownload } from '$lib/utils/asset-utils';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
|
@ -106,6 +107,12 @@
|
|||
assetInteractionStore.clearMultiselect();
|
||||
});
|
||||
};
|
||||
|
||||
const handleDownloadFiles = async () => {
|
||||
await bulkDownload('immich', Array.from($selectedAssets), () => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
|
@ -125,6 +132,11 @@
|
|||
</p>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="trailing">
|
||||
<CircleIconButton
|
||||
title="Download"
|
||||
logo={CloudDownloadOutline}
|
||||
on:click={handleDownloadFiles}
|
||||
/>
|
||||
<CircleIconButton title="Add" logo={Plus} on:click={handleShowMenu} />
|
||||
<CircleIconButton
|
||||
title="Delete"
|
||||
|
|
9
web/src/routes/share/[key]/+error.svelte
Normal file
9
web/src/routes/share/[key]/+error.svelte
Normal file
|
@ -0,0 +1,9 @@
|
|||
<svelte:head>
|
||||
<title>Opps! Error - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-screen h-screen flex place-items-center place-content-center">
|
||||
<div class="p-20 text-4xl dark:text-immich-dark-primary text-immich-primary">
|
||||
Page not found :/
|
||||
</div>
|
||||
</section>
|
18
web/src/routes/share/[key]/+page.server.ts
Normal file
18
web/src/routes/share/[key]/+page.server.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
export const prerender = false;
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
import { serverApi } from '@api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const { key } = params;
|
||||
|
||||
try {
|
||||
const { data: sharedLink } = await serverApi.shareApi.getMySharedLink({ params: { key } });
|
||||
return { sharedLink };
|
||||
} catch (e) {
|
||||
throw error(404, {
|
||||
message: 'Invalid shared link'
|
||||
});
|
||||
}
|
||||
};
|
22
web/src/routes/share/[key]/+page.svelte
Normal file
22
web/src/routes/share/[key]/+page.svelte
Normal file
|
@ -0,0 +1,22 @@
|
|||
<script lang="ts">
|
||||
import AlbumViewer from '$lib/components/album-page/album-viewer.svelte';
|
||||
import { AlbumResponseDto } from '../../../api';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let album: AlbumResponseDto | null = null;
|
||||
if (data.sharedLink.album) {
|
||||
album = { ...data.sharedLink.album, assets: data.sharedLink.assets };
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{data.sharedLink.album?.albumName || 'Public Shared'} - Immich</title>
|
||||
</svelte:head>
|
||||
|
||||
{#if album}
|
||||
<div class="immich-scrollbar">
|
||||
<AlbumViewer {album} sharedLink={data.sharedLink} />
|
||||
</div>
|
||||
{/if}
|
21
web/src/routes/share/[key]/photos/[assetId]/+page.server.ts
Normal file
21
web/src/routes/share/[key]/photos/[assetId]/+page.server.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
export const prerender = false;
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
import { serverApi } from '@api';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
try {
|
||||
const { key, assetId } = params;
|
||||
const { data: asset } = await serverApi.assetApi.getAssetById(assetId, {
|
||||
params: { key }
|
||||
});
|
||||
|
||||
if (!asset) {
|
||||
return error(404, 'Asset not found');
|
||||
}
|
||||
return { asset, key };
|
||||
} catch (e) {
|
||||
console.log('Error', e);
|
||||
}
|
||||
};
|
17
web/src/routes/share/[key]/photos/[assetId]/+page.svelte
Normal file
17
web/src/routes/share/[key]/photos/[assetId]/+page.svelte
Normal file
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import AssetViewer from '$lib/components/asset-viewer/asset-viewer.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { goto } from '$app/navigation';
|
||||
export let data: PageData;
|
||||
</script>
|
||||
|
||||
{#if data.asset && data.key}
|
||||
<AssetViewer
|
||||
asset={data.asset}
|
||||
publicSharedKey={data.key}
|
||||
on:navigate-previous={() => null}
|
||||
on:navigate-next={() => null}
|
||||
showNavigation={false}
|
||||
on:close={() => goto(`/share/${data.key}`)}
|
||||
/>
|
||||
{/if}
|
|
@ -2,6 +2,8 @@
|
|||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import SideBar from '$lib/components/shared-components/side-bar/side-bar.svelte';
|
||||
import PlusBoxOutline from 'svelte-material-icons/PlusBoxOutline.svelte';
|
||||
import Link from 'svelte-material-icons/Link.svelte';
|
||||
|
||||
import SharedAlbumListTile from '$lib/components/sharing-page/shared-album-list-tile.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { api } from '@api';
|
||||
|
@ -55,7 +57,7 @@
|
|||
<p class="font-medium">Sharing</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="flex">
|
||||
<button
|
||||
on:click={createSharedAlbum}
|
||||
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700 dark:hover:bg-immich-dark-primary/25 dark:text-immich-dark-fg"
|
||||
|
@ -65,6 +67,16 @@
|
|||
</span>
|
||||
<p>Create shared album</p>
|
||||
</button>
|
||||
|
||||
<button
|
||||
on:click={() => goto('/sharing/sharedlinks')}
|
||||
class="flex place-items-center gap-1 text-sm hover:bg-immich-primary/5 p-2 rounded-lg font-medium hover:text-gray-700 dark:hover:bg-immich-dark-primary/25 dark:text-immich-dark-fg"
|
||||
>
|
||||
<span>
|
||||
<Link size="18" />
|
||||
</span>
|
||||
<p>Shared links</p>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
18
web/src/routes/sharing/sharedlinks/+page.server.ts
Normal file
18
web/src/routes/sharing/sharedlinks/+page.server.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { redirect } from '@sveltejs/kit';
|
||||
export const prerender = false;
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
try {
|
||||
const { user } = await parent();
|
||||
if (!user) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
return {
|
||||
user
|
||||
};
|
||||
} catch (e) {
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue