mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05: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