feat (web/server) 360 degrees Web panoramas [attempt 2] (#3412)
* commit 1 (isPanorama: boolean) * working solution for projectiontypeenum * fix * format fix * fix * fix * fix * fix * enum projectiontype * working solution with exif * fix * reverted > * fix format * reverted auto-magic api.ts prettification * fix * reverted api.ts autogenerated * api ts regenerated * Update web/src/lib/components/assets/thumbnail/thumbnail.svelte Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com> * Update web/src/lib/components/asset-viewer/asset-viewer.svelte Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com> * exifProjectionType * Update server/src/microservices/processors/metadata-extraction.processor.ts Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com> * projectionType?: string = ProjectionType.NONE; * not null * projectionType!: ProjectionType; * opeapi generator fix * fixes * fix * fix * generate api * asset.exifInifo?.projectionType * Update server/src/domain/asset/response-dto/exif-response.dto.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * Update server/src/microservices/processors/metadata-extraction.processor.ts Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> * enum -> varchar;projectiontypeenum->projectiontype * asset-viewer fixed prettiffier * @Column({}) single line * enum | string * make api * enum | string * enum | str fix * fix * chore: use string instead of enum * chore: open api * fix: checks --------- Co-authored-by: Sergey Kondrikov <sergey.kondrikov@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Generated
+103
@@ -8,6 +8,7 @@
|
||||
"name": "immich-web",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
||||
"@zoom-image/svelte": "^0.1.8",
|
||||
"axios": "^0.27.2",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -1840,6 +1841,47 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@cfcs/core": {
|
||||
"version": "0.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@cfcs/core/-/core-0.0.24.tgz",
|
||||
"integrity": "sha512-feB38qu+eDk0Pggh/yR7gjaNmvUYA2uCxHP3Pz2MLE4LZ/9jPdtu8bzCSI47yTEhWyZCF5Pk698hdz8IN2mTjA==",
|
||||
"dependencies": {
|
||||
"@egjs/component": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/component": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/component/-/component-3.0.4.tgz",
|
||||
"integrity": "sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g=="
|
||||
},
|
||||
"node_modules/@egjs/imready": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/imready/-/imready-1.4.1.tgz",
|
||||
"integrity": "sha512-JIOBs4lB7FYdsKi5uvz2j3SObX8eShtZjtqlOH41tm185aJOQZwiKBK8+V4MxzG4X6DqVhpdN8UcuVwBbElfsg==",
|
||||
"dependencies": {
|
||||
"@cfcs/core": "^0.0.24",
|
||||
"@egjs/component": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/svelte-view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/svelte-view360/-/svelte-view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-qFNbLNME8H7QU2lg8SCKUTPoBXVdBcM5m8zmlDRE72esCTguDzUq2szXD7L1JWcb2lYPTFl3HVp/sZlcQ/1HpQ==",
|
||||
"dependencies": {
|
||||
"@egjs/view360": "4.0.0-beta.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/view360/-/view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-prVTTxuQ1/k59NM7G0tm58k2vPHGoaExoFr5E7MoJaSGF56Otj4okQHAxxosXH87aQLN0feZMtBlsKz0b/7zEw==",
|
||||
"dependencies": {
|
||||
"@egjs/component": "^3.0.2",
|
||||
"@egjs/imready": "^1.3.0",
|
||||
"@types/webxr": "^0.5.1",
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||
@@ -3821,6 +3863,11 @@
|
||||
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
|
||||
"integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw=="
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz",
|
||||
@@ -6290,6 +6337,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
@@ -13334,6 +13386,47 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@cfcs/core": {
|
||||
"version": "0.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@cfcs/core/-/core-0.0.24.tgz",
|
||||
"integrity": "sha512-feB38qu+eDk0Pggh/yR7gjaNmvUYA2uCxHP3Pz2MLE4LZ/9jPdtu8bzCSI47yTEhWyZCF5Pk698hdz8IN2mTjA==",
|
||||
"requires": {
|
||||
"@egjs/component": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"@egjs/component": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/component/-/component-3.0.4.tgz",
|
||||
"integrity": "sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g=="
|
||||
},
|
||||
"@egjs/imready": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/imready/-/imready-1.4.1.tgz",
|
||||
"integrity": "sha512-JIOBs4lB7FYdsKi5uvz2j3SObX8eShtZjtqlOH41tm185aJOQZwiKBK8+V4MxzG4X6DqVhpdN8UcuVwBbElfsg==",
|
||||
"requires": {
|
||||
"@cfcs/core": "^0.0.24",
|
||||
"@egjs/component": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"@egjs/svelte-view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/svelte-view360/-/svelte-view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-qFNbLNME8H7QU2lg8SCKUTPoBXVdBcM5m8zmlDRE72esCTguDzUq2szXD7L1JWcb2lYPTFl3HVp/sZlcQ/1HpQ==",
|
||||
"requires": {
|
||||
"@egjs/view360": "4.0.0-beta.7"
|
||||
}
|
||||
},
|
||||
"@egjs/view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/view360/-/view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-prVTTxuQ1/k59NM7G0tm58k2vPHGoaExoFr5E7MoJaSGF56Otj4okQHAxxosXH87aQLN0feZMtBlsKz0b/7zEw==",
|
||||
"requires": {
|
||||
"@egjs/component": "^3.0.2",
|
||||
"@egjs/imready": "^1.3.0",
|
||||
"@types/webxr": "^0.5.1",
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||
@@ -14748,6 +14841,11 @@
|
||||
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/webxr": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
|
||||
"integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw=="
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "17.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz",
|
||||
@@ -16510,6 +16608,11 @@
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"dev": true
|
||||
},
|
||||
"gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||
},
|
||||
"glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
||||
"@zoom-image/svelte": "^0.1.8",
|
||||
"axios": "^0.27.2",
|
||||
"buffer": "^6.0.3",
|
||||
|
||||
Generated
+6
@@ -1304,6 +1304,12 @@ export interface ExifResponseDto {
|
||||
* @memberof ExifResponseDto
|
||||
*/
|
||||
'description'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ExifResponseDto
|
||||
*/
|
||||
'projectionType'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import DetailPanel from './detail-panel.svelte';
|
||||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import VideoViewer from './video-viewer.svelte';
|
||||
import PanoramaViewer from './panorama-viewer.svelte';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
|
||||
|
||||
@@ -293,6 +295,8 @@
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||
/>
|
||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<PanoramaViewer {publicSharedKey} {asset} />
|
||||
{:else}
|
||||
<PhotoViewer {publicSharedKey} {asset} on:close={closeViewer} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
.view360-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: pan-y;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view360-canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import View360, { EquirectProjection } from '@egjs/svelte-view360';
|
||||
import './panorama-viewer.css';
|
||||
export let asset: AssetResponseDto;
|
||||
export let publicSharedKey = '';
|
||||
let dataUrl = '';
|
||||
let errorMessage = '';
|
||||
const loadAssetData = async () => {
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(
|
||||
{ id: asset.id, isThumb: false, isWeb: false, key: publicSharedKey },
|
||||
{ responseType: 'blob' },
|
||||
);
|
||||
if (data instanceof Blob) {
|
||||
dataUrl = URL.createObjectURL(data);
|
||||
return dataUrl;
|
||||
} else {
|
||||
throw new Error('Invalid data format');
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = 'Failed to load asset';
|
||||
return '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
{#await loadAssetData()}
|
||||
<LoadingSpinner />
|
||||
{:then assetData}
|
||||
{#if assetData}
|
||||
<View360 autoResize={true} initialZoom={0.5} projection={new EquirectProjection({ src: assetData })} />
|
||||
{:else}
|
||||
<p>{errorMessage}</p>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
|
||||
import { timeToSeconds } from '$lib/utils/time-to-seconds';
|
||||
import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
|
||||
@@ -12,6 +13,7 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import ImageThumbnail from './image-thumbnail.svelte';
|
||||
import VideoThumbnail from './video-thumbnail.svelte';
|
||||
import Rotate360Icon from 'svelte-material-icons/Rotate360.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -124,6 +126,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<div class="absolute right-0 top-0 z-20 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
<span class="pr-2 pt-2">
|
||||
<Rotate360Icon size="24" />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.resized}
|
||||
<ImageThumbnail
|
||||
url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
|
||||
|
||||
@@ -25,3 +25,14 @@ export enum AppRoute {
|
||||
AUTH_REGISTER = '/auth/register',
|
||||
AUTH_CHANGE_PASSWORD = '/auth/change-password',
|
||||
}
|
||||
|
||||
export enum ProjectionType {
|
||||
EQUIRECTANGULAR = 'EQUIRECTANGULAR',
|
||||
CUBEMAP = 'CUBEMAP',
|
||||
CUBESTRIP = 'CUBESTRIP',
|
||||
EQUIRECTANGULAR_STEREO = 'EQUIRECTANGULAR_STEREO',
|
||||
CUBEMAP_STEREO = 'CUBEMAP_STEREO',
|
||||
CUBESTRIP_STEREO = 'CUBESTRIP_STEREO',
|
||||
CYLINDER = 'CYLINDER',
|
||||
NONE = 'NONE',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user