mirror of
https://github.com/immich-app/immich.git
synced 2025-06-23 15:30:51 -04:00
feat(web): rotate image
This commit is contained in:
parent
dbbefde98d
commit
9cd0871178
@ -602,6 +602,8 @@
|
|||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
|
"rotate_left": "Rotate left",
|
||||||
|
"rotate_right": "Rotate right",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
"error_loading_image": "Error loading image",
|
"error_loading_image": "Error loading image",
|
||||||
"error_title": "Error - Something went wrong",
|
"error_title": "Error - Something went wrong",
|
||||||
@ -644,6 +646,7 @@
|
|||||||
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
"quota_higher_than_disk_size": "You set a quota higher than the disk size",
|
||||||
"repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}",
|
"repair_unable_to_check_items": "Unable to check {count, select, one {item} other {items}}",
|
||||||
"unable_to_add_album_users": "Unable to add users to album",
|
"unable_to_add_album_users": "Unable to add users to album",
|
||||||
|
"unable_to_rotate_image": "Unable to rotate image",
|
||||||
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
|
"unable_to_add_assets_to_shared_link": "Unable to add assets to shared link",
|
||||||
"unable_to_add_comment": "Unable to add comment",
|
"unable_to_add_comment": "Unable to add comment",
|
||||||
"unable_to_add_exclusion_pattern": "Unable to add exclusion pattern",
|
"unable_to_add_exclusion_pattern": "Unable to add exclusion pattern",
|
||||||
|
105
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
105
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
@ -20,6 +20,7 @@ class AssetBulkUpdateDto {
|
|||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.orientation,
|
||||||
this.rating,
|
this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,6 +68,8 @@ class AssetBulkUpdateDto {
|
|||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
|
AssetBulkUpdateDtoOrientationEnum? orientation;
|
||||||
|
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
///
|
///
|
||||||
@ -86,6 +89,7 @@ class AssetBulkUpdateDto {
|
|||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
other.longitude == longitude &&
|
other.longitude == longitude &&
|
||||||
|
other.orientation == orientation &&
|
||||||
other.rating == rating;
|
other.rating == rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -98,10 +102,11 @@ class AssetBulkUpdateDto {
|
|||||||
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
(isFavorite == null ? 0 : isFavorite!.hashCode) +
|
||||||
(latitude == null ? 0 : latitude!.hashCode) +
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
(longitude == null ? 0 : longitude!.hashCode) +
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
|
(orientation == null ? 0 : orientation!.hashCode) +
|
||||||
(rating == null ? 0 : rating!.hashCode);
|
(rating == null ? 0 : rating!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]';
|
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation, rating=$rating]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -136,6 +141,11 @@ class AssetBulkUpdateDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'longitude'] = null;
|
// json[r'longitude'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.orientation != null) {
|
||||||
|
json[r'orientation'] = this.orientation;
|
||||||
|
} else {
|
||||||
|
// json[r'orientation'] = null;
|
||||||
|
}
|
||||||
if (this.rating != null) {
|
if (this.rating != null) {
|
||||||
json[r'rating'] = this.rating;
|
json[r'rating'] = this.rating;
|
||||||
} else {
|
} else {
|
||||||
@ -162,6 +172,7 @@ class AssetBulkUpdateDto {
|
|||||||
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
|
||||||
latitude: num.parse('${json[r'latitude']}'),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
longitude: num.parse('${json[r'longitude']}'),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
|
orientation: AssetBulkUpdateDtoOrientationEnum.fromJson(json[r'orientation']),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: num.parse('${json[r'rating']}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -214,3 +225,95 @@ class AssetBulkUpdateDto {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AssetBulkUpdateDtoOrientationEnum {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const AssetBulkUpdateDtoOrientationEnum._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value.toString();
|
||||||
|
|
||||||
|
int toJson() => value;
|
||||||
|
|
||||||
|
static const number1 = AssetBulkUpdateDtoOrientationEnum._(1);
|
||||||
|
static const number2 = AssetBulkUpdateDtoOrientationEnum._(2);
|
||||||
|
static const number3 = AssetBulkUpdateDtoOrientationEnum._(3);
|
||||||
|
static const number4 = AssetBulkUpdateDtoOrientationEnum._(4);
|
||||||
|
static const number5 = AssetBulkUpdateDtoOrientationEnum._(5);
|
||||||
|
static const number6 = AssetBulkUpdateDtoOrientationEnum._(6);
|
||||||
|
static const number7 = AssetBulkUpdateDtoOrientationEnum._(7);
|
||||||
|
static const number8 = AssetBulkUpdateDtoOrientationEnum._(8);
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][AssetBulkUpdateDtoOrientationEnum].
|
||||||
|
static const values = <AssetBulkUpdateDtoOrientationEnum>[
|
||||||
|
number1,
|
||||||
|
number2,
|
||||||
|
number3,
|
||||||
|
number4,
|
||||||
|
number5,
|
||||||
|
number6,
|
||||||
|
number7,
|
||||||
|
number8,
|
||||||
|
];
|
||||||
|
|
||||||
|
static AssetBulkUpdateDtoOrientationEnum? fromJson(dynamic value) => AssetBulkUpdateDtoOrientationEnumTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<AssetBulkUpdateDtoOrientationEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <AssetBulkUpdateDtoOrientationEnum>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = AssetBulkUpdateDtoOrientationEnum.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [AssetBulkUpdateDtoOrientationEnum] to int,
|
||||||
|
/// and [decode] dynamic data back to [AssetBulkUpdateDtoOrientationEnum].
|
||||||
|
class AssetBulkUpdateDtoOrientationEnumTypeTransformer {
|
||||||
|
factory AssetBulkUpdateDtoOrientationEnumTypeTransformer() => _instance ??= const AssetBulkUpdateDtoOrientationEnumTypeTransformer._();
|
||||||
|
|
||||||
|
const AssetBulkUpdateDtoOrientationEnumTypeTransformer._();
|
||||||
|
|
||||||
|
int encode(AssetBulkUpdateDtoOrientationEnum data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a AssetBulkUpdateDtoOrientationEnum.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
AssetBulkUpdateDtoOrientationEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case 1: return AssetBulkUpdateDtoOrientationEnum.number1;
|
||||||
|
case 2: return AssetBulkUpdateDtoOrientationEnum.number2;
|
||||||
|
case 3: return AssetBulkUpdateDtoOrientationEnum.number3;
|
||||||
|
case 4: return AssetBulkUpdateDtoOrientationEnum.number4;
|
||||||
|
case 5: return AssetBulkUpdateDtoOrientationEnum.number5;
|
||||||
|
case 6: return AssetBulkUpdateDtoOrientationEnum.number6;
|
||||||
|
case 7: return AssetBulkUpdateDtoOrientationEnum.number7;
|
||||||
|
case 8: return AssetBulkUpdateDtoOrientationEnum.number8;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [AssetBulkUpdateDtoOrientationEnumTypeTransformer] instance.
|
||||||
|
static AssetBulkUpdateDtoOrientationEnumTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
105
mobile/openapi/lib/model/update_asset_dto.dart
generated
105
mobile/openapi/lib/model/update_asset_dto.dart
generated
@ -20,6 +20,7 @@ class UpdateAssetDto {
|
|||||||
this.latitude,
|
this.latitude,
|
||||||
this.livePhotoVideoId,
|
this.livePhotoVideoId,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.orientation,
|
||||||
this.rating,
|
this.rating,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -73,6 +74,8 @@ class UpdateAssetDto {
|
|||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
|
UpdateAssetDtoOrientationEnum? orientation;
|
||||||
|
|
||||||
/// Minimum value: -1
|
/// Minimum value: -1
|
||||||
/// Maximum value: 5
|
/// Maximum value: 5
|
||||||
///
|
///
|
||||||
@ -92,6 +95,7 @@ class UpdateAssetDto {
|
|||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
other.livePhotoVideoId == livePhotoVideoId &&
|
other.livePhotoVideoId == livePhotoVideoId &&
|
||||||
other.longitude == longitude &&
|
other.longitude == longitude &&
|
||||||
|
other.orientation == orientation &&
|
||||||
other.rating == rating;
|
other.rating == rating;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -104,10 +108,11 @@ class UpdateAssetDto {
|
|||||||
(latitude == null ? 0 : latitude!.hashCode) +
|
(latitude == null ? 0 : latitude!.hashCode) +
|
||||||
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
(livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) +
|
||||||
(longitude == null ? 0 : longitude!.hashCode) +
|
(longitude == null ? 0 : longitude!.hashCode) +
|
||||||
|
(orientation == null ? 0 : orientation!.hashCode) +
|
||||||
(rating == null ? 0 : rating!.hashCode);
|
(rating == null ? 0 : rating!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, rating=$rating]';
|
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, livePhotoVideoId=$livePhotoVideoId, longitude=$longitude, orientation=$orientation, rating=$rating]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -146,6 +151,11 @@ class UpdateAssetDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'longitude'] = null;
|
// json[r'longitude'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.orientation != null) {
|
||||||
|
json[r'orientation'] = this.orientation;
|
||||||
|
} else {
|
||||||
|
// json[r'orientation'] = null;
|
||||||
|
}
|
||||||
if (this.rating != null) {
|
if (this.rating != null) {
|
||||||
json[r'rating'] = this.rating;
|
json[r'rating'] = this.rating;
|
||||||
} else {
|
} else {
|
||||||
@ -170,6 +180,7 @@ class UpdateAssetDto {
|
|||||||
latitude: num.parse('${json[r'latitude']}'),
|
latitude: num.parse('${json[r'latitude']}'),
|
||||||
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
livePhotoVideoId: mapValueOfType<String>(json, r'livePhotoVideoId'),
|
||||||
longitude: num.parse('${json[r'longitude']}'),
|
longitude: num.parse('${json[r'longitude']}'),
|
||||||
|
orientation: UpdateAssetDtoOrientationEnum.fromJson(json[r'orientation']),
|
||||||
rating: num.parse('${json[r'rating']}'),
|
rating: num.parse('${json[r'rating']}'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -221,3 +232,95 @@ class UpdateAssetDto {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateAssetDtoOrientationEnum {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const UpdateAssetDtoOrientationEnum._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final int value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value.toString();
|
||||||
|
|
||||||
|
int toJson() => value;
|
||||||
|
|
||||||
|
static const number1 = UpdateAssetDtoOrientationEnum._(1);
|
||||||
|
static const number2 = UpdateAssetDtoOrientationEnum._(2);
|
||||||
|
static const number3 = UpdateAssetDtoOrientationEnum._(3);
|
||||||
|
static const number4 = UpdateAssetDtoOrientationEnum._(4);
|
||||||
|
static const number5 = UpdateAssetDtoOrientationEnum._(5);
|
||||||
|
static const number6 = UpdateAssetDtoOrientationEnum._(6);
|
||||||
|
static const number7 = UpdateAssetDtoOrientationEnum._(7);
|
||||||
|
static const number8 = UpdateAssetDtoOrientationEnum._(8);
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][UpdateAssetDtoOrientationEnum].
|
||||||
|
static const values = <UpdateAssetDtoOrientationEnum>[
|
||||||
|
number1,
|
||||||
|
number2,
|
||||||
|
number3,
|
||||||
|
number4,
|
||||||
|
number5,
|
||||||
|
number6,
|
||||||
|
number7,
|
||||||
|
number8,
|
||||||
|
];
|
||||||
|
|
||||||
|
static UpdateAssetDtoOrientationEnum? fromJson(dynamic value) => UpdateAssetDtoOrientationEnumTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<UpdateAssetDtoOrientationEnum> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <UpdateAssetDtoOrientationEnum>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = UpdateAssetDtoOrientationEnum.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [UpdateAssetDtoOrientationEnum] to int,
|
||||||
|
/// and [decode] dynamic data back to [UpdateAssetDtoOrientationEnum].
|
||||||
|
class UpdateAssetDtoOrientationEnumTypeTransformer {
|
||||||
|
factory UpdateAssetDtoOrientationEnumTypeTransformer() => _instance ??= const UpdateAssetDtoOrientationEnumTypeTransformer._();
|
||||||
|
|
||||||
|
const UpdateAssetDtoOrientationEnumTypeTransformer._();
|
||||||
|
|
||||||
|
int encode(UpdateAssetDtoOrientationEnum data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a UpdateAssetDtoOrientationEnum.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
UpdateAssetDtoOrientationEnum? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case 1: return UpdateAssetDtoOrientationEnum.number1;
|
||||||
|
case 2: return UpdateAssetDtoOrientationEnum.number2;
|
||||||
|
case 3: return UpdateAssetDtoOrientationEnum.number3;
|
||||||
|
case 4: return UpdateAssetDtoOrientationEnum.number4;
|
||||||
|
case 5: return UpdateAssetDtoOrientationEnum.number5;
|
||||||
|
case 6: return UpdateAssetDtoOrientationEnum.number6;
|
||||||
|
case 7: return UpdateAssetDtoOrientationEnum.number7;
|
||||||
|
case 8: return UpdateAssetDtoOrientationEnum.number8;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [UpdateAssetDtoOrientationEnumTypeTransformer] instance.
|
||||||
|
static UpdateAssetDtoOrientationEnumTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -7963,6 +7963,21 @@
|
|||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"orientation": {
|
||||||
|
"enum": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"maximum": 8,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
@ -12880,6 +12895,21 @@
|
|||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"orientation": {
|
||||||
|
"enum": [
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8
|
||||||
|
],
|
||||||
|
"maximum": 8,
|
||||||
|
"minimum": 1,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"rating": {
|
"rating": {
|
||||||
"maximum": 5,
|
"maximum": 5,
|
||||||
"minimum": -1,
|
"minimum": -1,
|
||||||
|
@ -391,6 +391,7 @@ export type AssetBulkUpdateDto = {
|
|||||||
isFavorite?: boolean;
|
isFavorite?: boolean;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
orientation?: Orientation;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
};
|
};
|
||||||
export type AssetBulkUploadCheckItem = {
|
export type AssetBulkUploadCheckItem = {
|
||||||
@ -439,6 +440,7 @@ export type UpdateAssetDto = {
|
|||||||
latitude?: number;
|
latitude?: number;
|
||||||
livePhotoVideoId?: string | null;
|
livePhotoVideoId?: string | null;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
orientation?: Orientation;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
};
|
};
|
||||||
export type AssetMediaReplaceDto = {
|
export type AssetMediaReplaceDto = {
|
||||||
@ -3481,6 +3483,16 @@ export enum AssetMediaStatus {
|
|||||||
Replaced = "replaced",
|
Replaced = "replaced",
|
||||||
Duplicate = "duplicate"
|
Duplicate = "duplicate"
|
||||||
}
|
}
|
||||||
|
export enum Orientation {
|
||||||
|
$1 = 1,
|
||||||
|
$2 = 2,
|
||||||
|
$3 = 3,
|
||||||
|
$4 = 4,
|
||||||
|
$5 = 5,
|
||||||
|
$6 = 6,
|
||||||
|
$7 = 7,
|
||||||
|
$8 = 8
|
||||||
|
}
|
||||||
export enum Action {
|
export enum Action {
|
||||||
Accept = "accept",
|
Accept = "accept",
|
||||||
Reject = "reject"
|
Reject = "reject"
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType, ExifOrientation } from 'src/enum';
|
||||||
import { AssetStats } from 'src/repositories/asset.repository';
|
import { AssetStats } from 'src/repositories/asset.repository';
|
||||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
@ -54,6 +54,12 @@ export class UpdateAssetBase {
|
|||||||
@Max(5)
|
@Max(5)
|
||||||
@Min(-1)
|
@Min(-1)
|
||||||
rating?: number;
|
rating?: number;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@Min(1)
|
||||||
|
@Max(8)
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
orientation?: ExifOrientation;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||||
|
@ -101,6 +101,7 @@ export class MetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||||
|
this.logger.verbose(`Writing tags ${JSON.stringify(tags)} to ${path}`);
|
||||||
try {
|
try {
|
||||||
await this.exiftool.write(path, tags);
|
await this.exiftool.write(path, tags);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -100,7 +100,7 @@ export class AssetService extends BaseService {
|
|||||||
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] });
|
||||||
|
|
||||||
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, rating, orientation, ...rest } = dto;
|
||||||
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
const repos = { asset: this.assetRepository, event: this.eventRepository };
|
||||||
|
|
||||||
let previousMotion: AssetEntity | null = null;
|
let previousMotion: AssetEntity | null = null;
|
||||||
@ -113,7 +113,7 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating, orientation });
|
||||||
|
|
||||||
const asset = await this.assetRepository.update({ id, ...rest });
|
const asset = await this.assetRepository.update({ id, ...rest });
|
||||||
|
|
||||||
@ -129,11 +129,12 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
const { ids, dateTimeOriginal, latitude, longitude, orientation, ...options } = dto;
|
||||||
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids });
|
||||||
|
|
||||||
|
// TODO rewrite this to support batching
|
||||||
for (const id of ids) {
|
for (const id of ids) {
|
||||||
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude, orientation });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -284,11 +285,14 @@ export class AssetService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async updateMetadata(dto: ISidecarWriteJob) {
|
private async updateMetadata(dto: ISidecarWriteJob) {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating, orientation } = dto;
|
||||||
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
|
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating, orientation }, _.isUndefined);
|
||||||
if (Object.keys(writes).length > 0) {
|
if (Object.keys(writes).length > 0) {
|
||||||
await this.assetRepository.upsertExif({ assetId: id, ...writes });
|
await this.assetRepository.upsertExif({ assetId: id, ...writes });
|
||||||
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
|
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } });
|
||||||
|
if (orientation !== undefined) {
|
||||||
|
await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id, notify: true } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,7 +215,7 @@ export class MediaService extends BaseService {
|
|||||||
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
|
||||||
const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true';
|
const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true';
|
||||||
|
|
||||||
const orientation = useExtracted && asset.exifInfo?.orientation ? Number(asset.exifInfo.orientation) : undefined;
|
const orientation = asset.exifInfo?.orientation ? Number(asset.exifInfo.orientation) : undefined;
|
||||||
const decodeOptions = { colorspace, processInvalidImages, size: image.preview.size, orientation };
|
const decodeOptions = { colorspace, processInvalidImages, size: image.preview.size, orientation };
|
||||||
const { data, info } = await this.mediaRepository.decodeImage(inputPath, decodeOptions);
|
const { data, info } = await this.mediaRepository.decodeImage(inputPath, decodeOptions);
|
||||||
|
|
||||||
|
@ -295,7 +295,7 @@ export class MetadataService extends BaseService {
|
|||||||
|
|
||||||
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
|
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
|
||||||
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
|
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags, orientation } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
|
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@ -311,6 +311,7 @@ export class MetadataService extends BaseService {
|
|||||||
DateTimeOriginal: dateTimeOriginal,
|
DateTimeOriginal: dateTimeOriginal,
|
||||||
GPSLatitude: latitude,
|
GPSLatitude: latitude,
|
||||||
GPSLongitude: longitude,
|
GPSLongitude: longitude,
|
||||||
|
'Orientation#': orientation,
|
||||||
Rating: rating,
|
Rating: rating,
|
||||||
TagsList: tags ? tagsList : undefined,
|
TagsList: tags ? tagsList : undefined,
|
||||||
},
|
},
|
||||||
|
@ -222,6 +222,7 @@ export interface ISidecarWriteJob extends IEntityJob {
|
|||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
rating?: number;
|
rating?: number;
|
||||||
|
orientation?: ExifOrientation;
|
||||||
tags?: true;
|
tags?: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,7 @@ type ActionMap = {
|
|||||||
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
|
[AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto };
|
||||||
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
|
[AssetAction.UNSTACK]: { assets: AssetResponseDto[] };
|
||||||
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
|
[AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: AssetResponseDto };
|
||||||
|
[AssetAction.ROTATE]: { asset: AssetResponseDto; counterclockwise: boolean };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Action = {
|
export type Action = {
|
||||||
|
@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
|
import { AssetAction, ExifOrientation } from '$lib/constants';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { updateAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
|
import { mdiRotateLeft, mdiRotateRight } from '@mdi/js';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { OnAction } from './action';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
asset: AssetResponseDto;
|
||||||
|
onAction: OnAction;
|
||||||
|
counterclockwise?: boolean;
|
||||||
|
menuItem?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { asset, onAction, counterclockwise = false, menuItem }: Props = $props();
|
||||||
|
|
||||||
|
const icon = $derived(counterclockwise ? mdiRotateLeft : mdiRotateRight);
|
||||||
|
const text = $derived(counterclockwise ? $t('rotate_left') : $t('rotate_right'));
|
||||||
|
|
||||||
|
const getNextOrientation = (current: number) => {
|
||||||
|
switch (current) {
|
||||||
|
case -1:
|
||||||
|
case 0:
|
||||||
|
case ExifOrientation.Horizontal: {
|
||||||
|
return ExifOrientation.Rotate90CW;
|
||||||
|
}
|
||||||
|
case ExifOrientation.Rotate90CW: {
|
||||||
|
return ExifOrientation.Rotate180;
|
||||||
|
}
|
||||||
|
case ExifOrientation.Rotate180: {
|
||||||
|
return ExifOrientation.Rotate270CW;
|
||||||
|
}
|
||||||
|
case ExifOrientation.Rotate270CW: {
|
||||||
|
return ExifOrientation.Horizontal;
|
||||||
|
}
|
||||||
|
case ExifOrientation.MirrorHorizontal: {
|
||||||
|
return ExifOrientation.MirrorHorizontalRotate90CW;
|
||||||
|
}
|
||||||
|
case ExifOrientation.MirrorHorizontalRotate90CW: {
|
||||||
|
return ExifOrientation.MirrorVertical;
|
||||||
|
}
|
||||||
|
case ExifOrientation.MirrorVertical: {
|
||||||
|
return ExifOrientation.MirrorHorizontalRotate270CW;
|
||||||
|
}
|
||||||
|
case ExifOrientation.MirrorHorizontalRotate270CW: {
|
||||||
|
return ExifOrientation.MirrorVertical;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRotate = async () => {
|
||||||
|
const current = Number(asset.exifInfo?.orientation);
|
||||||
|
if (!current && current !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orientation = counterclockwise
|
||||||
|
? getNextOrientation(getNextOrientation(getNextOrientation(current)))
|
||||||
|
: getNextOrientation(current);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await updateAsset({ id: asset.id, updateAssetDto: { orientation } });
|
||||||
|
|
||||||
|
// TODO: remove if/when there is immediate UI feedback on image rotation (css animation)
|
||||||
|
notificationController.show({ message: text, type: NotificationType.Info });
|
||||||
|
|
||||||
|
onAction({ type: AssetAction.ROTATE, asset: data, counterclockwise });
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('errors.unable_to_rotate_image'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:window use:shortcut={{ shortcut: { key: 'r', shift: counterclockwise }, onShortcut: handleRotate }} />
|
||||||
|
|
||||||
|
{#if menuItem}
|
||||||
|
<MenuOption {icon} onClick={handleRotate} {text} />
|
||||||
|
{:else}
|
||||||
|
<CircleIconButton
|
||||||
|
color="opaque"
|
||||||
|
icon={counterclockwise ? mdiRotateLeft : mdiRotateRight}
|
||||||
|
title={text}
|
||||||
|
onclick={handleRotate}
|
||||||
|
/>
|
||||||
|
{/if}
|
@ -7,14 +7,15 @@
|
|||||||
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
import DeleteAction from '$lib/components/asset-viewer/actions/delete-action.svelte';
|
||||||
import DownloadAction from '$lib/components/asset-viewer/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/asset-viewer/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
|
import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
|
||||||
|
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
|
||||||
import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte';
|
import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte';
|
||||||
|
import RotateAction from '$lib/components/asset-viewer/actions/rotate-action.svelte';
|
||||||
import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
|
import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
|
||||||
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
|
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
|
||||||
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
|
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
|
||||||
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
|
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
|
||||||
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
|
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
|
||||||
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
import UnstackAction from '$lib/components/asset-viewer/actions/unstack-action.svelte';
|
||||||
import KeepThisDeleteOthersAction from '$lib/components/asset-viewer/actions/keep-this-delete-others.svelte';
|
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
@ -22,6 +23,7 @@
|
|||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
||||||
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import {
|
import {
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
@ -45,9 +47,8 @@
|
|||||||
mdiPresentationPlay,
|
mdiPresentationPlay,
|
||||||
mdiUpload,
|
mdiUpload,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
@ -87,6 +88,9 @@
|
|||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||||
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
||||||
|
let canRotate = $derived(
|
||||||
|
asset.type === AssetTypeEnum.Image && !asset.livePhotoVideoId && asset.exifInfo?.orientation !== undefined,
|
||||||
|
);
|
||||||
// $: showEditorButton =
|
// $: showEditorButton =
|
||||||
// isOwner &&
|
// isOwner &&
|
||||||
// asset.type === AssetTypeEnum.Image &&
|
// asset.type === AssetTypeEnum.Image &&
|
||||||
@ -180,6 +184,11 @@
|
|||||||
<SetProfilePictureAction {asset} />
|
<SetProfilePictureAction {asset} />
|
||||||
{/if}
|
{/if}
|
||||||
<ArchiveAction {asset} {onAction} />
|
<ArchiveAction {asset} {onAction} />
|
||||||
|
|
||||||
|
{#if canRotate}
|
||||||
|
<RotateAction {asset} {onAction} counterclockwise menuItem />
|
||||||
|
<RotateAction {asset} {onAction} menuItem />
|
||||||
|
{/if}
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiUpload}
|
icon={mdiUpload}
|
||||||
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
onClick={() => openFileUploadDialog({ multiple: false, assetId: asset.id })}
|
||||||
|
@ -43,10 +43,10 @@
|
|||||||
import DetailPanel from './detail-panel.svelte';
|
import DetailPanel from './detail-panel.svelte';
|
||||||
import CropArea from './editor/crop-tool/crop-area.svelte';
|
import CropArea from './editor/crop-tool/crop-area.svelte';
|
||||||
import EditorPanel from './editor/editor-panel.svelte';
|
import EditorPanel from './editor/editor-panel.svelte';
|
||||||
|
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
|
||||||
import PhotoViewer from './photo-viewer.svelte';
|
import PhotoViewer from './photo-viewer.svelte';
|
||||||
import SlideshowBar from './slideshow-bar.svelte';
|
import SlideshowBar from './slideshow-bar.svelte';
|
||||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||||
import ImagePanoramaViewer from './image-panorama-viewer.svelte';
|
|
||||||
|
|
||||||
type HasAsset = boolean;
|
type HasAsset = boolean;
|
||||||
|
|
||||||
@ -190,7 +190,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssetUpdate = (assetUpdate: AssetResponseDto) => {
|
const onAssetUpdate = ({ asset: assetUpdate }: { event: 'upload' | 'update'; asset: AssetResponseDto }) => {
|
||||||
if (assetUpdate.id === asset.id) {
|
if (assetUpdate.id === asset.id) {
|
||||||
asset = assetUpdate;
|
asset = assetUpdate;
|
||||||
}
|
}
|
||||||
@ -198,8 +198,8 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
unsubscribes.push(
|
unsubscribes.push(
|
||||||
websocketEvents.on('on_upload_success', onAssetUpdate),
|
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
|
||||||
websocketEvents.on('on_asset_update', onAssetUpdate),
|
websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
|
||||||
);
|
);
|
||||||
|
|
||||||
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
||||||
@ -377,6 +377,7 @@
|
|||||||
case AssetAction.KEEP_THIS_DELETE_OTHERS:
|
case AssetAction.KEEP_THIS_DELETE_OTHERS:
|
||||||
case AssetAction.UNSTACK: {
|
case AssetAction.UNSTACK: {
|
||||||
closeViewer();
|
closeViewer();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -483,7 +484,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={previewStackedAsset.id}
|
assetId={previewStackedAsset.id}
|
||||||
checksum={previewStackedAsset.checksum}
|
cacheKey={previewStackedAsset.thumbhash}
|
||||||
projectionType={previewStackedAsset.exifInfo?.projectionType}
|
projectionType={previewStackedAsset.exifInfo?.projectionType}
|
||||||
loopVideo={true}
|
loopVideo={true}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
@ -500,7 +501,7 @@
|
|||||||
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.livePhotoVideoId}
|
assetId={asset.livePhotoVideoId}
|
||||||
checksum={asset.checksum}
|
cacheKey={asset.thumbhash}
|
||||||
projectionType={asset.exifInfo?.projectionType}
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
@ -529,7 +530,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.id}
|
assetId={asset.id}
|
||||||
checksum={asset.checksum}
|
cacheKey={asset.thumbhash}
|
||||||
projectionType={asset.exifInfo?.projectionType}
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
img = new Image();
|
img = new Image();
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
img.src = getAssetOriginalUrl({ id: asset.id, checksum: asset.checksum });
|
img.src = getAssetOriginalUrl({ id: asset.id, cacheKey: asset.thumbhash });
|
||||||
|
|
||||||
img.addEventListener('load', () => onImageLoad(true));
|
img.addEventListener('load', () => onImageLoad(true));
|
||||||
img.addEventListener('error', (error) => {
|
img.addEventListener('error', (error) => {
|
||||||
|
@ -40,7 +40,7 @@ describe('PhotoViewer component', () => {
|
|||||||
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
size: AssetMediaSize.Preview,
|
size: AssetMediaSize.Preview,
|
||||||
checksum: asset.checksum,
|
cacheKey: asset.thumbhash,
|
||||||
});
|
});
|
||||||
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
});
|
});
|
||||||
@ -50,7 +50,7 @@ describe('PhotoViewer component', () => {
|
|||||||
render(PhotoViewer, { asset });
|
render(PhotoViewer, { asset });
|
||||||
|
|
||||||
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
||||||
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, checksum: asset.checksum });
|
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads original for shared link when download permission is true and showMetadata permission is true', () => {
|
it('loads original for shared link when download permission is true and showMetadata permission is true', () => {
|
||||||
@ -59,7 +59,7 @@ describe('PhotoViewer component', () => {
|
|||||||
render(PhotoViewer, { asset, sharedLink });
|
render(PhotoViewer, { asset, sharedLink });
|
||||||
|
|
||||||
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
||||||
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, checksum: asset.checksum });
|
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not loads original image when shared link download permission is false', () => {
|
it('not loads original image when shared link download permission is false', () => {
|
||||||
@ -70,7 +70,7 @@ describe('PhotoViewer component', () => {
|
|||||||
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
size: AssetMediaSize.Preview,
|
size: AssetMediaSize.Preview,
|
||||||
checksum: asset.checksum,
|
cacheKey: asset.thumbhash,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
@ -84,7 +84,7 @@ describe('PhotoViewer component', () => {
|
|||||||
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
size: AssetMediaSize.Preview,
|
size: AssetMediaSize.Preview,
|
||||||
checksum: asset.checksum,
|
cacheKey: asset.thumbhash,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
|
@ -70,19 +70,19 @@
|
|||||||
for (const preloadAsset of preloadAssets || []) {
|
for (const preloadAsset of preloadAssets || []) {
|
||||||
if (preloadAsset.type === AssetTypeEnum.Image) {
|
if (preloadAsset.type === AssetTypeEnum.Image) {
|
||||||
let img = new Image();
|
let img = new Image();
|
||||||
img.src = getAssetUrl(preloadAsset.id, useOriginal, preloadAsset.checksum);
|
img.src = getAssetUrl(preloadAsset.id, useOriginal, preloadAsset.thumbhash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAssetUrl = (id: string, useOriginal: boolean, checksum: string) => {
|
const getAssetUrl = (id: string, useOriginal: boolean, cacheKey: string | null) => {
|
||||||
if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
|
if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
|
||||||
return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, checksum });
|
return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
return useOriginal
|
return useOriginal
|
||||||
? getAssetOriginalUrl({ id, checksum })
|
? getAssetOriginalUrl({ id, cacheKey })
|
||||||
: getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, checksum });
|
: getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
copyImage = async () => {
|
copyImage = async () => {
|
||||||
@ -144,7 +144,7 @@
|
|||||||
loader?.removeEventListener('error', onerror);
|
loader?.removeEventListener('error', onerror);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
let isWebCompatible = $derived(isWebCompatibleImage(asset));
|
let isWebCompatible = $derived(isWebCompatibleImage(asset) && !asset?.exifInfo?.orientation);
|
||||||
let useOriginalByDefault = $derived(isWebCompatible && $alwaysLoadOriginalFile);
|
let useOriginalByDefault = $derived(isWebCompatible && $alwaysLoadOriginalFile);
|
||||||
// when true, will force loading of the original image
|
// when true, will force loading of the original image
|
||||||
|
|
||||||
@ -158,7 +158,7 @@
|
|||||||
preload(useOriginalImage, preloadAssets);
|
preload(useOriginalImage, preloadAssets);
|
||||||
});
|
});
|
||||||
|
|
||||||
let imageLoaderUrl = $derived(getAssetUrl(asset.id, useOriginalImage, asset.checksum));
|
let imageLoaderUrl = $derived(getAssetUrl(asset.id, useOriginalImage, asset.thumbhash));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
loopVideo: boolean;
|
loopVideo: boolean;
|
||||||
checksum: string;
|
cacheKey: string | null;
|
||||||
onPreviousAsset?: () => void;
|
onPreviousAsset?: () => void;
|
||||||
onNextAsset?: () => void;
|
onNextAsset?: () => void;
|
||||||
onVideoEnded?: () => void;
|
onVideoEnded?: () => void;
|
||||||
@ -24,7 +24,7 @@
|
|||||||
let {
|
let {
|
||||||
assetId,
|
assetId,
|
||||||
loopVideo,
|
loopVideo,
|
||||||
checksum,
|
cacheKey,
|
||||||
onPreviousAsset = () => {},
|
onPreviousAsset = () => {},
|
||||||
onNextAsset = () => {},
|
onNextAsset = () => {},
|
||||||
onVideoEnded = () => {},
|
onVideoEnded = () => {},
|
||||||
@ -39,7 +39,7 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (videoPlayer) {
|
if (videoPlayer) {
|
||||||
assetFileUrl = getAssetPlaybackUrl({ id: assetId, checksum });
|
assetFileUrl = getAssetPlaybackUrl({ id: assetId, cacheKey });
|
||||||
forceMuted = false;
|
forceMuted = false;
|
||||||
videoPlayer.load();
|
videoPlayer.load();
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@
|
|||||||
onclose={() => onClose()}
|
onclose={() => onClose()}
|
||||||
muted={forceMuted || $videoViewerMuted}
|
muted={forceMuted || $videoViewerMuted}
|
||||||
bind:volume={$videoViewerVolume}
|
bind:volume={$videoViewerVolume}
|
||||||
poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, checksum })}
|
poster={getAssetThumbnailUrl({ id: assetId, size: AssetMediaSize.Preview, cacheKey })}
|
||||||
src={assetFileUrl}
|
src={assetFileUrl}
|
||||||
>
|
>
|
||||||
</video>
|
</video>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
projectionType: string | null | undefined;
|
projectionType: string | null | undefined;
|
||||||
checksum: string;
|
cacheKey: string | null;
|
||||||
loopVideo: boolean;
|
loopVideo: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onPreviousAsset?: () => void;
|
onPreviousAsset?: () => void;
|
||||||
@ -18,7 +18,7 @@
|
|||||||
let {
|
let {
|
||||||
assetId,
|
assetId,
|
||||||
projectionType,
|
projectionType,
|
||||||
checksum,
|
cacheKey,
|
||||||
loopVideo,
|
loopVideo,
|
||||||
onPreviousAsset,
|
onPreviousAsset,
|
||||||
onClose,
|
onClose,
|
||||||
@ -33,7 +33,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<VideoNativeViewer
|
<VideoNativeViewer
|
||||||
{loopVideo}
|
{loopVideo}
|
||||||
{checksum}
|
{cacheKey}
|
||||||
{assetId}
|
{assetId}
|
||||||
{onPreviousAsset}
|
{onPreviousAsset}
|
||||||
{onNextAsset}
|
{onNextAsset}
|
||||||
|
@ -327,7 +327,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<ImageThumbnail
|
<ImageThumbnail
|
||||||
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, checksum: asset.checksum })}
|
url={getAssetThumbnailUrl({ id: asset.id, size: AssetMediaSize.Thumbnail, cacheKey: asset.thumbhash })}
|
||||||
altText={$getAltText(asset)}
|
altText={$getAltText(asset)}
|
||||||
widthStyle="{width}px"
|
widthStyle="{width}px"
|
||||||
heightStyle="{height}px"
|
heightStyle="{height}px"
|
||||||
@ -339,7 +339,7 @@
|
|||||||
<div class="absolute top-0 h-full w-full">
|
<div class="absolute top-0 h-full w-full">
|
||||||
<VideoThumbnail
|
<VideoThumbnail
|
||||||
{assetStore}
|
{assetStore}
|
||||||
url={getAssetPlaybackUrl({ id: asset.id, checksum: asset.checksum })}
|
url={getAssetPlaybackUrl({ id: asset.id, cacheKey: asset.thumbhash })}
|
||||||
enablePlayback={mouseOver && $playVideoThumbnailOnHover}
|
enablePlayback={mouseOver && $playVideoThumbnailOnHover}
|
||||||
curve={selected}
|
curve={selected}
|
||||||
durationInSeconds={timeToSeconds(asset.duration)}
|
durationInSeconds={timeToSeconds(asset.duration)}
|
||||||
@ -352,7 +352,7 @@
|
|||||||
<div class="absolute top-0 h-full w-full">
|
<div class="absolute top-0 h-full w-full">
|
||||||
<VideoThumbnail
|
<VideoThumbnail
|
||||||
{assetStore}
|
{assetStore}
|
||||||
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, checksum: asset.checksum })}
|
url={getAssetPlaybackUrl({ id: asset.livePhotoVideoId, cacheKey: asset.thumbhash })}
|
||||||
pauseIcon={mdiMotionPauseOutline}
|
pauseIcon={mdiMotionPauseOutline}
|
||||||
playIcon={mdiMotionPlayOutline}
|
playIcon={mdiMotionPlayOutline}
|
||||||
showTime={false}
|
showTime={false}
|
||||||
|
@ -34,6 +34,8 @@
|
|||||||
{ key: ['i'], action: $t('show_or_hide_info') },
|
{ key: ['i'], action: $t('show_or_hide_info') },
|
||||||
{ key: ['s'], action: $t('stack_selected_photos') },
|
{ key: ['s'], action: $t('stack_selected_photos') },
|
||||||
{ key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') },
|
{ key: ['⇧', 'a'], action: $t('archive_or_unarchive_photo') },
|
||||||
|
{ key: ['r'], action: $t('rotate_right') },
|
||||||
|
{ key: ['⇧', 'r'], action: $t('rotate_left') },
|
||||||
{ key: ['⇧', 'd'], action: $t('download') },
|
{ key: ['⇧', 'd'], action: $t('download') },
|
||||||
{ key: ['Space'], action: $t('play_or_pause_video') },
|
{ key: ['Space'], action: $t('play_or_pause_video') },
|
||||||
{ key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') },
|
{ key: ['Del'], action: $t('trash_delete_asset'), info: $t('shift_to_permanent_delete') },
|
||||||
|
@ -10,6 +10,19 @@ export enum AssetAction {
|
|||||||
ADD_TO_ALBUM = 'add-to-album',
|
ADD_TO_ALBUM = 'add-to-album',
|
||||||
UNSTACK = 'unstack',
|
UNSTACK = 'unstack',
|
||||||
KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others',
|
KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others',
|
||||||
|
ROTATE = 'rotate',
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from the server because numeric enums lose their names
|
||||||
|
export enum ExifOrientation {
|
||||||
|
Horizontal = 1,
|
||||||
|
MirrorHorizontal = 2,
|
||||||
|
Rotate180 = 3,
|
||||||
|
MirrorVertical = 4,
|
||||||
|
MirrorHorizontalRotate270CW = 5,
|
||||||
|
Rotate90CW = 6,
|
||||||
|
MirrorHorizontalRotate90CW = 7,
|
||||||
|
Rotate270CW = 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AppRoute {
|
export enum AppRoute {
|
||||||
|
@ -180,28 +180,30 @@ const createUrl = (path: string, parameters?: Record<string, unknown>) => {
|
|||||||
return getBaseUrl() + url.pathname + url.search + url.hash;
|
return getBaseUrl() + url.pathname + url.search + url.hash;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetOriginalUrl = (options: string | { id: string; checksum?: string }) => {
|
type AssetUrlOptions = { id: string; cacheKey?: string | null };
|
||||||
|
|
||||||
|
export const getAssetOriginalUrl = (options: string | AssetUrlOptions) => {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
options = { id: options };
|
options = { id: options };
|
||||||
}
|
}
|
||||||
const { id, checksum } = options;
|
const { id, cacheKey } = options;
|
||||||
return createUrl(getAssetOriginalPath(id), { key: getKey(), c: checksum });
|
return createUrl(getAssetOriginalPath(id), { key: getKey(), c: cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetThumbnailUrl = (options: string | { id: string; size?: AssetMediaSize; checksum?: string }) => {
|
export const getAssetThumbnailUrl = (options: string | (AssetUrlOptions & { size?: AssetMediaSize })) => {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
options = { id: options };
|
options = { id: options };
|
||||||
}
|
}
|
||||||
const { id, size, checksum } = options;
|
const { id, size, cacheKey } = options;
|
||||||
return createUrl(getAssetThumbnailPath(id), { size, key: getKey(), c: checksum });
|
return createUrl(getAssetThumbnailPath(id), { size, key: getKey(), c: cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: string }) => {
|
export const getAssetPlaybackUrl = (options: string | AssetUrlOptions) => {
|
||||||
if (typeof options === 'string') {
|
if (typeof options === 'string') {
|
||||||
options = { id: options };
|
options = { id: options };
|
||||||
}
|
}
|
||||||
const { id, checksum } = options;
|
const { id, cacheKey } = options;
|
||||||
return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: checksum });
|
return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: cacheKey });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProfileImageUrl = (user: UserResponseDto) =>
|
export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user