Compare commits

...

14 commits

Author SHA1 Message Date
martabal
f9320069b7
try to fix orientation on load 2023-12-04 01:09:27 +01:00
martabal
9fdc982ca6
fix: rotation 2023-12-02 22:02:45 +01:00
martabal
14120e0c65
fix: transition animation 2023-12-02 16:51:06 +01:00
martabal
618a6cd524
Merge branch 'main' into feat/rotate-photo 2023-12-02 16:20:01 +01:00
martabal
abaaa4def6
pr feedback 2023-12-02 14:36:15 +01:00
martabal
7818aeebf2
simplify 2023-12-02 12:31:40 +01:00
martabal
728233bf86
fix: zoom & rotation 2023-12-02 02:25:10 +01:00
martabal
29aa3208c6
fix: full width & height 2023-12-02 00:55:32 +01:00
martabal
2d58d486e6
fix: orientation 2023-12-01 20:07:17 +01:00
martabal
0309bba7dd
save value 2023-12-01 18:34:24 +01:00
martabal
089fa7a550
merge main 2023-11-30 22:42:16 +01:00
martabal
2e7eb9d800
chore: merge main 2023-11-23 16:44:05 +01:00
martabal
2b2c7979a1
fix: direction of rotation 2023-11-23 16:43:02 +01:00
martabal
5fca8499ec
feat(web): rotate photo 2023-11-19 16:54:32 +01:00
21 changed files with 232 additions and 27 deletions

View file

@ -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;
} }
/** /**
* *

View file

@ -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]

View file

@ -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)

View file

@ -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'),
); );

View file

@ -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;

View file

@ -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

View file

@ -46,6 +46,11 @@ void main() {
// TODO // TODO
}); });
// num orientation
test('to test the property `orientation`', () async {
// TODO
});
}); });

View file

@ -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"

View file

@ -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 } });

View file

@ -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 {

View file

@ -39,4 +39,5 @@ export interface ISidecarWriteJob extends IEntityJob {
dateTimeOriginal?: string; dateTimeOriginal?: string;
latitude?: number; latitude?: number;
longitude?: number; longitude?: number;
orientation?: number;
} }

View file

@ -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,
); );

View file

@ -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
View file

@ -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",

View file

@ -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",

View file

@ -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
View file

@ -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;
} }
} }

View file

@ -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}

View file

@ -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}

View file

@ -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}

View file

@ -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' },
], ],