Compare commits
14 commits
main
...
feat/rotat
Author | SHA1 | Date | |
---|---|---|---|
|
f9320069b7 | ||
|
9fdc982ca6 | ||
|
14120e0c65 | ||
|
618a6cd524 | ||
|
abaaa4def6 | ||
|
7818aeebf2 | ||
|
728233bf86 | ||
|
29aa3208c6 | ||
|
2d58d486e6 | ||
|
0309bba7dd | ||
|
089fa7a550 | ||
|
2e7eb9d800 | ||
|
2b2c7979a1 | ||
|
5fca8499ec |
21 changed files with 232 additions and 27 deletions
12
cli/src/api/open-api/api.ts
generated
12
cli/src/api/open-api/api.ts
generated
|
@ -483,6 +483,12 @@ export interface AssetBulkUpdateDto {
|
||||||
* @memberof AssetBulkUpdateDto
|
* @memberof AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
'longitude'?: number;
|
'longitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'orientation'?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -4191,6 +4197,12 @@ export interface UpdateAssetDto {
|
||||||
* @memberof UpdateAssetDto
|
* @memberof UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
'longitude'?: number;
|
'longitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'orientation'?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
1
mobile/openapi/doc/AssetBulkUpdateDto.md
generated
1
mobile/openapi/doc/AssetBulkUpdateDto.md
generated
|
@ -14,6 +14,7 @@ Name | Type | Description | Notes
|
||||||
**isFavorite** | **bool** | | [optional]
|
**isFavorite** | **bool** | | [optional]
|
||||||
**latitude** | **num** | | [optional]
|
**latitude** | **num** | | [optional]
|
||||||
**longitude** | **num** | | [optional]
|
**longitude** | **num** | | [optional]
|
||||||
|
**orientation** | **num** | | [optional]
|
||||||
**removeParent** | **bool** | | [optional]
|
**removeParent** | **bool** | | [optional]
|
||||||
**stackParentId** | **String** | | [optional]
|
**stackParentId** | **String** | | [optional]
|
||||||
|
|
||||||
|
|
1
mobile/openapi/doc/UpdateAssetDto.md
generated
1
mobile/openapi/doc/UpdateAssetDto.md
generated
|
@ -14,6 +14,7 @@ Name | Type | Description | Notes
|
||||||
**isFavorite** | **bool** | | [optional]
|
**isFavorite** | **bool** | | [optional]
|
||||||
**latitude** | **num** | | [optional]
|
**latitude** | **num** | | [optional]
|
||||||
**longitude** | **num** | | [optional]
|
**longitude** | **num** | | [optional]
|
||||||
|
**orientation** | **num** | | [optional]
|
||||||
|
|
||||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||||
|
|
||||||
|
|
21
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
21
mobile/openapi/lib/model/asset_bulk_update_dto.dart
generated
|
@ -19,6 +19,7 @@ class AssetBulkUpdateDto {
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.orientation,
|
||||||
this.removeParent,
|
this.removeParent,
|
||||||
this.stackParentId,
|
this.stackParentId,
|
||||||
});
|
});
|
||||||
|
@ -65,6 +66,14 @@ class AssetBulkUpdateDto {
|
||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? orientation;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Please note: This property should have been non-nullable! Since the specification file
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
/// does not include a default value (using the "default:" property), however, the generated
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
@ -89,6 +98,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.removeParent == removeParent &&
|
other.removeParent == removeParent &&
|
||||||
other.stackParentId == stackParentId;
|
other.stackParentId == stackParentId;
|
||||||
|
|
||||||
|
@ -101,11 +111,12 @@ 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) +
|
||||||
(removeParent == null ? 0 : removeParent!.hashCode) +
|
(removeParent == null ? 0 : removeParent!.hashCode) +
|
||||||
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
(stackParentId == null ? 0 : stackParentId!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, removeParent=$removeParent, stackParentId=$stackParentId]';
|
String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation, removeParent=$removeParent, stackParentId=$stackParentId]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -135,6 +146,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.removeParent != null) {
|
if (this.removeParent != null) {
|
||||||
json[r'removeParent'] = this.removeParent;
|
json[r'removeParent'] = this.removeParent;
|
||||||
} else {
|
} else {
|
||||||
|
@ -168,6 +184,9 @@ class AssetBulkUpdateDto {
|
||||||
longitude: json[r'longitude'] == null
|
longitude: json[r'longitude'] == null
|
||||||
? null
|
? null
|
||||||
: num.parse(json[r'longitude'].toString()),
|
: num.parse(json[r'longitude'].toString()),
|
||||||
|
orientation: json[r'orientation'] == null
|
||||||
|
? null
|
||||||
|
: num.parse(json[r'orientation'].toString()),
|
||||||
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
removeParent: mapValueOfType<bool>(json, r'removeParent'),
|
||||||
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
stackParentId: mapValueOfType<String>(json, r'stackParentId'),
|
||||||
);
|
);
|
||||||
|
|
25
mobile/openapi/lib/model/update_asset_dto.dart
generated
25
mobile/openapi/lib/model/update_asset_dto.dart
generated
|
@ -19,6 +19,7 @@ class UpdateAssetDto {
|
||||||
this.isFavorite,
|
this.isFavorite,
|
||||||
this.latitude,
|
this.latitude,
|
||||||
this.longitude,
|
this.longitude,
|
||||||
|
this.orientation,
|
||||||
});
|
});
|
||||||
|
|
||||||
///
|
///
|
||||||
|
@ -69,6 +70,14 @@ class UpdateAssetDto {
|
||||||
///
|
///
|
||||||
num? longitude;
|
num? longitude;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
num? orientation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
|
@ -76,7 +85,8 @@ class UpdateAssetDto {
|
||||||
other.isArchived == isArchived &&
|
other.isArchived == isArchived &&
|
||||||
other.isFavorite == isFavorite &&
|
other.isFavorite == isFavorite &&
|
||||||
other.latitude == latitude &&
|
other.latitude == latitude &&
|
||||||
other.longitude == longitude;
|
other.longitude == longitude &&
|
||||||
|
other.orientation == orientation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
|
@ -86,10 +96,11 @@ class UpdateAssetDto {
|
||||||
(isArchived == null ? 0 : isArchived!.hashCode) +
|
(isArchived == null ? 0 : isArchived!.hashCode) +
|
||||||
(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);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude]';
|
String toString() => 'UpdateAssetDto[dateTimeOriginal=$dateTimeOriginal, description=$description, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, orientation=$orientation]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
|
@ -123,6 +134,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;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -144,6 +160,9 @@ class UpdateAssetDto {
|
||||||
longitude: json[r'longitude'] == null
|
longitude: json[r'longitude'] == null
|
||||||
? null
|
? null
|
||||||
: num.parse(json[r'longitude'].toString()),
|
: num.parse(json[r'longitude'].toString()),
|
||||||
|
orientation: json[r'orientation'] == null
|
||||||
|
? null
|
||||||
|
: num.parse(json[r'orientation'].toString()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -46,6 +46,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// num orientation
|
||||||
|
test('to test the property `orientation`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
// bool removeParent
|
// bool removeParent
|
||||||
test('to test the property `removeParent`', () async {
|
test('to test the property `removeParent`', () async {
|
||||||
// TODO
|
// TODO
|
||||||
|
|
5
mobile/openapi/test/update_asset_dto_test.dart
generated
5
mobile/openapi/test/update_asset_dto_test.dart
generated
|
@ -46,6 +46,11 @@ void main() {
|
||||||
// TODO
|
// TODO
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// num orientation
|
||||||
|
test('to test the property `orientation`', () async {
|
||||||
|
// TODO
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6471,6 +6471,9 @@
|
||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
|
"orientation": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
"removeParent": {
|
"removeParent": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
@ -9369,6 +9372,9 @@
|
||||||
},
|
},
|
||||||
"longitude": {
|
"longitude": {
|
||||||
"type": "number"
|
"type": "number"
|
||||||
|
},
|
||||||
|
"orientation": {
|
||||||
|
"type": "number"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
|
|
@ -393,8 +393,8 @@ export class AssetService {
|
||||||
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id);
|
||||||
|
|
||||||
const { description, dateTimeOriginal, latitude, longitude, ...rest } = dto;
|
const { description, dateTimeOriginal, latitude, longitude, orientation, ...rest } = dto;
|
||||||
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude });
|
await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, orientation });
|
||||||
|
|
||||||
const asset = await this.assetRepository.save({ id, ...rest });
|
const asset = await this.assetRepository.save({ id, ...rest });
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids: [id] } });
|
||||||
|
@ -402,7 +402,7 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
async updateAll(authUser: AuthUserDto, dto: AssetBulkUpdateDto): Promise<void> {
|
||||||
const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto;
|
const { ids, removeParent, dateTimeOriginal, latitude, longitude, orientation, ...options } = dto;
|
||||||
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, ids);
|
||||||
|
|
||||||
if (removeParent) {
|
if (removeParent) {
|
||||||
|
@ -423,7 +423,7 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_ASSET, data: { ids } });
|
||||||
|
@ -591,8 +591,8 @@ export class AssetService {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateMetadata(dto: ISidecarWriteJob) {
|
private async updateMetadata(dto: ISidecarWriteJob) {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude } = dto;
|
const { id, description, dateTimeOriginal, latitude, longitude, orientation } = dto;
|
||||||
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude }, _.isUndefined);
|
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, 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 } });
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
IsNotEmpty,
|
IsNotEmpty,
|
||||||
IsPositive,
|
IsPositive,
|
||||||
IsString,
|
IsString,
|
||||||
|
Max,
|
||||||
Min,
|
Min,
|
||||||
ValidateIf,
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
@ -202,6 +203,13 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
|
||||||
@IsLongitude()
|
@IsLongitude()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(8)
|
||||||
|
@Type(() => Number)
|
||||||
|
orientation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateAssetDto {
|
export class UpdateAssetDto {
|
||||||
|
@ -230,6 +238,13 @@ export class UpdateAssetDto {
|
||||||
@IsLongitude()
|
@IsLongitude()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
@Max(8)
|
||||||
|
@Type(() => Number)
|
||||||
|
orientation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RandomAssetsDto {
|
export class RandomAssetsDto {
|
||||||
|
|
|
@ -39,4 +39,5 @@ export interface ISidecarWriteJob extends IEntityJob {
|
||||||
dateTimeOriginal?: string;
|
dateTimeOriginal?: string;
|
||||||
latitude?: number;
|
latitude?: number;
|
||||||
longitude?: number;
|
longitude?: number;
|
||||||
|
orientation?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -245,7 +245,7 @@ export class MetadataService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleSidecarWrite(job: ISidecarWriteJob) {
|
async handleSidecarWrite(job: ISidecarWriteJob) {
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude } = job;
|
const { id, description, dateTimeOriginal, latitude, longitude, orientation } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -258,6 +258,7 @@ export class MetadataService {
|
||||||
CreationDate: dateTimeOriginal,
|
CreationDate: dateTimeOriginal,
|
||||||
GPSLatitude: latitude,
|
GPSLatitude: latitude,
|
||||||
GPSLongitude: longitude,
|
GPSLongitude: longitude,
|
||||||
|
Orientation: orientation,
|
||||||
},
|
},
|
||||||
_.isUndefined,
|
_.isUndefined,
|
||||||
);
|
);
|
||||||
|
|
|
@ -27,6 +27,7 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration'> {
|
||||||
ImagePixelDepth?: string;
|
ImagePixelDepth?: string;
|
||||||
FocalLength?: number;
|
FocalLength?: number;
|
||||||
Duration?: number | ExifDuration;
|
Duration?: number | ExifDuration;
|
||||||
|
Orientation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetadataRepository {
|
export interface IMetadataRepository {
|
||||||
|
|
16
web/package-lock.json
generated
16
web/package-lock.json
generated
|
@ -10,7 +10,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
||||||
"@mdi/js": "^7.3.67",
|
"@mdi/js": "^7.3.67",
|
||||||
"@zoom-image/svelte": "^0.2.0",
|
"@zoom-image/svelte": "^0.2.2",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
|
@ -3935,9 +3935,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@zoom-image/core": {
|
"node_modules/@zoom-image/core": {
|
||||||
"version": "0.31.0",
|
"version": "0.31.1",
|
||||||
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.31.0.tgz",
|
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.31.1.tgz",
|
||||||
"integrity": "sha512-lvFVfIe/CSASXVq1E2vWnt/inXqrBMgjW96lW/l1JdM9EaCj5yis6YXPL5z+Rz2WHmMg5bb7Ps6w1Gzs/bC8LQ==",
|
"integrity": "sha512-VoAo4OkrD6sZIXNutnTzMeR0KZPkK+VOkyYfU9FTJsJxHHoHTCi9qixu8tzfrFkz3l0iQQBKkVVofu+ujM8sGw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@namnode/store": "^0.1.0"
|
"@namnode/store": "^0.1.0"
|
||||||
},
|
},
|
||||||
|
@ -3947,11 +3947,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@zoom-image/svelte": {
|
"node_modules/@zoom-image/svelte": {
|
||||||
"version": "0.2.1",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.2.tgz",
|
||||||
"integrity": "sha512-UGOFsXJN5Sk/uJxp7ZMajedXusmdmQ23nTNgphR4T9Q0Aef4qJJZI5dpGZtMCbGH2kdLbpIm30Sbht9kIe1L1Q==",
|
"integrity": "sha512-y8NGL3XAY4utyCtF4bk8Z8H/5JXBxRRYAvoR1o9fzHWNQ9dWVog3gniQ2C9WNPFQPet/4SxIi6G2RakJ6gu6Aw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zoom-image/core": "0.31.0"
|
"@zoom-image/core": "0.31.1"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
||||||
"@mdi/js": "^7.3.67",
|
"@mdi/js": "^7.3.67",
|
||||||
"@zoom-image/svelte": "^0.2.0",
|
"@zoom-image/svelte": "^0.2.2",
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"copy-image-clipboard": "^2.1.2",
|
"copy-image-clipboard": "^2.1.2",
|
||||||
|
|
12
web/src/api/open-api/api.ts
generated
12
web/src/api/open-api/api.ts
generated
|
@ -483,6 +483,12 @@ export interface AssetBulkUpdateDto {
|
||||||
* @memberof AssetBulkUpdateDto
|
* @memberof AssetBulkUpdateDto
|
||||||
*/
|
*/
|
||||||
'longitude'?: number;
|
'longitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof AssetBulkUpdateDto
|
||||||
|
*/
|
||||||
|
'orientation'?: number;
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
|
@ -4191,6 +4197,12 @@ export interface UpdateAssetDto {
|
||||||
* @memberof UpdateAssetDto
|
* @memberof UpdateAssetDto
|
||||||
*/
|
*/
|
||||||
'longitude'?: number;
|
'longitude'?: number;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {number}
|
||||||
|
* @memberof UpdateAssetDto
|
||||||
|
*/
|
||||||
|
'orientation'?: number;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
1
web/src/app.d.ts
vendored
1
web/src/app.d.ts
vendored
|
@ -25,5 +25,6 @@ declare namespace svelteHTML {
|
||||||
interface HTMLAttributes<T> {
|
interface HTMLAttributes<T> {
|
||||||
'on:copyImage'?: () => void;
|
'on:copyImage'?: () => void;
|
||||||
'on:zoomImage'?: () => void;
|
'on:zoomImage'?: () => void;
|
||||||
|
'on:rotateImage'?: () => void;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,7 +36,14 @@
|
||||||
|
|
||||||
$: isOwner = asset.ownerId === $page.data.user?.id;
|
$: isOwner = asset.ownerId === $page.data.user?.id;
|
||||||
|
|
||||||
type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob' | 'playSlideShow' | 'unstack';
|
type MenuItemEvent =
|
||||||
|
| 'addToAlbum'
|
||||||
|
| 'addToSharedAlbum'
|
||||||
|
| 'asProfileImage'
|
||||||
|
| 'runJob'
|
||||||
|
| 'playSlideShow'
|
||||||
|
| 'unstack'
|
||||||
|
| 'rotate';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
goBack: void;
|
goBack: void;
|
||||||
|
@ -53,6 +60,7 @@
|
||||||
runJob: AssetJobName;
|
runJob: AssetJobName;
|
||||||
playSlideShow: void;
|
playSlideShow: void;
|
||||||
unstack: void;
|
unstack: void;
|
||||||
|
rotate: void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
let contextMenuPosition = { x: 0, y: 0 };
|
let contextMenuPosition = { x: 0, y: 0 };
|
||||||
|
@ -175,7 +183,7 @@
|
||||||
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
text={asset.isArchived ? 'Unarchive' : 'Archive'}
|
||||||
/>
|
/>
|
||||||
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
|
<MenuOption on:click={() => onMenuClick('asProfileImage')} text="As profile picture" />
|
||||||
|
<MenuOption on:click={() => onMenuClick('rotate')} text="Rotate right" />
|
||||||
{#if hasStackChildren}
|
{#if hasStackChildren}
|
||||||
<MenuOption on:click={() => onMenuClick('unstack')} text="Un-Stack" />
|
<MenuOption on:click={() => onMenuClick('unstack')} text="Un-Stack" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -54,7 +54,6 @@
|
||||||
export let album: AlbumResponseDto | null = null;
|
export let album: AlbumResponseDto | null = null;
|
||||||
|
|
||||||
let reactions: ActivityResponseDto[] = [];
|
let reactions: ActivityResponseDto[] = [];
|
||||||
|
|
||||||
const { setAssetId } = assetViewingStore;
|
const { setAssetId } = assetViewingStore;
|
||||||
const {
|
const {
|
||||||
restartProgress: restartSlideshowProgress,
|
restartProgress: restartSlideshowProgress,
|
||||||
|
@ -255,6 +254,11 @@
|
||||||
isShowActivity = !isShowActivity;
|
isShowActivity = !isShowActivity;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRotate = () => {
|
||||||
|
const rotateImage = new CustomEvent('rotateImage');
|
||||||
|
window.dispatchEvent(rotateImage);
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyboardPress = (event: KeyboardEvent) => {
|
const handleKeyboardPress = (event: KeyboardEvent) => {
|
||||||
if (shouldIgnoreShortcut(event)) {
|
if (shouldIgnoreShortcut(event)) {
|
||||||
return;
|
return;
|
||||||
|
@ -299,6 +303,11 @@
|
||||||
isShowActivity = false;
|
isShowActivity = false;
|
||||||
$isShowDetail = !$isShowDetail;
|
$isShowDetail = !$isShowDetail;
|
||||||
return;
|
return;
|
||||||
|
case 'R':
|
||||||
|
case 'r':
|
||||||
|
if (shiftKey) {
|
||||||
|
handleRotate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -599,6 +608,7 @@
|
||||||
on:runJob={({ detail: job }) => handleRunJob(job)}
|
on:runJob={({ detail: job }) => handleRunJob(job)}
|
||||||
on:playSlideShow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
|
on:playSlideShow={() => ($slideshowState = SlideshowState.PlaySlideshow)}
|
||||||
on:unstack={handleUnstack}
|
on:unstack={handleUnstack}
|
||||||
|
on:rotate={handleRotate}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -8,11 +8,89 @@
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||||
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
import { shouldIgnoreShortcut } from '$lib/utils/shortcut';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { user } from '$lib/stores/user.store';
|
||||||
|
|
||||||
export let asset: AssetResponseDto;
|
export let asset: AssetResponseDto;
|
||||||
export let element: HTMLDivElement | undefined = undefined;
|
|
||||||
export let haveFadeTransition = true;
|
export let haveFadeTransition = true;
|
||||||
|
export let element: HTMLDivElement | undefined = undefined;
|
||||||
|
|
||||||
|
const orientationToRotation = (value: string): number => {
|
||||||
|
if (value === '1' || value === '6' || value === '8') {
|
||||||
|
if (imgWidth > imgHeight) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (value) {
|
||||||
|
case '1':
|
||||||
|
return 0;
|
||||||
|
case '2':
|
||||||
|
return 0;
|
||||||
|
case '3':
|
||||||
|
return 180;
|
||||||
|
case '4':
|
||||||
|
return 0;
|
||||||
|
case '5':
|
||||||
|
return 270;
|
||||||
|
case '6':
|
||||||
|
return 90;
|
||||||
|
case '7':
|
||||||
|
return 90;
|
||||||
|
case '8':
|
||||||
|
return 270;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRotationModulo = (rotation: number): number => {
|
||||||
|
return ((rotation % 360) + 360) % 360;
|
||||||
|
};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
getRotationModulo($zoomImageWheelState.currentRotation) === 0 ||
|
||||||
|
getRotationModulo($zoomImageWheelState.currentRotation) === 180
|
||||||
|
? ([imgHeight, imgWidth] = [clientHeight, clientWidth])
|
||||||
|
: ([imgWidth, imgHeight] = [clientHeight, clientWidth]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rotationToOrientation = (rotation: number): number => {
|
||||||
|
switch (getRotationModulo(rotation)) {
|
||||||
|
case 0:
|
||||||
|
return 1;
|
||||||
|
case 90:
|
||||||
|
return 8;
|
||||||
|
case 180:
|
||||||
|
return 3;
|
||||||
|
case 270:
|
||||||
|
return 6;
|
||||||
|
default:
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const doRotate = async () => {
|
||||||
|
setZoomImageWheelState({ currentRotation: $zoomImageWheelState.currentRotation + 90, currentZoom: 1 });
|
||||||
|
|
||||||
|
if (($user && $user.id !== asset.ownerId) || $user === null || asset.isReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await api.assetApi.updateAsset({
|
||||||
|
id: asset.id,
|
||||||
|
updateAssetDto: { orientation: rotationToOrientation($zoomImageWheelState.currentRotation) },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Unable to change orientation');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let clientWidth: number;
|
||||||
|
let clientHeight: number;
|
||||||
|
let imgWidth: number;
|
||||||
|
let imgHeight: number;
|
||||||
let imgElement: HTMLDivElement;
|
let imgElement: HTMLDivElement;
|
||||||
let assetData: string;
|
let assetData: string;
|
||||||
let abortController: AbortController;
|
let abortController: AbortController;
|
||||||
|
@ -117,23 +195,32 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeypress} on:copyImage={doCopy} on:zoomImage={doZoomImage} />
|
<svelte:window on:keydown={handleKeypress} on:rotateImage={doRotate} on:copyImage={doCopy} on:zoomImage={doZoomImage} />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
|
bind:clientHeight
|
||||||
|
bind:clientWidth
|
||||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||||
class="flex h-full select-none place-content-center place-items-center"
|
class="flex h-full w-full select-none place-content-center place-items-center"
|
||||||
>
|
>
|
||||||
{#await loadAssetData({ loadOriginal: false })}
|
{#await loadAssetData({ loadOriginal: false })}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then}
|
{:then}
|
||||||
<div bind:this={imgElement} class="h-full w-full">
|
<div
|
||||||
|
bind:this={imgElement}
|
||||||
|
class="duration-500"
|
||||||
|
style={asset.exifInfo?.orientation
|
||||||
|
? `transform: rotate(${orientationToRotation(asset.exifInfo?.orientation)}deg);`
|
||||||
|
: ''}
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
transition:fade={{ duration: haveFadeTransition ? 150 : 0 }}
|
||||||
src={assetData}
|
src={assetData}
|
||||||
alt={asset.id}
|
alt={asset.id}
|
||||||
class="h-full w-full object-contain"
|
class="h-full w-full object-contain"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
|
style={`width:${imgWidth}px;height:${imgHeight}px;transform-origin: 0px 0px 0px;`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
{ key: ['i'], action: 'Show or hide info' },
|
{ key: ['i'], action: 'Show or hide info' },
|
||||||
{ key: ['⇧', 'a'], action: 'Archive or unarchive photo' },
|
{ key: ['⇧', 'a'], action: 'Archive or unarchive photo' },
|
||||||
{ key: ['⇧', 'd'], action: 'Download' },
|
{ key: ['⇧', 'd'], action: 'Download' },
|
||||||
|
{ key: ['⇧', 'r'], action: 'Rotate' },
|
||||||
{ key: ['Space'], action: 'Play or pause video' },
|
{ key: ['Space'], action: 'Play or pause video' },
|
||||||
{ key: ['Del'], action: 'Delete Asset' },
|
{ key: ['Del'], action: 'Delete Asset' },
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in a new issue