mirror of
https://github.com/immich-app/immich.git
synced 2026-01-29 16:27:14 -08:00
Compare commits
4 Commits
feat/share
...
release/ne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f228f5c935 | ||
|
|
0be1ffade6 | ||
|
|
1a04caee29 | ||
|
|
3ace578fc0 |
31
CHANGELOG.md
Normal file
31
CHANGELOG.md
Normal file
@@ -0,0 +1,31 @@
|
||||
|
||||
# v2.5.3
|
||||
|
||||
## Highlights
|
||||
|
||||
{{RELEASE HIGHLIGHTS}}
|
||||
|
||||
As always, please consider supporting the project.
|
||||
|
||||
🎉 Cheers! 🎉
|
||||
|
||||
|
||||
----
|
||||
|
||||
And as always, bugs are fixed, and many other improvements also come with this release.
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### 🐛 Bug fixes
|
||||
* chore: remove random code snippet by @jrasm91 in https://github.com/immich-app/immich/pull/25677
|
||||
* fix: reset and unsaved change states in editor by @bwees in https://github.com/immich-app/immich/pull/25588
|
||||
* fix: no notification if release check is disabled by @jrasm91 in https://github.com/immich-app/immich/pull/25688
|
||||
### 📚 Documentation
|
||||
* docs(openapi): Add descriptions to OpenAPI specification by @timonrieger in https://github.com/immich-app/immich/pull/25185
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/immich-app/immich/compare/v2.5.2...v2.5.3
|
||||
|
||||
---
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tools]
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.10.7"
|
||||
opentofu = "1.11.4"
|
||||
|
||||
[tasks."tg:fmt"]
|
||||
run = "terragrunt hclfmt"
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v2.5.2",
|
||||
"url": "https://docs.v2.5.2.archive.immich.app"
|
||||
"label": "v2.5.3",
|
||||
"url": "https://docs.v2.5.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.4.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('/shared-links', () => {
|
||||
const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.passwordRequired);
|
||||
expect(body).toEqual(errorDto.invalidSharePassword);
|
||||
});
|
||||
|
||||
it('should get data for correct password protected link', async () => {
|
||||
|
||||
@@ -43,10 +43,10 @@ export const errorDto = {
|
||||
message: 'Invalid share key',
|
||||
correlationId: expect.any(String),
|
||||
},
|
||||
passwordRequired: {
|
||||
invalidSharePassword: {
|
||||
error: 'Unauthorized',
|
||||
statusCode: 401,
|
||||
message: 'Password required',
|
||||
message: 'Invalid password',
|
||||
correlationId: expect.any(String),
|
||||
},
|
||||
badRequest: (message: any = null) => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-i18n",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"format": "prettier --check .",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
2
machine-learning/uv.lock
generated
2
machine-learning/uv.lock
generated
@@ -919,7 +919,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "2.5.2"
|
||||
version = "2.5.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -18,7 +18,7 @@ node = "24.13.0"
|
||||
flutter = "3.35.7"
|
||||
pnpm = "10.28.0"
|
||||
terragrunt = "0.98.0"
|
||||
opentofu = "1.10.7"
|
||||
opentofu = "1.11.4"
|
||||
java = "25.0.1"
|
||||
|
||||
[tools."github:CQLabs/homebrew-dcm"]
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3033,
|
||||
"android.injected.version.name" => "2.5.2",
|
||||
"android.injected.version.code" => 3034,
|
||||
"android.injected.version.name" => "2.5.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')
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.5.2</string>
|
||||
<string>2.5.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
4
mobile/openapi/README.md
generated
4
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: 2.5.2
|
||||
- API version: 2.5.3
|
||||
- Generator version: 7.8.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
@@ -256,7 +256,6 @@ Class | Method | HTTP request | Description
|
||||
*SharedLinksApi* | [**getSharedLinkById**](doc//SharedLinksApi.md#getsharedlinkbyid) | **GET** /shared-links/{id} | Retrieve a shared link
|
||||
*SharedLinksApi* | [**removeSharedLink**](doc//SharedLinksApi.md#removesharedlink) | **DELETE** /shared-links/{id} | Delete a shared link
|
||||
*SharedLinksApi* | [**removeSharedLinkAssets**](doc//SharedLinksApi.md#removesharedlinkassets) | **DELETE** /shared-links/{id}/assets | Remove assets from a shared link
|
||||
*SharedLinksApi* | [**sharedLinkLogin**](doc//SharedLinksApi.md#sharedlinklogin) | **POST** /shared-links/login | Shared link login
|
||||
*SharedLinksApi* | [**updateSharedLink**](doc//SharedLinksApi.md#updatesharedlink) | **PATCH** /shared-links/{id} | Update a shared link
|
||||
*StacksApi* | [**createStack**](doc//StacksApi.md#createstack) | **POST** /stacks | Create a stack
|
||||
*StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} | Delete a stack
|
||||
@@ -553,7 +552,6 @@ Class | Method | HTTP request | Description
|
||||
- [SetMaintenanceModeDto](doc//SetMaintenanceModeDto.md)
|
||||
- [SharedLinkCreateDto](doc//SharedLinkCreateDto.md)
|
||||
- [SharedLinkEditDto](doc//SharedLinkEditDto.md)
|
||||
- [SharedLinkLoginDto](doc//SharedLinkLoginDto.md)
|
||||
- [SharedLinkResponseDto](doc//SharedLinkResponseDto.md)
|
||||
- [SharedLinkType](doc//SharedLinkType.md)
|
||||
- [SharedLinksResponse](doc//SharedLinksResponse.md)
|
||||
|
||||
1
mobile/openapi/lib/api.dart
generated
1
mobile/openapi/lib/api.dart
generated
@@ -292,7 +292,6 @@ part 'model/session_update_dto.dart';
|
||||
part 'model/set_maintenance_mode_dto.dart';
|
||||
part 'model/shared_link_create_dto.dart';
|
||||
part 'model/shared_link_edit_dto.dart';
|
||||
part 'model/shared_link_login_dto.dart';
|
||||
part 'model/shared_link_response_dto.dart';
|
||||
part 'model/shared_link_type.dart';
|
||||
part 'model/shared_links_response.dart';
|
||||
|
||||
71
mobile/openapi/lib/api/shared_links_api.dart
generated
71
mobile/openapi/lib/api/shared_links_api.dart
generated
@@ -495,77 +495,6 @@ class SharedLinksApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Shared link login
|
||||
///
|
||||
/// Login to a password protected shared link
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SharedLinkLoginDto] sharedLinkLoginDto (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<Response> sharedLinkLoginWithHttpInfo(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final apiPath = r'/shared-links/login';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = sharedLinkLoginDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
if (key != null) {
|
||||
queryParams.addAll(_queryParams('', 'key', key));
|
||||
}
|
||||
if (slug != null) {
|
||||
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||
}
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
apiPath,
|
||||
'POST',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Shared link login
|
||||
///
|
||||
/// Login to a password protected shared link
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [SharedLinkLoginDto] sharedLinkLoginDto (required):
|
||||
///
|
||||
/// * [String] key:
|
||||
///
|
||||
/// * [String] slug:
|
||||
Future<SharedLinkResponseDto?> sharedLinkLogin(SharedLinkLoginDto sharedLinkLoginDto, { String? key, String? slug, }) async {
|
||||
final response = await sharedLinkLoginWithHttpInfo(sharedLinkLoginDto, key: key, slug: slug, );
|
||||
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;
|
||||
}
|
||||
|
||||
/// Update a shared link
|
||||
///
|
||||
/// Update an existing shared link by its ID.
|
||||
|
||||
2
mobile/openapi/lib/api_client.dart
generated
2
mobile/openapi/lib/api_client.dart
generated
@@ -630,8 +630,6 @@ class ApiClient {
|
||||
return SharedLinkCreateDto.fromJson(value);
|
||||
case 'SharedLinkEditDto':
|
||||
return SharedLinkEditDto.fromJson(value);
|
||||
case 'SharedLinkLoginDto':
|
||||
return SharedLinkLoginDto.fromJson(value);
|
||||
case 'SharedLinkResponseDto':
|
||||
return SharedLinkResponseDto.fromJson(value);
|
||||
case 'SharedLinkType':
|
||||
|
||||
100
mobile/openapi/lib/model/shared_link_login_dto.dart
generated
100
mobile/openapi/lib/model/shared_link_login_dto.dart
generated
@@ -1,100 +0,0 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.18
|
||||
|
||||
// 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 SharedLinkLoginDto {
|
||||
/// Returns a new [SharedLinkLoginDto] instance.
|
||||
SharedLinkLoginDto({
|
||||
required this.password,
|
||||
});
|
||||
|
||||
/// Shared link password
|
||||
String password;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is SharedLinkLoginDto &&
|
||||
other.password == password;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(password.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'SharedLinkLoginDto[password=$password]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'password'] = this.password;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [SharedLinkLoginDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static SharedLinkLoginDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "SharedLinkLoginDto");
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return SharedLinkLoginDto(
|
||||
password: mapValueOfType<String>(json, r'password')!,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<SharedLinkLoginDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <SharedLinkLoginDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = SharedLinkLoginDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, SharedLinkLoginDto> mapFromJson(dynamic json) {
|
||||
final map = <String, SharedLinkLoginDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = SharedLinkLoginDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of SharedLinkLoginDto-objects as value to a dart map
|
||||
static Map<String, List<SharedLinkLoginDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<SharedLinkLoginDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = SharedLinkLoginDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'password',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 2.5.2+3033
|
||||
version: 2.5.3+3034
|
||||
|
||||
environment:
|
||||
sdk: '>=3.8.0 <4.0.0'
|
||||
|
||||
@@ -11224,78 +11224,6 @@
|
||||
"x-immich-state": "Stable"
|
||||
}
|
||||
},
|
||||
"/shared-links/login": {
|
||||
"post": {
|
||||
"description": "Login to a password protected shared link",
|
||||
"operationId": "sharedLinkLogin",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "key",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "slug",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkLoginDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SharedLinkResponseDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
],
|
||||
"summary": "Shared link login",
|
||||
"tags": [
|
||||
"Shared links"
|
||||
],
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Beta"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Beta"
|
||||
}
|
||||
},
|
||||
"/shared-links/me": {
|
||||
"get": {
|
||||
"description": "Retrieve the current shared link associated with authentication method.",
|
||||
@@ -15129,7 +15057,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
@@ -21741,19 +21669,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinkLoginDto": {
|
||||
"properties": {
|
||||
"password": {
|
||||
"description": "Shared link password",
|
||||
"example": "password",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"password"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SharedLinkResponseDto": {
|
||||
"properties": {
|
||||
"album": {
|
||||
@@ -21812,25 +21727,9 @@
|
||||
"type": "string"
|
||||
},
|
||||
"token": {
|
||||
"deprecated": true,
|
||||
"description": "Access token",
|
||||
"nullable": true,
|
||||
"type": "string",
|
||||
"x-immich-history": [
|
||||
{
|
||||
"version": "v1",
|
||||
"state": "Added"
|
||||
},
|
||||
{
|
||||
"version": "v2",
|
||||
"state": "Stable"
|
||||
},
|
||||
{
|
||||
"version": "v2.6.0",
|
||||
"state": "Deprecated"
|
||||
}
|
||||
],
|
||||
"x-immich-state": "Deprecated"
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"allOf": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"type": "module",
|
||||
"main": "./build/index.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 2.5.2
|
||||
* 2.5.3
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
@@ -2285,10 +2285,6 @@ export type SharedLinkCreateDto = {
|
||||
/** Shared link type */
|
||||
"type": SharedLinkType;
|
||||
};
|
||||
export type SharedLinkLoginDto = {
|
||||
/** Shared link password */
|
||||
password: string;
|
||||
};
|
||||
export type SharedLinkEditDto = {
|
||||
/** Allow downloads */
|
||||
allowDownload?: boolean;
|
||||
@@ -5863,26 +5859,6 @@ export function createSharedLink({ sharedLinkCreateDto }: {
|
||||
body: sharedLinkCreateDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Shared link login
|
||||
*/
|
||||
export function sharedLinkLogin({ key, slug, sharedLinkLoginDto }: {
|
||||
key?: string;
|
||||
slug?: string;
|
||||
sharedLinkLoginDto: SharedLinkLoginDto;
|
||||
}, opts?: Oazapfts.RequestOpts) {
|
||||
return oazapfts.ok(oazapfts.fetchJson<{
|
||||
status: 201;
|
||||
data: SharedLinkResponseDto;
|
||||
}>(`/shared-links/login${QS.query(QS.explode({
|
||||
key,
|
||||
slug
|
||||
}))}`, oazapfts.json({
|
||||
...opts,
|
||||
method: "POST",
|
||||
body: sharedLinkLoginDto
|
||||
})));
|
||||
}
|
||||
/**
|
||||
* Retrieve current shared link
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "Monorepo for Immich",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -22,39 +22,21 @@ import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import {
|
||||
SharedLinkCreateDto,
|
||||
SharedLinkEditDto,
|
||||
SharedLinkLoginDto,
|
||||
SharedLinkPasswordDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkSearchDto,
|
||||
} from 'src/dtos/shared-link.dto';
|
||||
import { ApiTag, ImmichCookie, Permission } from 'src/enum';
|
||||
import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { LoginDetails } from 'src/services/auth.service';
|
||||
import { SharedLinkService } from 'src/services/shared-link.service';
|
||||
import { respondWithCookie } from 'src/utils/response';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
const getAuthTokens = (cookies: Record<string, string> | undefined) => {
|
||||
return cookies?.[ImmichCookie.SharedLinkToken]?.split(',') || [];
|
||||
};
|
||||
|
||||
const merge = (cookies: Record<string, string> | undefined, token: string) => {
|
||||
const authTokens = getAuthTokens(cookies);
|
||||
if (!authTokens.includes(token)) {
|
||||
authTokens.push(token);
|
||||
}
|
||||
|
||||
return authTokens.join(',');
|
||||
};
|
||||
|
||||
@ApiTags(ApiTag.SharedLinks)
|
||||
@Controller('shared-links')
|
||||
export class SharedLinkController {
|
||||
constructor(
|
||||
private service: SharedLinkService,
|
||||
private logger: LoggingRepository,
|
||||
) {}
|
||||
constructor(private service: SharedLinkService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ permission: Permission.SharedLinkRead })
|
||||
@@ -67,28 +49,6 @@ export class SharedLinkController {
|
||||
return this.service.getAll(auth, dto);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Endpoint({
|
||||
summary: 'Shared link login',
|
||||
description: 'Login to a password protected shared link',
|
||||
history: new HistoryBuilder().added('v2.6.0').beta('v2.6.0'),
|
||||
})
|
||||
async sharedLinkLogin(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: SharedLinkLoginDto,
|
||||
@Req() req: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
const { sharedLink, token } = await this.service.login(auth, dto);
|
||||
|
||||
return respondWithCookie(res, sharedLink, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [{ key: ImmichCookie.SharedLinkToken, value: merge(req.cookies, token) }],
|
||||
});
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@Endpoint({
|
||||
@@ -99,19 +59,19 @@ export class SharedLinkController {
|
||||
async getMySharedLink(
|
||||
@Auth() auth: AuthDto,
|
||||
@Query() dto: SharedLinkPasswordDto,
|
||||
@Req() req: Request,
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
if (dto.password) {
|
||||
this.logger.deprecate(
|
||||
'Passing shared link password via query parameters is deprecated and will be removed in the next major release. Please use POST /shared-links/login instead.',
|
||||
);
|
||||
|
||||
return this.sharedLinkLogin(auth, { password: dto.password }, req, res, loginDetails);
|
||||
const sharedLinkToken = request.cookies?.[ImmichCookie.SharedLinkToken];
|
||||
if (sharedLinkToken) {
|
||||
dto.token = sharedLinkToken;
|
||||
}
|
||||
|
||||
return this.service.getMine(auth, getAuthTokens(req.cookies));
|
||||
const body = await this.service.getMine(auth, dto);
|
||||
return respondWithCookie(res, body, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: body.token ? [{ key: ImmichCookie.SharedLinkToken, value: body.token }] : [],
|
||||
});
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsString } from 'class-validator';
|
||||
import { SharedLink } from 'src/database';
|
||||
import { HistoryBuilder, Property } from 'src/decorators';
|
||||
import { HistoryBuilder } from 'src/decorators';
|
||||
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class SharedLinkSearchDto {
|
||||
@ValidateUUID({ optional: true, description: 'Filter by album ID' })
|
||||
@@ -94,11 +94,6 @@ export class SharedLinkEditDto {
|
||||
changeExpiryTime?: boolean;
|
||||
}
|
||||
|
||||
export class SharedLinkLoginDto {
|
||||
@ValidateString({ description: 'Shared link password', example: 'password' })
|
||||
password!: string;
|
||||
}
|
||||
|
||||
export class SharedLinkPasswordDto {
|
||||
@ApiPropertyOptional({ example: 'password', description: 'Link password' })
|
||||
@IsString()
|
||||
@@ -117,10 +112,7 @@ export class SharedLinkResponseDto {
|
||||
description!: string | null;
|
||||
@ApiProperty({ description: 'Has password' })
|
||||
password!: string | null;
|
||||
@Property({
|
||||
description: 'Access token',
|
||||
history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'),
|
||||
})
|
||||
@ApiPropertyOptional({ description: 'Access token' })
|
||||
token?: string | null;
|
||||
@ApiProperty({ description: 'Owner user ID' })
|
||||
userId!: string;
|
||||
|
||||
@@ -35,14 +35,14 @@ describe(SharedLinkService.name, () => {
|
||||
|
||||
describe('getMine', () => {
|
||||
it('should only work for a public user', async () => {
|
||||
await expect(sut.getMine(authStub.admin, [])).rejects.toBeInstanceOf(ForbiddenException);
|
||||
await expect(sut.getMine(authStub.admin, {})).rejects.toBeInstanceOf(ForbiddenException);
|
||||
expect(mocks.sharedLink.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the shared link for the public user', async () => {
|
||||
const authDto = authStub.adminSharedLink;
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.valid);
|
||||
await expect(sut.getMine(authDto, [])).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
await expect(sut.getMine(authDto, {})).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
@@ -55,22 +55,21 @@ describe(SharedLinkService.name, () => {
|
||||
},
|
||||
});
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.readonlyNoExif);
|
||||
const response = await sut.getMine(authDto, []);
|
||||
const response = await sut.getMine(authDto, {});
|
||||
expect(response.assets[0]).toMatchObject({ hasMetadata: false });
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
it('should throw an error for a request without a shared link auth token', async () => {
|
||||
it('should throw an error for an invalid password protected shared link', async () => {
|
||||
const authDto = authStub.adminSharedLink;
|
||||
mocks.sharedLink.get.mockResolvedValue(sharedLinkStub.passwordRequired);
|
||||
await expect(sut.getMine(authDto, [])).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
await expect(sut.getMine(authDto, {})).rejects.toBeInstanceOf(UnauthorizedException);
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(authDto.user.id, authDto.sharedLink?.id);
|
||||
});
|
||||
|
||||
it('should accept a valid shared link auth token', async () => {
|
||||
it('should allow a correct password on a password protected shared link', async () => {
|
||||
mocks.sharedLink.get.mockResolvedValue({ ...sharedLinkStub.individual, password: '123' });
|
||||
mocks.crypto.hashSha256.mockReturnValue('hashed-auth-token');
|
||||
await expect(sut.getMine(authStub.adminSharedLink, ['hashed-auth-token'])).resolves.toBeDefined();
|
||||
await expect(sut.getMine(authStub.adminSharedLink, { password: '123' })).resolves.toBeDefined();
|
||||
expect(mocks.sharedLink.get).toHaveBeenCalledWith(
|
||||
authStub.adminSharedLink.user.id,
|
||||
authStub.adminSharedLink.sharedLink?.id,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PostgresError } from 'postgres';
|
||||
import { SharedLink } from 'src/database';
|
||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
mapSharedLink,
|
||||
SharedLinkCreateDto,
|
||||
SharedLinkEditDto,
|
||||
SharedLinkLoginDto,
|
||||
SharedLinkPasswordDto,
|
||||
SharedLinkResponseDto,
|
||||
SharedLinkSearchDto,
|
||||
} from 'src/dtos/shared-link.dto';
|
||||
@@ -23,41 +24,18 @@ export class SharedLinkService extends BaseService {
|
||||
.then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));
|
||||
}
|
||||
|
||||
async login(auth: AuthDto, dto: SharedLinkLoginDto) {
|
||||
async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
|
||||
if (!auth.sharedLink) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);
|
||||
const { id, password } = sharedLink;
|
||||
|
||||
if (!password) {
|
||||
throw new BadRequestException('Shared link is not password protected');
|
||||
const response = mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif });
|
||||
if (sharedLink.password) {
|
||||
response.token = this.validateAndRefreshToken(sharedLink, dto);
|
||||
}
|
||||
|
||||
if (password !== dto.password) {
|
||||
throw new UnauthorizedException('Invalid password');
|
||||
}
|
||||
|
||||
return {
|
||||
sharedLink: mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif }),
|
||||
token: this.asToken({ id, password }),
|
||||
};
|
||||
}
|
||||
|
||||
async getMine(auth: AuthDto, authTokens: string[]) {
|
||||
if (!auth.sharedLink) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);
|
||||
const { id, password } = sharedLink;
|
||||
|
||||
if (password && !authTokens.includes(this.asToken({ id, password }))) {
|
||||
throw new UnauthorizedException('Password required');
|
||||
}
|
||||
|
||||
return mapSharedLink(sharedLink, { stripAssetMetadata: !sharedLink.showExif });
|
||||
return response;
|
||||
}
|
||||
|
||||
async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
|
||||
@@ -235,7 +213,16 @@ export class SharedLinkService extends BaseService {
|
||||
};
|
||||
}
|
||||
|
||||
private asToken(sharedLink: { id: string; password: string }) {
|
||||
return this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`);
|
||||
private validateAndRefreshToken(sharedLink: SharedLink, dto: SharedLinkPasswordDto): string {
|
||||
const token = this.cryptoRepository.hashSha256(`${sharedLink.id}-${sharedLink.password}`);
|
||||
const sharedLinkTokens = dto.token?.split(',') || [];
|
||||
if (sharedLink.password !== dto.password && !sharedLinkTokens.includes(token)) {
|
||||
throw new UnauthorizedException('Invalid password');
|
||||
}
|
||||
|
||||
if (!sharedLinkTokens.includes(token)) {
|
||||
sharedLinkTokens.push(token);
|
||||
}
|
||||
return sharedLinkTokens.join(',');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ describe(VersionService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('onWebsocketConnectionEvent', () => {
|
||||
describe('onWebsocketConnection', () => {
|
||||
it('should send on_server_version client event', async () => {
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
@@ -143,5 +143,12 @@ describe(VersionService.name, () => {
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
|
||||
it('should not send a release notification when the version check is disabled', async () => {
|
||||
mocks.systemMetadata.get.mockResolvedValueOnce({ newVersionCheck: { enabled: false } });
|
||||
await sut.onWebsocketConnection({ userId: '42' });
|
||||
expect(mocks.websocket.clientSend).toHaveBeenCalledWith('on_server_version', '42', expect.any(SemVer));
|
||||
expect(mocks.websocket.clientSend).not.toHaveBeenCalledWith('on_new_release', '42', expect.any(Object));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,6 +105,12 @@ export class VersionService extends BaseService {
|
||||
@OnEvent({ name: 'WebsocketConnect' })
|
||||
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
||||
this.websocketRepository.clientSend('on_server_version', userId, serverVersion);
|
||||
|
||||
const { newVersionCheck } = await this.getConfig({ withCache: true });
|
||||
if (!newVersionCheck.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VersionCheckState);
|
||||
if (metadata) {
|
||||
this.websocketRepository.clientSend('on_new_release', userId, asNotification(metadata));
|
||||
|
||||
@@ -90,7 +90,7 @@ describe(SharedLinkService.name, () => {
|
||||
assetIds: assets.map(({ asset }) => asset.id),
|
||||
});
|
||||
|
||||
await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({
|
||||
await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({
|
||||
assets: assets.map(({ asset }) => expect.objectContaining({ id: asset.id })),
|
||||
});
|
||||
});
|
||||
@@ -114,7 +114,7 @@ describe(SharedLinkService.name, () => {
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
|
||||
await expect(sut.getMine({ user, sharedLink }, [])).resolves.toMatchObject({
|
||||
await expect(sut.getMine({ user, sharedLink }, {})).resolves.toMatchObject({
|
||||
assets: [expect.objectContaining({ id: asset.id })],
|
||||
});
|
||||
|
||||
@@ -122,6 +122,6 @@ describe(SharedLinkService.name, () => {
|
||||
assetIds: [asset.id],
|
||||
});
|
||||
|
||||
await expect(sut.getMine({ user, sharedLink }, [])).resolves.toHaveProperty('assets', []);
|
||||
await expect(sut.getMine({ user, sharedLink }, {})).resolves.toHaveProperty('assets', []);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.3",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -194,9 +194,7 @@
|
||||
|
||||
const closeEditor = async () => {
|
||||
if (editManager.hasAppliedEdits) {
|
||||
console.log(asset);
|
||||
const refreshedAsset = await getAssetInfo({ id: asset.id });
|
||||
console.log(refreshedAsset);
|
||||
onAssetChange?.(refreshedAsset);
|
||||
assetViewingStore.setAsset(refreshedAsset);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => editManager.resetAllChanges()}
|
||||
disabled={!editManager.hasChanges}
|
||||
disabled={!editManager.canReset}
|
||||
class="self-start"
|
||||
shape="round"
|
||||
size="small"
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { setSharedLink } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { sharedLinkLogin, SharedLinkType, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { getMySharedLink, SharedLinkType, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { Button, Logo, PasswordInput } from '@immich/ui';
|
||||
import { tick } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -39,7 +39,7 @@
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
try {
|
||||
sharedLink = await sharedLinkLogin({ key, slug, sharedLinkLoginDto: { password } });
|
||||
sharedLink = await getMySharedLink({ password, key, slug });
|
||||
setSharedLink(sharedLink);
|
||||
passwordRequired = false;
|
||||
title = (sharedLink.album ? sharedLink.album.albumName : $t('public_share')) + ' - Immich';
|
||||
|
||||
@@ -15,6 +15,7 @@ export interface EditToolManager {
|
||||
onDeactivate: () => void;
|
||||
resetAllChanges: () => Promise<void>;
|
||||
hasChanges: boolean;
|
||||
canReset: boolean;
|
||||
edits: EditAction[];
|
||||
}
|
||||
|
||||
@@ -41,19 +42,22 @@ export class EditManager {
|
||||
|
||||
currentAsset = $state<AssetResponseDto | null>(null);
|
||||
selectedTool = $state<EditTool | null>(null);
|
||||
hasChanges = $derived(this.tools.some((t) => t.manager.hasChanges));
|
||||
|
||||
// used to disable multiple confirm dialogs and mouse events while one is open
|
||||
isShowingConfirmDialog = $state(false);
|
||||
isApplyingEdits = $state(false);
|
||||
hasAppliedEdits = $state(false);
|
||||
|
||||
hasUnsavedChanges = $derived(this.tools.some((t) => t.manager.hasChanges) && !this.hasAppliedEdits);
|
||||
canReset = $derived(this.tools.some((t) => t.manager.canReset));
|
||||
|
||||
async closeConfirm(): Promise<boolean> {
|
||||
// Prevent multiple dialogs (usually happens with rapid escape key presses)
|
||||
if (this.isShowingConfirmDialog) {
|
||||
return false;
|
||||
}
|
||||
if (!this.hasChanges || this.hasAppliedEdits) {
|
||||
|
||||
if (!this.hasUnsavedChanges) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ type RegionConvertParams = {
|
||||
};
|
||||
|
||||
class TransformManager implements EditToolManager {
|
||||
hasChanges: boolean = $derived.by(() => this.checkEdits());
|
||||
canReset: boolean = $derived.by(() => this.checkEdits());
|
||||
hasChanges: boolean = $state(false);
|
||||
|
||||
darkenLevel = $state(0.65);
|
||||
isInteracting = $state(false);
|
||||
@@ -56,7 +57,7 @@ class TransformManager implements EditToolManager {
|
||||
cropAspectRatio = $state('free');
|
||||
originalImageSize = $state<ImageDimensions>({ width: 1000, height: 1000 });
|
||||
region = $state({ x: 0, y: 0, width: 100, height: 100 });
|
||||
preveiwImgSize = $derived({
|
||||
previewImageSize = $derived({
|
||||
width: this.cropImageSize.width * this.cropImageScale,
|
||||
height: this.cropImageSize.height * this.cropImageScale,
|
||||
});
|
||||
@@ -73,6 +74,7 @@ class TransformManager implements EditToolManager {
|
||||
edits = $derived.by(() => this.getEdits());
|
||||
|
||||
setAspectRatio(aspectRatio: string) {
|
||||
this.hasChanges = true;
|
||||
this.cropAspectRatio = aspectRatio;
|
||||
|
||||
if (!this.imgElement || !this.cropAreaEl) {
|
||||
@@ -88,8 +90,8 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
checkEdits() {
|
||||
return (
|
||||
Math.abs(this.preveiwImgSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.height - this.region.height) > 2 ||
|
||||
Math.abs(this.previewImageSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.previewImageSize.height - this.region.height) > 2 ||
|
||||
this.mirrorHorizontal ||
|
||||
this.mirrorVertical ||
|
||||
this.normalizedRotation !== 0
|
||||
@@ -98,8 +100,8 @@ class TransformManager implements EditToolManager {
|
||||
|
||||
checkCropEdits() {
|
||||
return (
|
||||
Math.abs(this.preveiwImgSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.preveiwImgSize.height - this.region.height) > 2
|
||||
Math.abs(this.previewImageSize.width - this.region.width) > 2 ||
|
||||
Math.abs(this.previewImageSize.height - this.region.height) > 2
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,9 +234,12 @@ class TransformManager implements EditToolManager {
|
||||
this.originalImageSize = { width: 1000, height: 1000 };
|
||||
this.cropImageScale = 1;
|
||||
this.cropAspectRatio = 'free';
|
||||
this.hasChanges = false;
|
||||
}
|
||||
|
||||
mirror(axis: 'horizontal' | 'vertical') {
|
||||
this.hasChanges = true;
|
||||
|
||||
if (this.imageRotation % 180 !== 0) {
|
||||
axis = axis === 'horizontal' ? 'vertical' : 'horizontal';
|
||||
}
|
||||
@@ -247,6 +252,8 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
|
||||
async rotate(angle: number) {
|
||||
this.hasChanges = true;
|
||||
|
||||
this.imageRotation += angle;
|
||||
await tick();
|
||||
this.onImageLoad();
|
||||
@@ -760,6 +767,7 @@ class TransformManager implements EditToolManager {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasChanges = true;
|
||||
const newX = Math.max(0, Math.min(mouseX - this.dragOffset.x, cropArea.clientWidth - this.region.width));
|
||||
const newY = Math.max(0, Math.min(mouseY - this.dragOffset.y, cropArea.clientHeight - this.region.height));
|
||||
|
||||
@@ -781,6 +789,7 @@ class TransformManager implements EditToolManager {
|
||||
}
|
||||
this.fadeOverlay(false);
|
||||
|
||||
this.hasChanges = true;
|
||||
const { x, y, width, height } = crop;
|
||||
const minSize = 50;
|
||||
let newRegion = { ...crop };
|
||||
|
||||
@@ -49,7 +49,7 @@ export const loadSharedLink = async ({
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
if (isHttpError(error) && error.data.message === 'Password required') {
|
||||
if (isHttpError(error) && error.data.message === 'Invalid password') {
|
||||
return {
|
||||
...common,
|
||||
passwordRequired: true,
|
||||
|
||||
Reference in New Issue
Block a user