Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a25d359b7 | ||
|
|
7cfb257c00 | ||
|
|
b660240059 | ||
|
|
125ec1e85f | ||
|
|
d31b35873f | ||
|
|
e1c520b9e7 | ||
|
|
1361f18964 | ||
|
|
0f00f22212 | ||
|
|
0d543bbb0a | ||
|
|
86b3bdb90b | ||
|
|
d47cdfb647 |
@@ -36,7 +36,7 @@ platform :android do
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 70,
|
||||
"android.injected.version.name" => "1.47.2",
|
||||
"android.injected.version.name" => "1.47.3",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
* Adds over scroll at end of timeline to select images on the bottom
|
||||
* Fixes back navigation with tab controller
|
||||
* Shows a toast after adding to favorites
|
||||
* Fixed download button style
|
||||
* Cleaned up action bar, changed horizontal icon more to info icon
|
||||
* Responsive display of exif data in bottom sheet
|
||||
* Upgrade to Flutter 3.7
|
||||
* Fix freeze bug on app start
|
||||
* Uses profile photo for user avatar drawer
|
||||
* Spinning flower
|
||||
* Remove unsplash placeholder image and style empty places
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000281">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000209">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="142.850758">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="79.840593">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="39.589103">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="21.361905">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ SPEC CHECKSUMS:
|
||||
shared_preferences_foundation: 297b3ebca31b34ec92be11acd7fb0ba932c822ca
|
||||
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: ae1517e5e344f5544fb090b079e11f399dfbe4d2
|
||||
url_launcher_ios: fb12c43172927bb5cf75aeebd073f883801f1993
|
||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -498,7 +498,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -526,7 +526,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
CURRENT_PROJECT_VERSION = 86;
|
||||
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<string>1.47.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<string>86</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.47.2"
|
||||
version_number: "1.47.3"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000283">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000269">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.755864">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="3.705535">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="7.319767">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.23144">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.376562">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.423549">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="105.396514">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="98.940158">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="86.092896">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="64.950609">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
3
mobile/openapi/.openapi-generator/FILES
generated
3
mobile/openapi/.openapi-generator/FILES
generated
@@ -86,7 +86,6 @@ doc/ThumbnailFormat.md
|
||||
doc/TimeGroupEnum.md
|
||||
doc/UpdateAlbumDto.md
|
||||
doc/UpdateAssetDto.md
|
||||
doc/UpdateAssetsToSharedLinkDto.md
|
||||
doc/UpdateTagDto.md
|
||||
doc/UpdateUserDto.md
|
||||
doc/UpsertDeviceInfoDto.md
|
||||
@@ -189,7 +188,6 @@ lib/model/thumbnail_format.dart
|
||||
lib/model/time_group_enum.dart
|
||||
lib/model/update_album_dto.dart
|
||||
lib/model/update_asset_dto.dart
|
||||
lib/model/update_assets_to_shared_link_dto.dart
|
||||
lib/model/update_tag_dto.dart
|
||||
lib/model/update_user_dto.dart
|
||||
lib/model/upsert_device_info_dto.dart
|
||||
@@ -281,7 +279,6 @@ test/thumbnail_format_test.dart
|
||||
test/time_group_enum_test.dart
|
||||
test/update_album_dto_test.dart
|
||||
test/update_asset_dto_test.dart
|
||||
test/update_assets_to_shared_link_dto_test.dart
|
||||
test/update_tag_dto_test.dart
|
||||
test/update_user_dto_test.dart
|
||||
test/upsert_device_info_dto_test.dart
|
||||
|
||||
6
mobile/openapi/README.md
generated
6
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.46.1
|
||||
- API version: 1.47.2
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -75,6 +75,7 @@ Class | Method | HTTP request | Description
|
||||
*AlbumApi* | [**removeAssetFromAlbum**](doc//AlbumApi.md#removeassetfromalbum) | **DELETE** /album/{albumId}/assets |
|
||||
*AlbumApi* | [**removeUserFromAlbum**](doc//AlbumApi.md#removeuserfromalbum) | **DELETE** /album/{albumId}/user/{userId} |
|
||||
*AlbumApi* | [**updateAlbumInfo**](doc//AlbumApi.md#updatealbuminfo) | **PATCH** /album/{albumId} |
|
||||
*AssetApi* | [**addAssetsToSharedLink**](doc//AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
|
||||
*AssetApi* | [**checkDuplicateAsset**](doc//AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
||||
*AssetApi* | [**checkExistingAssets**](doc//AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
*AssetApi* | [**createAssetsSharedLink**](doc//AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
|
||||
@@ -92,10 +93,10 @@ Class | Method | HTTP request | Description
|
||||
*AssetApi* | [**getCuratedLocations**](doc//AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||
*AssetApi* | [**getCuratedObjects**](doc//AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
||||
*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||
*AssetApi* | [**removeAssetsFromSharedLink**](doc//AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
||||
*AssetApi* | [**searchAsset**](doc//AssetApi.md#searchasset) | **POST** /asset/search |
|
||||
*AssetApi* | [**serveFile**](doc//AssetApi.md#servefile) | **GET** /asset/file/{assetId} |
|
||||
*AssetApi* | [**updateAsset**](doc//AssetApi.md#updateasset) | **PUT** /asset/{assetId} |
|
||||
*AssetApi* | [**updateAssetsInSharedLink**](doc//AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link |
|
||||
*AssetApi* | [**uploadFile**](doc//AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||
*AuthenticationApi* | [**adminSignUp**](doc//AuthenticationApi.md#adminsignup) | **POST** /auth/admin-sign-up |
|
||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password |
|
||||
@@ -214,7 +215,6 @@ Class | Method | HTTP request | Description
|
||||
- [TimeGroupEnum](doc//TimeGroupEnum.md)
|
||||
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
|
||||
- [UpdateAssetDto](doc//UpdateAssetDto.md)
|
||||
- [UpdateAssetsToSharedLinkDto](doc//UpdateAssetsToSharedLinkDto.md)
|
||||
- [UpdateTagDto](doc//UpdateTagDto.md)
|
||||
- [UpdateUserDto](doc//UpdateUserDto.md)
|
||||
- [UpsertDeviceInfoDto](doc//UpsertDeviceInfoDto.md)
|
||||
|
||||
150
mobile/openapi/doc/AssetApi.md
generated
150
mobile/openapi/doc/AssetApi.md
generated
@@ -9,6 +9,7 @@ All URIs are relative to */api*
|
||||
|
||||
Method | HTTP request | Description
|
||||
------------- | ------------- | -------------
|
||||
[**addAssetsToSharedLink**](AssetApi.md#addassetstosharedlink) | **PATCH** /asset/shared-link/add |
|
||||
[**checkDuplicateAsset**](AssetApi.md#checkduplicateasset) | **POST** /asset/check |
|
||||
[**checkExistingAssets**](AssetApi.md#checkexistingassets) | **POST** /asset/exist |
|
||||
[**createAssetsSharedLink**](AssetApi.md#createassetssharedlink) | **POST** /asset/shared-link |
|
||||
@@ -26,13 +27,62 @@ Method | HTTP request | Description
|
||||
[**getCuratedLocations**](AssetApi.md#getcuratedlocations) | **GET** /asset/curated-locations |
|
||||
[**getCuratedObjects**](AssetApi.md#getcuratedobjects) | **GET** /asset/curated-objects |
|
||||
[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} |
|
||||
[**removeAssetsFromSharedLink**](AssetApi.md#removeassetsfromsharedlink) | **PATCH** /asset/shared-link/remove |
|
||||
[**searchAsset**](AssetApi.md#searchasset) | **POST** /asset/search |
|
||||
[**serveFile**](AssetApi.md#servefile) | **GET** /asset/file/{assetId} |
|
||||
[**updateAsset**](AssetApi.md#updateasset) | **PUT** /asset/{assetId} |
|
||||
[**updateAssetsInSharedLink**](AssetApi.md#updateassetsinsharedlink) | **PATCH** /asset/shared-link |
|
||||
[**uploadFile**](AssetApi.md#uploadfile) | **POST** /asset/upload |
|
||||
|
||||
|
||||
# **addAssetsToSharedLink**
|
||||
> SharedLinkResponseDto addAssetsToSharedLink(addAssetsDto)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 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 addAssetsDto = AddAssetsDto(); // AddAssetsDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.addAssetsToSharedLink(addAssetsDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->addAssetsToSharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**addAssetsDto** | [**AddAssetsDto**](AddAssetsDto.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)
|
||||
|
||||
# **checkDuplicateAsset**
|
||||
> CheckDuplicateAssetResponseDto checkDuplicateAsset(checkDuplicateAssetDto)
|
||||
|
||||
@@ -856,6 +906,55 @@ 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)
|
||||
|
||||
# **removeAssetsFromSharedLink**
|
||||
> SharedLinkResponseDto removeAssetsFromSharedLink(removeAssetsDto)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### 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 removeAssetsDto = RemoveAssetsDto(); // RemoveAssetsDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.removeAssetsFromSharedLink(removeAssetsDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->removeAssetsFromSharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**removeAssetsDto** | [**RemoveAssetsDto**](RemoveAssetsDto.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)
|
||||
|
||||
# **searchAsset**
|
||||
> List<AssetResponseDto> searchAsset(searchAssetDto)
|
||||
|
||||
@@ -1009,55 +1108,6 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **updateAssetsInSharedLink**
|
||||
> SharedLinkResponseDto updateAssetsInSharedLink(updateAssetsToSharedLinkDto)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = AssetApi();
|
||||
final updateAssetsToSharedLinkDto = UpdateAssetsToSharedLinkDto(); // UpdateAssetsToSharedLinkDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.updateAssetsInSharedLink(updateAssetsToSharedLinkDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling AssetApi->updateAssetsInSharedLink: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**updateAssetsToSharedLinkDto** | [**UpdateAssetsToSharedLinkDto**](UpdateAssetsToSharedLinkDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**SharedLinkResponseDto**](SharedLinkResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **uploadFile**
|
||||
> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration)
|
||||
|
||||
|
||||
15
mobile/openapi/doc/UpdateAssetsToSharedLinkDto.md
generated
15
mobile/openapi/doc/UpdateAssetsToSharedLinkDto.md
generated
@@ -1,15 +0,0 @@
|
||||
# openapi.model.UpdateAssetsToSharedLinkDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**assetIds** | **List<String>** | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -113,7 +113,6 @@ part 'model/thumbnail_format.dart';
|
||||
part 'model/time_group_enum.dart';
|
||||
part 'model/update_album_dto.dart';
|
||||
part 'model/update_asset_dto.dart';
|
||||
part 'model/update_assets_to_shared_link_dto.dart';
|
||||
part 'model/update_tag_dto.dart';
|
||||
part 'model/update_user_dto.dart';
|
||||
part 'model/upsert_device_info_dto.dart';
|
||||
|
||||
156
mobile/openapi/lib/api/asset_api.dart
generated
156
mobile/openapi/lib/api/asset_api.dart
generated
@@ -16,6 +16,58 @@ class AssetApi {
|
||||
|
||||
final ApiClient apiClient;
|
||||
|
||||
///
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [AddAssetsDto] addAssetsDto (required):
|
||||
Future<Response> addAssetsToSharedLinkWithHttpInfo(AddAssetsDto addAssetsDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/shared-link/add';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = addAssetsDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [AddAssetsDto] addAssetsDto (required):
|
||||
Future<SharedLinkResponseDto?> addAssetsToSharedLink(AddAssetsDto addAssetsDto,) async {
|
||||
final response = await addAssetsToSharedLinkWithHttpInfo(addAssetsDto,);
|
||||
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;
|
||||
}
|
||||
|
||||
/// Check duplicated asset before uploading - for Web upload used
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
@@ -926,6 +978,58 @@ class AssetApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [RemoveAssetsDto] removeAssetsDto (required):
|
||||
Future<Response> removeAssetsFromSharedLinkWithHttpInfo(RemoveAssetsDto removeAssetsDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/shared-link/remove';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = removeAssetsDto;
|
||||
|
||||
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:
|
||||
///
|
||||
/// * [RemoveAssetsDto] removeAssetsDto (required):
|
||||
Future<SharedLinkResponseDto?> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto,) async {
|
||||
final response = await removeAssetsFromSharedLinkWithHttpInfo(removeAssetsDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
@@ -1106,58 +1210,6 @@ class AssetApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required):
|
||||
Future<Response> updateAssetsInSharedLinkWithHttpInfo(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/asset/shared-link';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = updateAssetsToSharedLinkDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PATCH',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [UpdateAssetsToSharedLinkDto] updateAssetsToSharedLinkDto (required):
|
||||
Future<SharedLinkResponseDto?> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto,) async {
|
||||
final response = await updateAssetsInSharedLinkWithHttpInfo(updateAssetsToSharedLinkDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SharedLinkResponseDto',) as SharedLinkResponseDto;
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
///
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -336,8 +336,6 @@ class ApiClient {
|
||||
return UpdateAlbumDto.fromJson(value);
|
||||
case 'UpdateAssetDto':
|
||||
return UpdateAssetDto.fromJson(value);
|
||||
case 'UpdateAssetsToSharedLinkDto':
|
||||
return UpdateAssetsToSharedLinkDto.fromJson(value);
|
||||
case 'UpdateTagDto':
|
||||
return UpdateTagDto.fromJson(value);
|
||||
case 'UpdateUserDto':
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class UpdateAssetsToSharedLinkDto {
|
||||
/// Returns a new [UpdateAssetsToSharedLinkDto] instance.
|
||||
UpdateAssetsToSharedLinkDto({
|
||||
this.assetIds = const [],
|
||||
});
|
||||
|
||||
List<String> assetIds;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetsToSharedLinkDto &&
|
||||
other.assetIds == assetIds;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(assetIds.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'UpdateAssetsToSharedLinkDto[assetIds=$assetIds]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'assetIds'] = this.assetIds;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [UpdateAssetsToSharedLinkDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static UpdateAssetsToSharedLinkDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
// Ensure that the map contains the required keys.
|
||||
// Note 1: the values aren't checked for validity beyond being non-null.
|
||||
// Note 2: this code is stripped in release mode!
|
||||
assert(() {
|
||||
requiredKeys.forEach((key) {
|
||||
assert(json.containsKey(key), 'Required key "UpdateAssetsToSharedLinkDto[$key]" is missing from JSON.');
|
||||
assert(json[key] != null, 'Required key "UpdateAssetsToSharedLinkDto[$key]" has a null value in JSON.');
|
||||
});
|
||||
return true;
|
||||
}());
|
||||
|
||||
return UpdateAssetsToSharedLinkDto(
|
||||
assetIds: json[r'assetIds'] is List
|
||||
? (json[r'assetIds'] as List).cast<String>()
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<UpdateAssetsToSharedLinkDto>? listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <UpdateAssetsToSharedLinkDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = UpdateAssetsToSharedLinkDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, UpdateAssetsToSharedLinkDto> mapFromJson(dynamic json) {
|
||||
final map = <String, UpdateAssetsToSharedLinkDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UpdateAssetsToSharedLinkDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of UpdateAssetsToSharedLinkDto-objects as value to a dart map
|
||||
static Map<String, List<UpdateAssetsToSharedLinkDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<UpdateAssetsToSharedLinkDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = UpdateAssetsToSharedLinkDto.listFromJson(entry.value, growable: growable,);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'assetIds',
|
||||
};
|
||||
}
|
||||
|
||||
21
mobile/openapi/test/asset_api_test.dart
generated
21
mobile/openapi/test/asset_api_test.dart
generated
@@ -17,6 +17,13 @@ void main() {
|
||||
// final instance = AssetApi();
|
||||
|
||||
group('tests for AssetApi', () {
|
||||
//
|
||||
//
|
||||
//Future<SharedLinkResponseDto> addAssetsToSharedLink(AddAssetsDto addAssetsDto) async
|
||||
test('test addAssetsToSharedLink', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// Check duplicated asset before uploading - for Web upload used
|
||||
//
|
||||
//Future<CheckDuplicateAssetResponseDto> checkDuplicateAsset(CheckDuplicateAssetDto checkDuplicateAssetDto) async
|
||||
@@ -136,6 +143,13 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//
|
||||
//
|
||||
//Future<SharedLinkResponseDto> removeAssetsFromSharedLink(RemoveAssetsDto removeAssetsDto) async
|
||||
test('test removeAssetsFromSharedLink', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//
|
||||
//
|
||||
//Future<List<AssetResponseDto>> searchAsset(SearchAssetDto searchAssetDto) async
|
||||
@@ -157,13 +171,6 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//
|
||||
//
|
||||
//Future<SharedLinkResponseDto> updateAssetsInSharedLink(UpdateAssetsToSharedLinkDto updateAssetsToSharedLinkDto) async
|
||||
test('test updateAssetsInSharedLink', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//
|
||||
//
|
||||
//Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile livePhotoData, bool isVisible, String duration }) async
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for UpdateAssetsToSharedLinkDto
|
||||
void main() {
|
||||
// final instance = UpdateAssetsToSharedLinkDto();
|
||||
|
||||
group('test UpdateAssetsToSharedLinkDto', () {
|
||||
// List<String> assetIds (default value: const [])
|
||||
test('to test the property `assetIds`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.46.0+69
|
||||
version: 1.47.0+70
|
||||
isar_version: &isar_version 3.0.5
|
||||
|
||||
environment:
|
||||
|
||||
44
nginx/10-listen-on-ipv6-by-default.sh
Executable file
44
nginx/10-listen-on-ipv6-by-default.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/bin/sh
|
||||
# vim:sw=4:ts=4:et
|
||||
|
||||
set -e
|
||||
|
||||
entrypoint_log() {
|
||||
if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then
|
||||
echo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
ME=$(basename $0)
|
||||
DEFAULT_CONF_FILE="etc/nginx/conf.d/default.conf"
|
||||
|
||||
# check if we have ipv6 available
|
||||
if [ ! -f "/proc/net/if_inet6" ]; then
|
||||
entrypoint_log "$ME: info: ipv6 not available"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -f "/$DEFAULT_CONF_FILE" ]; then
|
||||
entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE is not a file or does not exist"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# check if the file can be modified, e.g. not on a r/o filesystem
|
||||
touch /$DEFAULT_CONF_FILE 2>/dev/null || { entrypoint_log "$ME: info: can not modify /$DEFAULT_CONF_FILE (read-only file system?)"; exit 0; }
|
||||
|
||||
# check if the file is already modified, e.g. on a container restart
|
||||
grep -q "listen \[::]\:8080;" /$DEFAULT_CONF_FILE && { entrypoint_log "$ME: info: IPv6 listen already enabled"; exit 0; }
|
||||
|
||||
if [ -f "/etc/os-release" ]; then
|
||||
. /etc/os-release
|
||||
else
|
||||
entrypoint_log "$ME: info: can not guess the operating system"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# enable ipv6 on default.conf listen sockets
|
||||
sed -i -E 's,listen 8080;,listen 8080;\n listen [::]:8080;,' /$DEFAULT_CONF_FILE
|
||||
|
||||
entrypoint_log "$ME: info: Enabled listen on IPv6 in /$DEFAULT_CONF_FILE"
|
||||
|
||||
exit 0
|
||||
@@ -1,4 +1,4 @@
|
||||
#! /bin/sh
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
export IMMICH_WEB_URL="${IMMICH_WEB_URL:-http://immich-web:3000}"
|
||||
@@ -11,7 +11,3 @@ IMMICH_SERVER_SCHEME=$(echo "$IMMICH_WEB_URL" | grep -Eo '^https?://' || echo "h
|
||||
export IMMICH_SERVER_SCHEME
|
||||
IMMICH_SERVER_HOST=$(echo "$IMMICH_SERVER_URL" | cut -d '/' -f 3)
|
||||
export IMMICH_SERVER_HOST
|
||||
|
||||
envsubst '$IMMICH_WEB_SCHEME $IMMICH_WEB_HOST $IMMICH_SERVER_SCHEME $IMMICH_SERVER_HOST' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
exec nginx -g 'daemon off;'
|
||||
@@ -3,9 +3,7 @@ FROM docker.io/nginxinc/nginx-unprivileged:latest
|
||||
COPY LICENSE /licenses/LICENSE.txt
|
||||
COPY LICENSE /LICENSE
|
||||
|
||||
COPY nginx.conf "/etc/nginx/nginx.conf.template"
|
||||
COPY start.sh /start.sh
|
||||
COPY 10-listen-on-ipv6-by-default.sh /docker-entrypoint.d
|
||||
COPY 15-set-env-variables.envsh /docker-entrypoint.d
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
ENTRYPOINT ["/start.sh"]
|
||||
COPY templates/ /etc/nginx/templates
|
||||
|
||||
104
nginx/nginx.conf
104
nginx/nginx.conf
@@ -1,104 +0,0 @@
|
||||
# NOTE: This file is generated on startup. See /start.sh
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log;
|
||||
pid /tmp/nginx.pid;
|
||||
|
||||
# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic.
|
||||
include /usr/share/nginx/modules/*.conf;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp_path;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
# events {
|
||||
# worker_connections 1000;
|
||||
# }
|
||||
|
||||
upstream server {
|
||||
server $IMMICH_SERVER_HOST;
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
upstream web {
|
||||
server $IMMICH_WEB_HOST;
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
server {
|
||||
|
||||
# Compression
|
||||
gzip on;
|
||||
gzip_comp_level 2;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied any;
|
||||
gzip_types
|
||||
application/javascript
|
||||
application/json
|
||||
font/truetype
|
||||
image/svg+xml
|
||||
text/css
|
||||
text/html;
|
||||
gzip_vary on;
|
||||
gunzip on;
|
||||
|
||||
client_max_body_size 50000M;
|
||||
|
||||
listen 8080;
|
||||
|
||||
access_log off;
|
||||
|
||||
location /api {
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
proxy_buffers 64 4k;
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
rewrite /api/(.*) /$1 break;
|
||||
|
||||
proxy_pass ${IMMICH_SERVER_SCHEME}server;
|
||||
}
|
||||
|
||||
location / {
|
||||
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
proxy_buffers 64 4k;
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass ${IMMICH_WEB_SCHEME}web;
|
||||
}
|
||||
}
|
||||
}
|
||||
77
nginx/templates/default.conf.template
Normal file
77
nginx/templates/default.conf.template
Normal file
@@ -0,0 +1,77 @@
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
|
||||
upstream server {
|
||||
server ${IMMICH_SERVER_HOST};
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
upstream web {
|
||||
server ${IMMICH_WEB_HOST};
|
||||
keepalive 2;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
|
||||
access_log off;
|
||||
client_max_body_size 50000M;
|
||||
|
||||
# Compression
|
||||
gzip off;
|
||||
gzip_comp_level 2;
|
||||
gzip_min_length 1000;
|
||||
gzip_proxied any;
|
||||
gzip_vary on;
|
||||
gunzip on;
|
||||
|
||||
# text/html is included by default
|
||||
gzip_types
|
||||
application/javascript
|
||||
application/json
|
||||
font/ttf
|
||||
image/svg+xml
|
||||
text/css;
|
||||
|
||||
location /api {
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
proxy_buffers 64 4k;
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
rewrite /api/(.*) /$1 break;
|
||||
|
||||
proxy_pass ${IMMICH_SERVER_SCHEME}server;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_buffering off;
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k;
|
||||
proxy_buffers 64 4k;
|
||||
proxy_force_ranges on;
|
||||
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
proxy_pass ${IMMICH_WEB_SCHEME}web;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AddAssetsDto } from './../album/dto/add-assets.dto';
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -52,10 +53,10 @@ import {
|
||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||
import { SharedLinkResponseDto } from '@app/domain';
|
||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
||||
import FileNotEmptyValidator from '../validation/file-not-empty-validator';
|
||||
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
|
||||
|
||||
function asStreamableFile({ stream, type, length }: ImmichReadStream) {
|
||||
return new StreamableFile(stream, { type, length });
|
||||
@@ -330,11 +331,20 @@ export class AssetController {
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Patch('/shared-link')
|
||||
async updateAssetsInSharedLink(
|
||||
@Patch('/shared-link/add')
|
||||
async addAssetsToSharedLink(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) dto: UpdateAssetsToSharedLinkDto,
|
||||
@Body(ValidationPipe) dto: AddAssetsDto,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
return await this.assetService.updateAssetsInSharedLink(authUser, dto);
|
||||
return await this.assetService.addAssetsToSharedLink(authUser, dto);
|
||||
}
|
||||
|
||||
@Authenticated({ isShared: true })
|
||||
@Patch('/shared-link/remove')
|
||||
async removeAssetsFromSharedLink(
|
||||
@GetAuthUser() authUser: AuthUserDto,
|
||||
@Body(ValidationPipe) dto: RemoveAssetsDto,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
return await this.assetService.removeAssetsFromSharedLink(authUser, dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +198,31 @@ describe('AssetService', () => {
|
||||
sharedLinkRepositoryMock.get.mockResolvedValue(null);
|
||||
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
await expect(sut.addAssetsToSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||
|
||||
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
|
||||
expect(sharedLinkRepositoryMock.save).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add assets to a shared link', async () => {
|
||||
const asset1 = _getAsset_1();
|
||||
|
||||
const authDto = authStub.adminSharedLink;
|
||||
const dto = { assetIds: [asset1.id] };
|
||||
|
||||
assetRepositoryMock.getById.mockResolvedValue(asset1);
|
||||
sharedLinkRepositoryMock.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
||||
|
||||
await expect(sut.addAssetsToSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
|
||||
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove assets from a shared link', async () => {
|
||||
const asset1 = _getAsset_1();
|
||||
|
||||
@@ -217,11 +234,11 @@ describe('AssetService', () => {
|
||||
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
||||
|
||||
await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
await expect(sut.removeAssetsFromSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
|
||||
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||
expect(sharedLinkRepositoryMock.hasAssetAccess).toHaveBeenCalledWith(authDto.sharedLinkId, asset1.id);
|
||||
expect(sharedLinkRepositoryMock.save).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -58,8 +58,9 @@ import { ISharedLinkRepository } from '@app/domain';
|
||||
import { DownloadFilesDto } from './dto/download-files.dto';
|
||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
|
||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||
import { AddAssetsDto } from '../album/dto/add-assets.dto';
|
||||
import { RemoveAssetsDto } from '../album/dto/remove-assets.dto';
|
||||
|
||||
const fileInfo = promisify(stat);
|
||||
|
||||
@@ -606,23 +607,35 @@ export class AssetService {
|
||||
return mapSharedLink(sharedLink);
|
||||
}
|
||||
|
||||
async updateAssetsInSharedLink(
|
||||
authUser: AuthUserDto,
|
||||
dto: UpdateAssetsToSharedLinkDto,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
async addAssetsToSharedLink(authUser: AuthUserDto, dto: AddAssetsDto): Promise<SharedLinkResponseDto> {
|
||||
if (!authUser.sharedLinkId) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const assets = [];
|
||||
|
||||
await this.checkAssetsAccess(authUser, dto.assetIds);
|
||||
for (const assetId of dto.assetIds) {
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
assets.push(asset);
|
||||
}
|
||||
|
||||
const updatedLink = await this.shareCore.updateAssets(authUser.id, authUser.sharedLinkId, assets);
|
||||
const updatedLink = await this.shareCore.addAssets(authUser.id, authUser.sharedLinkId, assets);
|
||||
return mapSharedLink(updatedLink);
|
||||
}
|
||||
|
||||
async removeAssetsFromSharedLink(authUser: AuthUserDto, dto: RemoveAssetsDto): Promise<SharedLinkResponseDto> {
|
||||
if (!authUser.sharedLinkId) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const assets = [];
|
||||
|
||||
for (const assetId of dto.assetIds) {
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
assets.push(asset);
|
||||
}
|
||||
|
||||
const updatedLink = await this.shareCore.removeAssets(authUser.id, authUser.sharedLinkId, assets);
|
||||
return mapSharedLink(updatedLink);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
|
||||
export class UpdateAssetsToSharedLinkDto {
|
||||
@IsNotEmpty()
|
||||
assetIds!: string[];
|
||||
}
|
||||
@@ -55,7 +55,7 @@ describe('assetUploadOption', () => {
|
||||
});
|
||||
|
||||
it('should allow videos', async () => {
|
||||
const file = { mimetype: 'image/mp4', originalname: 'test.mp4' } as any;
|
||||
const file = { mimetype: 'video/mp4', originalname: 'test.mp4' } as any;
|
||||
fileFilter(mock.userRequest, file, callback);
|
||||
expect(callback).toHaveBeenCalledWith(null, true);
|
||||
});
|
||||
@@ -66,6 +66,18 @@ describe('assetUploadOption', () => {
|
||||
expect(callback).toHaveBeenCalledWith(null, true);
|
||||
});
|
||||
|
||||
it('should allow .raf recognized', () => {
|
||||
const file = { mimetype: 'image/x-fuji-raf', originalname: 'test.raf' } as any;
|
||||
fileFilter(mock.userRequest, file, callback);
|
||||
expect(callback).toHaveBeenCalledWith(null, true);
|
||||
});
|
||||
|
||||
it('should allow .srw recognized', () => {
|
||||
const file = { mimetype: 'image/x-samsung-srw', originalname: 'test.srw' } as any;
|
||||
fileFilter(mock.userRequest, file, callback);
|
||||
expect(callback).toHaveBeenCalledWith(null, true);
|
||||
});
|
||||
|
||||
it('should not allow unknown types', async () => {
|
||||
const file = { mimetype: 'application/html', originalname: 'test.html' } as any;
|
||||
const callback = jest.fn();
|
||||
|
||||
@@ -10,8 +10,6 @@ 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');
|
||||
|
||||
export interface ImmichFile extends Express.Multer.File {
|
||||
/** sha1 hash of file */
|
||||
checksum: Buffer;
|
||||
@@ -48,13 +46,15 @@ export function customStorage(): StorageEngine {
|
||||
|
||||
export const multerUtils = { fileFilter, filename, destination };
|
||||
|
||||
const logger = new Logger('AssetUploadConfig');
|
||||
|
||||
function fileFilter(req: Request, file: any, cb: any) {
|
||||
if (!req.user || (req.user.isPublicUser && !req.user.isAllowUpload)) {
|
||||
return cb(new UnauthorizedException());
|
||||
}
|
||||
if (
|
||||
file.mimetype.match(
|
||||
/\/(jpg|jpeg|png|gif|mp4|webm|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff|3gpp|nef|x-nikon-nef)$/,
|
||||
/\/(jpg|jpeg|png|gif|mp4|webm|x-msvideo|quicktime|heic|heif|dng|x-adobe-dng|webp|tiff|3gpp|nef|x-nikon-nef|x-fuji-raf|x-samsung-srw)$/,
|
||||
)
|
||||
) {
|
||||
cb(null, true);
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Repository } from 'typeorm/repository/Repository';
|
||||
import { join } from 'path';
|
||||
import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway';
|
||||
import { IMachineLearningJob } from '@app/domain';
|
||||
import { exiftool } from 'exiftool-vendored';
|
||||
|
||||
@Processor(QueueName.THUMBNAIL_GENERATION)
|
||||
export class ThumbnailGeneratorProcessor {
|
||||
@@ -53,7 +54,15 @@ export class ThumbnailGeneratorProcessor {
|
||||
.resize(1440, 1440, { fit: 'outside', withoutEnlargement: true })
|
||||
.jpeg()
|
||||
.rotate()
|
||||
.toFile(jpegThumbnailPath);
|
||||
.toFile(jpegThumbnailPath)
|
||||
.catch(() => {
|
||||
this.logger.warn(
|
||||
'Failed to generate jpeg thumbnail for asset: ' +
|
||||
asset.id +
|
||||
' using sharp, failing over to exiftool-vendored',
|
||||
);
|
||||
return exiftool.extractThumbnail(asset.originalPath, jpegThumbnailPath);
|
||||
});
|
||||
await this.assetRepository.update({ id: asset.id }, { resizePath: jpegThumbnailPath });
|
||||
} catch (error: any) {
|
||||
this.logger.error('Failed to generate jpeg thumbnail for asset: ' + asset.id, error.stack);
|
||||
|
||||
@@ -1869,9 +1869,11 @@
|
||||
"bearer": []
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
"/asset/shared-link/add": {
|
||||
"patch": {
|
||||
"operationId": "updateAssetsInSharedLink",
|
||||
"operationId": "addAssetsToSharedLink",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
@@ -1879,7 +1881,44 @@
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateAssetsToSharedLinkDto"
|
||||
"$ref": "#/components/schemas/AddAssetsDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Asset"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/asset/shared-link/remove": {
|
||||
"patch": {
|
||||
"operationId": "removeAssetsFromSharedLink",
|
||||
"description": "",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/RemoveAssetsDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2689,7 +2728,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.47.2",
|
||||
"version": "1.47.3",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -4171,7 +4210,21 @@
|
||||
"assetIds"
|
||||
]
|
||||
},
|
||||
"UpdateAssetsToSharedLinkDto": {
|
||||
"AddAssetsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetIds"
|
||||
]
|
||||
},
|
||||
"RemoveAssetsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
@@ -4267,20 +4320,6 @@
|
||||
"sharedUserIds"
|
||||
]
|
||||
},
|
||||
"AddAssetsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetIds"
|
||||
]
|
||||
},
|
||||
"AddAssetsResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -4302,20 +4341,6 @@
|
||||
"alreadyInAlbum"
|
||||
]
|
||||
},
|
||||
"RemoveAssetsDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"assetIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"assetIds"
|
||||
]
|
||||
},
|
||||
"UpdateAlbumDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -63,13 +63,24 @@ export class ShareCore {
|
||||
return this.repository.remove(link);
|
||||
}
|
||||
|
||||
async updateAssets(userId: string, id: string, assets: AssetEntity[]) {
|
||||
async addAssets(userId: string, id: string, assets: AssetEntity[]) {
|
||||
const link = await this.get(userId, id);
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
return this.repository.save({ ...link, assets });
|
||||
return this.repository.save({ ...link, assets: [...link.assets, ...assets] });
|
||||
}
|
||||
|
||||
async removeAssets(userId: string, id: string, assets: AssetEntity[]) {
|
||||
const link = await this.get(userId, id);
|
||||
if (!link) {
|
||||
throw new BadRequestException('Shared link not found');
|
||||
}
|
||||
|
||||
const newAssets = link.assets.filter((asset) => assets.find((a) => a.id === asset.id));
|
||||
|
||||
return this.repository.save({ ...link, assets: newAssets });
|
||||
}
|
||||
|
||||
async hasAssetAccess(id: string, assetId: string): Promise<boolean> {
|
||||
|
||||
2
server/package-lock.json
generated
2
server/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.47.2",
|
||||
"version": "1.47.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.47.2",
|
||||
"version": "1.47.3",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -140,9 +140,9 @@
|
||||
},
|
||||
"./libs/domain/": {
|
||||
"branches": 80,
|
||||
"functions": 89,
|
||||
"functions": 88,
|
||||
"lines": 95,
|
||||
"statements": 95
|
||||
"statements": 94
|
||||
}
|
||||
},
|
||||
"testEnvironment": "node",
|
||||
|
||||
222
web/src/api/open-api/api.ts
generated
222
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.46.1
|
||||
* The version of the OpenAPI document: 1.47.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -2083,19 +2083,6 @@ export interface UpdateAssetDto {
|
||||
*/
|
||||
'isFavorite'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface UpdateAssetsToSharedLinkDto
|
||||
*/
|
||||
export interface UpdateAssetsToSharedLinkDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<string>}
|
||||
* @memberof UpdateAssetsToSharedLinkDto
|
||||
*/
|
||||
'assetIds': Array<string>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -3588,6 +3575,45 @@ export class AlbumApi extends BaseAPI {
|
||||
*/
|
||||
export const AssetApiAxiosParamCreator = function (configuration?: Configuration) {
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {AddAssetsDto} addAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
addAssetsToSharedLink: async (addAssetsDto: AddAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'addAssetsDto' is not null or undefined
|
||||
assertParamExists('addAssetsToSharedLink', 'addAssetsDto', addAssetsDto)
|
||||
const localVarPath = `/asset/shared-link/add`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(addAssetsDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@@ -4232,6 +4258,45 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {RemoveAssetsDto} removeAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
removeAssetsFromSharedLink: async (removeAssetsDto: RemoveAssetsDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'removeAssetsDto' is not null or undefined
|
||||
assertParamExists('removeAssetsFromSharedLink', 'removeAssetsDto', removeAssetsDto)
|
||||
const localVarPath = `/asset/shared-link/remove`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(removeAssetsDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -4361,45 +4426,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updateAssetsInSharedLink: async (updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'updateAssetsToSharedLinkDto' is not null or undefined
|
||||
assertParamExists('updateAssetsInSharedLink', 'updateAssetsToSharedLinkDto', updateAssetsToSharedLinkDto)
|
||||
const localVarPath = `/asset/shared-link`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PATCH', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(updateAssetsToSharedLinkDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
@@ -4518,6 +4544,16 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
|
||||
export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosParamCreator = AssetApiAxiosParamCreator(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {AddAssetsDto} addAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.addAssetsToSharedLink(addAssetsDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@@ -4687,6 +4723,16 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.getUserAssetsByDeviceId(deviceId, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {RemoveAssetsDto} removeAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.removeAssetsFromSharedLink(removeAssetsDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -4720,16 +4766,6 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAsset(assetId, updateAssetDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SharedLinkResponseDto>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
@@ -4760,6 +4796,15 @@ export const AssetApiFp = function(configuration?: Configuration) {
|
||||
export const AssetApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
|
||||
const localVarFp = AssetApiFp(configuration)
|
||||
return {
|
||||
/**
|
||||
*
|
||||
* @param {AddAssetsDto} addAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.addAssetsToSharedLink(addAssetsDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@@ -4912,6 +4957,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
getUserAssetsByDeviceId(deviceId: string, options?: any): AxiosPromise<Array<string>> {
|
||||
return localVarFp.getUserAssetsByDeviceId(deviceId, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {RemoveAssetsDto} removeAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.removeAssetsFromSharedLink(removeAssetsDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -4942,15 +4996,6 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
updateAsset(assetId: string, updateAssetDto: UpdateAssetDto, options?: any): AxiosPromise<AssetResponseDto> {
|
||||
return localVarFp.updateAsset(assetId, updateAssetDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: any): AxiosPromise<SharedLinkResponseDto> {
|
||||
return localVarFp.updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
@@ -4980,6 +5025,17 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
|
||||
* @extends {BaseAPI}
|
||||
*/
|
||||
export class AssetApi extends BaseAPI {
|
||||
/**
|
||||
*
|
||||
* @param {AddAssetsDto} addAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public addAssetsToSharedLink(addAssetsDto: AddAssetsDto, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).addAssetsToSharedLink(addAssetsDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check duplicated asset before uploading - for Web upload used
|
||||
* @param {CheckDuplicateAssetDto} checkDuplicateAssetDto
|
||||
@@ -5166,6 +5222,17 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).getUserAssetsByDeviceId(deviceId, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {RemoveAssetsDto} removeAssetsDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public removeAssetsFromSharedLink(removeAssetsDto: RemoveAssetsDto, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).removeAssetsFromSharedLink(removeAssetsDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {SearchAssetDto} searchAssetDto
|
||||
@@ -5202,17 +5269,6 @@ export class AssetApi extends BaseAPI {
|
||||
return AssetApiFp(this.configuration).updateAsset(assetId, updateAssetDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {UpdateAssetsToSharedLinkDto} updateAssetsToSharedLinkDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof AssetApi
|
||||
*/
|
||||
public updateAssetsInSharedLink(updateAssetsToSharedLinkDto: UpdateAssetsToSharedLinkDto, options?: AxiosRequestConfig) {
|
||||
return AssetApiFp(this.configuration).updateAssetsInSharedLink(updateAssetsToSharedLinkDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AssetTypeEnum} assetType
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.46.1
|
||||
* The version of the OpenAPI document: 1.47.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.46.1
|
||||
* The version of the OpenAPI document: 1.47.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.46.1
|
||||
* The version of the OpenAPI document: 1.47.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.46.1
|
||||
* The version of the OpenAPI document: 1.47.2
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -68,6 +68,14 @@ input:focus-visible {
|
||||
@apply bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray text-gray-100 border dark:border-immich-dark-gray rounded-xl py-2 px-4 transition-all duration-150 hover:bg-immich-primary dark:hover:bg-immich-dark-primary/90 hover:shadow-lg text-sm font-medium;
|
||||
}
|
||||
|
||||
.immich-btn-primary-big {
|
||||
@apply inline-flex justify-center items-center bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray text-white enabled:dark:hover:bg-immich-dark-primary/80 enabled:hover:bg-immich-primary/75 disabled:cursor-not-allowed px-6 py-4 rounded-md shadow-md w-full font-semibold;
|
||||
}
|
||||
|
||||
.immich-btn-secondary-big {
|
||||
@apply inline-flex justify-center items-center bg-gray-500 dark:bg-gray-200 text-white enabled:hover:bg-gray-500/75 enabled:dark:hover:bg-gray-200/80 dark:text-immich-dark-gray disabled:cursor-not-allowed px-6 py-4 rounded-md shadow-md w-full font-semibold;
|
||||
}
|
||||
|
||||
.immich-text-button {
|
||||
@apply flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,13 @@ import type { Handle, HandleServerError } from '@sveltejs/kit';
|
||||
import { AxiosError } from 'axios';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
return await resolve(event);
|
||||
const res = await resolve(event);
|
||||
|
||||
// The link header can grow quite big and has caused issues with our nginx
|
||||
// proxy returning a 502 Bad Gateway error. Therefore the header gets deleted.
|
||||
res.headers.delete('Link');
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
export const handleError: HandleServerError = async ({ error }) => {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
export let albumId: string;
|
||||
export let assetsInAlbum: AssetResponseDto[];
|
||||
const locale = navigator.language;
|
||||
|
||||
onMount(() => {
|
||||
$assetsInAlbumStoreState = assetsInAlbum;
|
||||
@@ -28,8 +29,11 @@
|
||||
|
||||
assetInteractionStore.clearMultiselect();
|
||||
};
|
||||
|
||||
const locale = navigator.language;
|
||||
const handleSelectFromComputerClicked = async () => {
|
||||
await openFileUploadDialog(albumId, '');
|
||||
assetInteractionStore.clearMultiselect();
|
||||
dispatch('go-back');
|
||||
};
|
||||
</script>
|
||||
|
||||
<section
|
||||
@@ -54,11 +58,7 @@
|
||||
|
||||
<svelte:fragment slot="trailing">
|
||||
<button
|
||||
on:click={() =>
|
||||
openFileUploadDialog(albumId, '', () => {
|
||||
assetInteractionStore.clearMultiselect();
|
||||
dispatch('go-back');
|
||||
})}
|
||||
on:click={handleSelectFromComputerClicked}
|
||||
class="text-immich-primary dark:text-immich-dark-primary text-sm hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/25 transition-all px-6 py-2 rounded-lg font-medium"
|
||||
>
|
||||
Select from computer
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||
/>
|
||||
{:else}
|
||||
<PhotoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />
|
||||
<PhotoViewer {publicSharedKey} {asset} on:close={closeViewer} />
|
||||
{/if}
|
||||
{:else}
|
||||
<VideoViewer {publicSharedKey} assetId={asset.id} on:close={closeViewer} />
|
||||
|
||||
@@ -10,22 +10,14 @@
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
|
||||
export let assetId: string;
|
||||
export let asset: AssetResponseDto;
|
||||
export let publicSharedKey = '';
|
||||
|
||||
let assetInfo: AssetResponseDto;
|
||||
let assetData: string;
|
||||
|
||||
let copyImageToClipboard: (src: string) => Promise<Blob>;
|
||||
|
||||
onMount(async () => {
|
||||
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
|
||||
const module = await import('copy-image-clipboard');
|
||||
copyImageToClipboard = module.copyImageToClipboard;
|
||||
@@ -33,7 +25,7 @@
|
||||
|
||||
const loadAssetData = async () => {
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(assetInfo.id, false, true, {
|
||||
const { data } = await api.assetApi.serveFile(asset.id, false, true, {
|
||||
params: {
|
||||
key: publicSharedKey
|
||||
},
|
||||
@@ -75,18 +67,15 @@
|
||||
transition:fade={{ duration: 150 }}
|
||||
class="flex place-items-center place-content-center h-full select-none"
|
||||
>
|
||||
{#if assetInfo}
|
||||
{#await loadAssetData()}
|
||||
<LoadingSpinner />
|
||||
{:then assetData}
|
||||
<img
|
||||
transition:fade={{ duration: 150 }}
|
||||
src={assetData}
|
||||
alt={assetId}
|
||||
class="object-contain h-full transition-all"
|
||||
loading="lazy"
|
||||
draggable="false"
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
{#await loadAssetData()}
|
||||
<LoadingSpinner />
|
||||
{:then assetData}
|
||||
<img
|
||||
transition:fade={{ duration: 150 }}
|
||||
src={assetData}
|
||||
alt={asset.id}
|
||||
class="object-contain h-full transition-all"
|
||||
draggable="false"
|
||||
/>
|
||||
{/await}
|
||||
</div>
|
||||
|
||||
@@ -1,40 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { api, AssetResponseDto, getFileUrl } from '@api';
|
||||
import { getFileUrl } from '@api';
|
||||
|
||||
export let assetId: string;
|
||||
export let publicSharedKey = '';
|
||||
let asset: AssetResponseDto;
|
||||
|
||||
let isVideoLoading = true;
|
||||
let videoUrl: string;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(async () => {
|
||||
const { data: assetInfo } = await api.assetApi.getAssetById(assetId, {
|
||||
params: {
|
||||
key: publicSharedKey
|
||||
}
|
||||
});
|
||||
|
||||
await loadVideoData(assetInfo);
|
||||
|
||||
asset = assetInfo;
|
||||
});
|
||||
|
||||
const loadVideoData = async (assetInfo: AssetResponseDto) => {
|
||||
isVideoLoading = true;
|
||||
|
||||
videoUrl = getFileUrl(assetInfo.id, false, true, publicSharedKey);
|
||||
|
||||
return assetInfo;
|
||||
};
|
||||
|
||||
const handleCanPlay = (ev: Event) => {
|
||||
const playerNode = ev.target as HTMLVideoElement;
|
||||
const handleCanPlay = (ev: Event & { currentTarget: HTMLVideoElement }) => {
|
||||
const playerNode = ev.currentTarget;
|
||||
|
||||
playerNode.muted = true;
|
||||
playerNode.play();
|
||||
@@ -48,21 +25,19 @@
|
||||
transition:fade={{ duration: 150 }}
|
||||
class="flex place-items-center place-content-center h-full select-none"
|
||||
>
|
||||
{#if asset}
|
||||
<video
|
||||
controls
|
||||
class="h-full object-contain"
|
||||
on:canplay={handleCanPlay}
|
||||
on:ended={() => dispatch('onVideoEnded')}
|
||||
>
|
||||
<source src={videoUrl} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
<video
|
||||
controls
|
||||
class="h-full object-contain"
|
||||
on:canplay={handleCanPlay}
|
||||
on:ended={() => dispatch('onVideoEnded')}
|
||||
>
|
||||
<source src={getFileUrl(assetId, false, true, publicSharedKey)} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
|
||||
{#if isVideoLoading}
|
||||
<div class="absolute flex place-items-center place-content-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
{#if isVideoLoading}
|
||||
<div class="absolute flex place-items-center place-content-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, oauth, OAuthConfigResponseDto } from '@api';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
|
||||
let error: string;
|
||||
@@ -75,10 +76,10 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-4 shadow-sm w-[500px] max-w-[95vw] rounded-md py-8"
|
||||
class="border bg-white dark:bg-immich-dark-gray dark:border-immich-dark-gray p-8 shadow-sm w-full max-w-lg rounded-md"
|
||||
>
|
||||
<div class="flex flex-col place-items-center place-content-center gap-4 px-4">
|
||||
<ImmichLogo class="text-center" height="100" width="100" />
|
||||
<div class="flex flex-col place-items-center place-content-center gap-4 py-4">
|
||||
<ImmichLogo class="text-center h-24 w-24" />
|
||||
<h1 class="text-2xl text-immich-primary dark:text-immich-dark-primary font-medium">Login</h1>
|
||||
</div>
|
||||
|
||||
@@ -90,73 +91,84 @@
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="flex place-items-center place-content-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else}
|
||||
{#if authConfig.passwordLoginEnabled}
|
||||
<form on:submit|preventDefault={login} class="flex flex-col gap-5 mt-5">
|
||||
{#if error}
|
||||
<p class="text-red-400" transition:fade>
|
||||
{error}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="email">Email</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="password">Password</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="my-5 flex w-full">
|
||||
<button
|
||||
type="submit"
|
||||
class="immich-btn-primary-big inline-flex items-center h-14"
|
||||
disabled={loading}
|
||||
>
|
||||
{#if loading}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
Login
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if authConfig.enabled}
|
||||
{#if authConfig.passwordLoginEnabled}
|
||||
<form on:submit|preventDefault={login} autocomplete="off">
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="email">Email</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
bind:value={email}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="m-4 flex flex-col gap-2">
|
||||
<label class="immich-form-label" for="password">Password</label>
|
||||
<input
|
||||
class="immich-form-input"
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<p class="text-red-400 pl-4">{error}</p>
|
||||
{/if}
|
||||
|
||||
<div class="flex w-full">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
class="m-4 p-2 bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80 hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full font-semibold"
|
||||
>Login</button
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
|
||||
{#if authConfig.enabled}
|
||||
<div class="flex flex-col gap-4 px-4">
|
||||
{#if authConfig.passwordLoginEnabled}
|
||||
<hr />
|
||||
{/if}
|
||||
{#if oauthError}
|
||||
<p class="text-red-400">{oauthError}</p>
|
||||
{/if}
|
||||
<a href={authConfig.url} class="flex w-full">
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
class="bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:hover:bg-immich-dark-primary/80 hover:bg-immich-primary/75 px-6 py-4 text-white rounded-md shadow-md w-full font-semibold"
|
||||
>{authConfig.buttonText || 'Login with OAuth'}</button
|
||||
>
|
||||
</a>
|
||||
<div class="inline-flex items-center justify-center w-full">
|
||||
<hr class="w-3/4 h-px my-6 bg-gray-200 border-0 dark:bg-gray-600" />
|
||||
<span
|
||||
class="absolute px-3 font-medium text-gray-900 -translate-x-1/2 left-1/2 dark:text-white bg-white dark:bg-immich-dark-gray"
|
||||
>
|
||||
or
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="my-5 flex flex-col gap-5">
|
||||
{#if oauthError}
|
||||
<p class="text-red-400" transition:fade>{oauthError}</p>
|
||||
{/if}
|
||||
<a href={authConfig.url} class="flex w-full">
|
||||
<button
|
||||
type="button"
|
||||
disabled={loading}
|
||||
class={authConfig.passwordLoginEnabled
|
||||
? 'immich-btn-secondary-big'
|
||||
: 'immich-btn-primary-big'}
|
||||
>
|
||||
{authConfig.buttonText || 'Login with OAuth'}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !authConfig.enabled && !authConfig.passwordLoginEnabled}
|
||||
<p class="text-center dark:text-immich-dark-fg p-4">Login has been disabled.</p>
|
||||
{/if}
|
||||
{#if !authConfig.enabled && !authConfig.passwordLoginEnabled}
|
||||
<p class="text-center dark:text-immich-dark-fg p-4">Login has been disabled.</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
import CloudDownloadOutline from 'svelte-material-icons/CloudDownloadOutline.svelte';
|
||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||
import DeleteOutline from 'svelte-material-icons/DeleteOutline.svelte';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '../shared-components/notification/notification';
|
||||
import ImmichLogo from '../shared-components/immich-logo.svelte';
|
||||
|
||||
export let sharedLink: SharedLinkResponseDto;
|
||||
export let isOwned: boolean;
|
||||
@@ -43,11 +43,15 @@
|
||||
);
|
||||
};
|
||||
|
||||
const handleUploadAssets = () => {
|
||||
openFileUploadDialog(undefined, sharedLink?.key, async (assetId) => {
|
||||
await api.assetApi.updateAssetsInSharedLink(
|
||||
const handleUploadAssets = async () => {
|
||||
try {
|
||||
const results = await openFileUploadDialog(undefined, sharedLink?.key);
|
||||
|
||||
const assetIds = results.filter((id) => !!id) as string[];
|
||||
|
||||
await api.assetApi.addAssetsToSharedLink(
|
||||
{
|
||||
assetIds: [...assets.map((a) => a.id), assetId]
|
||||
assetIds
|
||||
},
|
||||
{
|
||||
params: {
|
||||
@@ -57,15 +61,17 @@
|
||||
);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Add asset to shared link successfully',
|
||||
message: `Successfully add ${assetIds.length} to the shared link`,
|
||||
type: NotificationType.Info
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('handleUploadAssets', e);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveAssetsFromSharedLink = async () => {
|
||||
if (window.confirm('Do you want to remove selected assets from the shared link?')) {
|
||||
await api.assetApi.updateAssetsInSharedLink(
|
||||
await api.assetApi.removeAssetsFromSharedLink(
|
||||
{
|
||||
assetIds: assets.filter((a) => !selectedAssets.has(a)).map((a) => a.id)
|
||||
},
|
||||
|
||||
@@ -141,7 +141,11 @@ export function getFileMimeType(file: File): string {
|
||||
case '3gp':
|
||||
return 'video/3gpp';
|
||||
case 'nef':
|
||||
return 'image/nef';
|
||||
return 'image/x-nikon-nef';
|
||||
case 'raf':
|
||||
return 'image/x-fuji-raf';
|
||||
case 'srw':
|
||||
return 'image/x-samsung-srw';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -6,70 +6,65 @@ import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
import type { UploadAsset } from '../models/upload-asset';
|
||||
import { api, AssetFileUploadResponseDto } from '@api';
|
||||
import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils';
|
||||
import { Subject, mergeMap } from 'rxjs';
|
||||
import { mergeMap, filter, firstValueFrom, from, of, combineLatestAll } from 'rxjs';
|
||||
import axios from 'axios';
|
||||
|
||||
export const openFileUploadDialog = (
|
||||
export const openFileUploadDialog = async (
|
||||
albumId: string | undefined = undefined,
|
||||
sharedKey: string | undefined = undefined,
|
||||
onDone?: (id: string) => void
|
||||
sharedKey: string | undefined = undefined
|
||||
) => {
|
||||
try {
|
||||
const fileSelector = document.createElement('input');
|
||||
return new Promise<(string | undefined)[]>((resolve, reject) => {
|
||||
try {
|
||||
const fileSelector = document.createElement('input');
|
||||
|
||||
fileSelector.type = 'file';
|
||||
fileSelector.multiple = true;
|
||||
fileSelector.type = 'file';
|
||||
fileSelector.multiple = true;
|
||||
|
||||
// When adding a content type that is unsupported by browsers, make sure
|
||||
// to also add it to getFileMimeType() otherwise the upload will fail.
|
||||
fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef';
|
||||
// When adding a content type that is unsupported by browsers, make sure
|
||||
// to also add it to getFileMimeType() otherwise the upload will fail.
|
||||
fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef,.srw,.raf';
|
||||
|
||||
fileSelector.onchange = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (!target.files) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from<File>(target.files);
|
||||
fileSelector.onchange = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (!target.files) {
|
||||
return;
|
||||
}
|
||||
const files = Array.from<File>(target.files);
|
||||
|
||||
await fileUploadHandler(files, albumId, sharedKey, onDone);
|
||||
};
|
||||
resolve(await fileUploadHandler(files, albumId, sharedKey));
|
||||
};
|
||||
|
||||
fileSelector.click();
|
||||
} catch (e) {
|
||||
console.log('Error selecting file', e);
|
||||
}
|
||||
fileSelector.click();
|
||||
} catch (e) {
|
||||
console.log('Error selecting file', e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const fileUploadHandler = async (
|
||||
files: File[],
|
||||
albumId: string | undefined = undefined,
|
||||
sharedKey: string | undefined = undefined,
|
||||
onDone?: (id: string) => void
|
||||
sharedKey: string | undefined = undefined
|
||||
) => {
|
||||
const files$ = new Subject<File>();
|
||||
files$
|
||||
.pipe(
|
||||
mergeMap(async (file) => {
|
||||
await fileUploader(file, albumId, sharedKey, onDone);
|
||||
}, 2)
|
||||
return firstValueFrom(
|
||||
from(files).pipe(
|
||||
filter((file) => {
|
||||
const assetType = getFileMimeType(file).split('/')[0];
|
||||
return assetType === 'video' || assetType === 'image';
|
||||
}),
|
||||
mergeMap(async (file) => of(await fileUploader(file, albumId, sharedKey)), 2),
|
||||
combineLatestAll()
|
||||
)
|
||||
.subscribe();
|
||||
const acceptedFile = files.filter((file) => {
|
||||
const assetType = getFileMimeType(file).split('/')[0];
|
||||
return assetType === 'video' || assetType === 'image';
|
||||
});
|
||||
for (const file of acceptedFile) {
|
||||
files$.next(file);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
//TODO: should probably use the @api SDK
|
||||
async function fileUploader(
|
||||
asset: File,
|
||||
albumId: string | undefined = undefined,
|
||||
sharedKey: string | undefined = undefined,
|
||||
onDone?: (id: string) => void
|
||||
) {
|
||||
console.log('uploading', asset.name);
|
||||
sharedKey: string | undefined = undefined
|
||||
): Promise<string | undefined> {
|
||||
const mimeType = getFileMimeType(asset);
|
||||
const assetType = mimeType.split('/')[0].toUpperCase();
|
||||
const fileExtension = getFilenameExtension(asset.name);
|
||||
@@ -120,67 +115,50 @@ async function fileUploader(
|
||||
}
|
||||
);
|
||||
|
||||
if (status === 200) {
|
||||
if (data.isExist) {
|
||||
const dataId = data.id;
|
||||
if (albumId && dataId) {
|
||||
addAssetsToAlbum(albumId, [dataId], sharedKey);
|
||||
}
|
||||
onDone && dataId && onDone(dataId);
|
||||
return;
|
||||
if (status === 200 && data.isExist && data.id) {
|
||||
if (albumId) {
|
||||
await addAssetsToAlbum(albumId, [data.id], sharedKey);
|
||||
}
|
||||
|
||||
return data.id;
|
||||
}
|
||||
|
||||
const request = new XMLHttpRequest();
|
||||
request.upload.onloadstart = () => {
|
||||
const newUploadAsset: UploadAsset = {
|
||||
id: deviceAssetId,
|
||||
file: asset,
|
||||
progress: 0,
|
||||
fileExtension: fileExtension
|
||||
};
|
||||
|
||||
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
|
||||
const newUploadAsset: UploadAsset = {
|
||||
id: deviceAssetId,
|
||||
file: asset,
|
||||
progress: 0,
|
||||
fileExtension: fileExtension
|
||||
};
|
||||
|
||||
request.upload.onload = () => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
const res: AssetFileUploadResponseDto = JSON.parse(request.response || '{}');
|
||||
if (albumId) {
|
||||
try {
|
||||
if (res.id) {
|
||||
addAssetsToAlbum(albumId, [res.id], sharedKey);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('ERROR parsing data JSON in upload onload');
|
||||
}
|
||||
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
|
||||
|
||||
const response = await axios.post(`/api/asset/upload`, formData, {
|
||||
params: {
|
||||
key: sharedKey
|
||||
},
|
||||
onUploadProgress: (event) => {
|
||||
const percentComplete = Math.floor((event.loaded / event.total) * 100);
|
||||
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
|
||||
}
|
||||
onDone && onDone(res.id);
|
||||
};
|
||||
});
|
||||
|
||||
// listen for `error` event
|
||||
request.upload.onerror = () => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
handleUploadError(asset, request.response);
|
||||
};
|
||||
if (response.status == 200 || response.status == 201) {
|
||||
const res: AssetFileUploadResponseDto = response.data;
|
||||
|
||||
// listen for `abort` event
|
||||
request.upload.onabort = () => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
handleUploadError(asset, request.response);
|
||||
};
|
||||
if (albumId && res.id) {
|
||||
await addAssetsToAlbum(albumId, [res.id], sharedKey);
|
||||
}
|
||||
|
||||
// listen for `progress` event
|
||||
request.upload.onprogress = (event) => {
|
||||
const percentComplete = Math.floor((event.loaded / event.total) * 100);
|
||||
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
|
||||
};
|
||||
setTimeout(() => {
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
}, 1000);
|
||||
|
||||
request.open('POST', `/api/asset/upload?key=${sharedKey ?? ''}`);
|
||||
|
||||
request.send(formData);
|
||||
return res.id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error uploading file ', e);
|
||||
handleUploadError(asset, JSON.stringify(e));
|
||||
uploadAssetsStore.removeUploadAsset(deviceAssetId);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const load: LayoutServerLoad = async ({ request }) => {
|
||||
user: userInfo
|
||||
};
|
||||
} catch (e) {
|
||||
console.error('[ERROR] layout.server.ts [LayoutServerLoad]: ', e);
|
||||
console.error('[ERROR] layout.server.ts [LayoutServerLoad]: ');
|
||||
return {
|
||||
user: undefined
|
||||
};
|
||||
|
||||
25
web/src/routes/+page.server.ts
Normal file
25
web/src/routes/+page.server.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export const prerender = false;
|
||||
import { serverApi } from '@api';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
if (user) {
|
||||
throw redirect(302, '/photos');
|
||||
}
|
||||
|
||||
const { data } = await serverApi.userApi.getUserCount(true);
|
||||
|
||||
if (data.userCount > 0) {
|
||||
// Redirect to login page if an admin is already registered.
|
||||
throw redirect(302, '/auth/login');
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: 'Welcome 🎉',
|
||||
description: 'Immich Web Interface'
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -15,7 +15,7 @@
|
||||
</h1>
|
||||
<button
|
||||
class="border px-4 py-4 rounded-md bg-immich-primary dark:bg-immich-dark-primary dark:text-immich-dark-gray dark:border-immich-dark-gray hover:bg-immich-primary/75 text-white font-bold w-[200px]"
|
||||
on:click={() => goto('/auth/login')}
|
||||
on:click={() => goto('/auth/register')}
|
||||
>Getting Started
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export const prerender = false;
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async ({ parent }) => {
|
||||
const { user } = await parent();
|
||||
if (user) {
|
||||
throw redirect(302, '/photos');
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: 'Welcome 🎉',
|
||||
description: 'Immich Web Interface'
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -5,11 +5,12 @@
|
||||
import LoginForm from '$lib/components/forms/login-form.svelte';
|
||||
</script>
|
||||
|
||||
<section class="h-screen w-screen flex place-items-center place-content-center">
|
||||
<div in:fade={{ duration: 100 }} out:fade={{ duration: 100 }}>
|
||||
<LoginForm
|
||||
on:success={() => goto('/photos')}
|
||||
on:first-login={() => goto('/auth/change-password')}
|
||||
/>
|
||||
</div>
|
||||
<section
|
||||
class="min-h-screen w-screen flex place-items-center place-content-center p-4"
|
||||
transition:fade={{ duration: 100 }}
|
||||
>
|
||||
<LoginForm
|
||||
on:success={() => goto('/photos')}
|
||||
on:first-login={() => goto('/auth/change-password')}
|
||||
/>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user