mirror of
https://github.com/immich-app/immich.git
synced 2026-05-22 23:52:32 -04:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 471e2ffee4 | |||
| 700707e399 | |||
| 0cc420007f | |||
| 4348d10ea2 |
Generated
+1
@@ -121,6 +121,7 @@ Class | Method | HTTP request | Description
|
|||||||
*AssetsApi* | [**updateBulkAssetMetadata**](doc//AssetsApi.md#updatebulkassetmetadata) | **PUT** /assets/metadata | Upsert asset metadata
|
*AssetsApi* | [**updateBulkAssetMetadata**](doc//AssetsApi.md#updatebulkassetmetadata) | **PUT** /assets/metadata | Upsert asset metadata
|
||||||
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | Upload asset
|
*AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | Upload asset
|
||||||
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | View asset thumbnail
|
*AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | View asset thumbnail
|
||||||
|
*AssetsApi* | [**viewAssetTile**](doc//AssetsApi.md#viewassettile) | **GET** /assets/{id}/tiles/{level}/{col}/{row} | View an asset tile
|
||||||
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | Change password
|
*AuthenticationApi* | [**changePassword**](doc//AuthenticationApi.md#changepassword) | **POST** /auth/change-password | Change password
|
||||||
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | Change pin code
|
*AuthenticationApi* | [**changePinCode**](doc//AuthenticationApi.md#changepincode) | **PUT** /auth/pin-code | Change pin code
|
||||||
*AuthenticationApi* | [**finishOAuth**](doc//AuthenticationApi.md#finishoauth) | **POST** /oauth/callback | Finish OAuth
|
*AuthenticationApi* | [**finishOAuth**](doc//AuthenticationApi.md#finishoauth) | **POST** /oauth/callback | Finish OAuth
|
||||||
|
|||||||
Generated
+87
@@ -1836,4 +1836,91 @@ class AssetsApi {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// View an asset tile
|
||||||
|
///
|
||||||
|
/// Download a specific tile from an image at the specified level - must currently be 0 - and position
|
||||||
|
///
|
||||||
|
/// Note: This method returns the HTTP [Response].
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [num] col (required):
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [num] level (required):
|
||||||
|
///
|
||||||
|
/// * [num] row (required):
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<Response> viewAssetTileWithHttpInfo(num col, String id, num level, num row, { String? key, String? slug, }) async {
|
||||||
|
// ignore: prefer_const_declarations
|
||||||
|
final apiPath = r'/assets/{id}/tiles/{level}/{col}/{row}'
|
||||||
|
.replaceAll('{col}', col.toString())
|
||||||
|
.replaceAll('{id}', id)
|
||||||
|
.replaceAll('{level}', level.toString())
|
||||||
|
.replaceAll('{row}', row.toString());
|
||||||
|
|
||||||
|
// ignore: prefer_final_locals
|
||||||
|
Object? postBody;
|
||||||
|
|
||||||
|
final queryParams = <QueryParam>[];
|
||||||
|
final headerParams = <String, String>{};
|
||||||
|
final formParams = <String, String>{};
|
||||||
|
|
||||||
|
if (key != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'key', key));
|
||||||
|
}
|
||||||
|
if (slug != null) {
|
||||||
|
queryParams.addAll(_queryParams('', 'slug', slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentTypes = <String>[];
|
||||||
|
|
||||||
|
|
||||||
|
return apiClient.invokeAPI(
|
||||||
|
apiPath,
|
||||||
|
'GET',
|
||||||
|
queryParams,
|
||||||
|
postBody,
|
||||||
|
headerParams,
|
||||||
|
formParams,
|
||||||
|
contentTypes.isEmpty ? null : contentTypes.first,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// View an asset tile
|
||||||
|
///
|
||||||
|
/// Download a specific tile from an image at the specified level - must currently be 0 - and position
|
||||||
|
///
|
||||||
|
/// Parameters:
|
||||||
|
///
|
||||||
|
/// * [num] col (required):
|
||||||
|
///
|
||||||
|
/// * [String] id (required):
|
||||||
|
///
|
||||||
|
/// * [num] level (required):
|
||||||
|
///
|
||||||
|
/// * [num] row (required):
|
||||||
|
///
|
||||||
|
/// * [String] key:
|
||||||
|
///
|
||||||
|
/// * [String] slug:
|
||||||
|
Future<MultipartFile?> viewAssetTile(num col, String id, num level, num row, { String? key, String? slug, }) async {
|
||||||
|
final response = await viewAssetTileWithHttpInfo(col, id, level, row, key: key, slug: slug, );
|
||||||
|
if (response.statusCode >= HttpStatus.badRequest) {
|
||||||
|
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||||
|
}
|
||||||
|
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||||
|
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||||
|
// FormatException when trying to decode an empty string.
|
||||||
|
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||||
|
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'MultipartFile',) as MultipartFile;
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4397,6 +4397,103 @@
|
|||||||
"x-immich-state": "Stable"
|
"x-immich-state": "Stable"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/assets/{id}/tiles/{level}/{col}/{row}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Download a specific tile from an image at the specified level - must currently be 0 - and position",
|
||||||
|
"operationId": "viewAssetTile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "col",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"format": "uuid",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "key",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "level",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "row",
|
||||||
|
"required": true,
|
||||||
|
"in": "path",
|
||||||
|
"schema": {
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slug",
|
||||||
|
"required": false,
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/octet-stream": {
|
||||||
|
"schema": {
|
||||||
|
"format": "binary",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearer": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cookie": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"api_key": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"summary": "View an asset tile",
|
||||||
|
"tags": [
|
||||||
|
"Assets"
|
||||||
|
],
|
||||||
|
"x-immich-history": [
|
||||||
|
{
|
||||||
|
"version": "v2.7.0",
|
||||||
|
"state": "Added"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"version": "v2.7.0",
|
||||||
|
"state": "Stable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"x-immich-permission": "asset.view",
|
||||||
|
"x-immich-state": "Stable"
|
||||||
|
}
|
||||||
|
},
|
||||||
"/assets/{id}/video/playback": {
|
"/assets/{id}/video/playback": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "Streams the video file for the specified asset. This endpoint also supports byte range requests.",
|
"description": "Streams the video file for the specified asset. This endpoint also supports byte range requests.",
|
||||||
|
|||||||
@@ -4313,6 +4313,27 @@ export function viewAsset({ edited, id, key, size, slug }: {
|
|||||||
...opts
|
...opts
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* View an asset tile
|
||||||
|
*/
|
||||||
|
export function viewAssetTile({ col, id, key, level, row, slug }: {
|
||||||
|
col: number;
|
||||||
|
id: string;
|
||||||
|
key?: string;
|
||||||
|
level: number;
|
||||||
|
row: number;
|
||||||
|
slug?: string;
|
||||||
|
}, opts?: Oazapfts.RequestOpts) {
|
||||||
|
return oazapfts.ok(oazapfts.fetchBlob<{
|
||||||
|
status: 200;
|
||||||
|
data: Blob;
|
||||||
|
}>(`/assets/${encodeURIComponent(id)}/tiles/${encodeURIComponent(level)}/${encodeURIComponent(col)}/${encodeURIComponent(row)}${QS.query(QS.explode({
|
||||||
|
key,
|
||||||
|
slug
|
||||||
|
}))}`, {
|
||||||
|
...opts
|
||||||
|
}));
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Play asset video
|
* Play asset video
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ export const getAssetThumbnailPath = (id: string) => `/assets/${id}/thumbnail`;
|
|||||||
export const getAssetPlaybackPath = (id: string) =>
|
export const getAssetPlaybackPath = (id: string) =>
|
||||||
`/assets/${id}/video/playback`;
|
`/assets/${id}/video/playback`;
|
||||||
|
|
||||||
|
export const getAssetTilePath = (
|
||||||
|
id: string,
|
||||||
|
level: number,
|
||||||
|
col: number,
|
||||||
|
row: number
|
||||||
|
) => `/assets/${id}/tiles/${level}/${col}/${row}`;
|
||||||
|
|
||||||
export const getUserProfileImagePath = (userId: string) =>
|
export const getUserProfileImagePath = (userId: string) =>
|
||||||
`/users/${userId}/profile-image`;
|
`/users/${userId}/profile-image`;
|
||||||
|
|
||||||
|
|||||||
Generated
+34
-127
@@ -67,7 +67,7 @@ importers:
|
|||||||
version: 24.11.0
|
version: 24.11.0
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
byte-size:
|
byte-size:
|
||||||
specifier: ^9.0.0
|
specifier: ^9.0.0
|
||||||
version: 9.0.1
|
version: 9.0.1
|
||||||
@@ -115,10 +115,10 @@ importers:
|
|||||||
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
vitest-fetch-mock:
|
vitest-fetch-mock:
|
||||||
specifier: ^0.4.0
|
specifier: ^0.4.0
|
||||||
version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.3.1
|
specifier: ^2.3.1
|
||||||
version: 2.8.2
|
version: 2.8.2
|
||||||
@@ -673,7 +673,7 @@ importers:
|
|||||||
version: 13.15.10
|
version: 13.15.10
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
eslint:
|
eslint:
|
||||||
specifier: ^10.0.0
|
specifier: ^10.0.0
|
||||||
version: 10.0.2(jiti@2.6.1)
|
version: 10.0.2(jiti@2.6.1)
|
||||||
@@ -730,7 +730,7 @@ importers:
|
|||||||
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 6.1.1(typescript@5.9.3)(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
version: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
web:
|
web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -755,6 +755,9 @@ importers:
|
|||||||
'@photo-sphere-viewer/core':
|
'@photo-sphere-viewer/core':
|
||||||
specifier: ^5.14.0
|
specifier: ^5.14.0
|
||||||
version: 5.14.1
|
version: 5.14.1
|
||||||
|
'@photo-sphere-viewer/equirectangular-tiles-adapter':
|
||||||
|
specifier: ^5.14.1
|
||||||
|
version: 5.14.1(@photo-sphere-viewer/core@5.14.1)
|
||||||
'@photo-sphere-viewer/equirectangular-video-adapter':
|
'@photo-sphere-viewer/equirectangular-video-adapter':
|
||||||
specifier: ^5.14.0
|
specifier: ^5.14.0
|
||||||
version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))
|
version: 5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))
|
||||||
@@ -784,7 +787,7 @@ importers:
|
|||||||
version: 2.6.0
|
version: 2.6.0
|
||||||
fabric:
|
fabric:
|
||||||
specifier: ^7.0.0
|
specifier: ^7.0.0
|
||||||
version: 7.2.0
|
version: 7.2.0(encoding@0.1.13)
|
||||||
geo-coordinates-parser:
|
geo-coordinates-parser:
|
||||||
specifier: ^1.7.4
|
specifier: ^1.7.4
|
||||||
version: 1.7.4
|
version: 1.7.4
|
||||||
@@ -884,7 +887,7 @@ importers:
|
|||||||
version: 6.9.1
|
version: 6.9.1
|
||||||
'@testing-library/svelte':
|
'@testing-library/svelte':
|
||||||
specifier: ^5.2.8
|
specifier: ^5.2.8
|
||||||
version: 5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
'@testing-library/user-event':
|
'@testing-library/user-event':
|
||||||
specifier: ^14.5.2
|
specifier: ^14.5.2
|
||||||
version: 14.6.1(@testing-library/dom@10.4.1)
|
version: 14.6.1(@testing-library/dom@10.4.1)
|
||||||
@@ -908,7 +911,7 @@ importers:
|
|||||||
version: 1.5.6
|
version: 1.5.6
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^17.0.0
|
specifier: ^17.0.0
|
||||||
version: 17.3.1
|
version: 17.3.1
|
||||||
@@ -971,7 +974,7 @@ importers:
|
|||||||
version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
version: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -3886,6 +3889,11 @@ packages:
|
|||||||
'@photo-sphere-viewer/core@5.14.1':
|
'@photo-sphere-viewer/core@5.14.1':
|
||||||
resolution: {integrity: sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==}
|
resolution: {integrity: sha512-qrwUudrX9YZms4c2shlY/H3jUP0oh9FyGEqIDr/95ulNZgKbhQ6C/i8zDQ4j8ooFR4+z5FDORQtGvLgPyX8VCA==}
|
||||||
|
|
||||||
|
'@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.1':
|
||||||
|
resolution: {integrity: sha512-QHd9y5cIFXAAZInbKbh+nUz5uzSRTiR8HYApm+ONlDu8JHAp410xNhB1vNm2Q1mVgg8IigQpD9Za5mPq10fESA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@photo-sphere-viewer/core': 5.14.1
|
||||||
|
|
||||||
'@photo-sphere-viewer/equirectangular-video-adapter@5.14.1':
|
'@photo-sphere-viewer/equirectangular-video-adapter@5.14.1':
|
||||||
resolution: {integrity: sha512-rZ6igEy1TEfgHB8Ak/8N0rZNYQLbNEGLVmhwNxDMWESCJ9nrNx3tJHFn7k6eZYjj9zJA73xF5YdY6XWUCpZDzg==}
|
resolution: {integrity: sha512-rZ6igEy1TEfgHB8Ak/8N0rZNYQLbNEGLVmhwNxDMWESCJ9nrNx3tJHFn7k6eZYjj9zJA73xF5YdY6XWUCpZDzg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -15190,22 +15198,6 @@ snapshots:
|
|||||||
|
|
||||||
'@mapbox/mapbox-gl-rtl-text@0.3.0': {}
|
'@mapbox/mapbox-gl-rtl-text@0.3.0': {}
|
||||||
|
|
||||||
'@mapbox/node-pre-gyp@1.0.11':
|
|
||||||
dependencies:
|
|
||||||
detect-libc: 2.1.2
|
|
||||||
https-proxy-agent: 5.0.1
|
|
||||||
make-dir: 3.1.0
|
|
||||||
node-fetch: 2.7.0
|
|
||||||
nopt: 5.0.0
|
|
||||||
npmlog: 5.0.1
|
|
||||||
rimraf: 3.0.2
|
|
||||||
semver: 7.7.4
|
|
||||||
tar: 6.2.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
|
'@mapbox/node-pre-gyp@1.0.11(encoding@0.1.13)':
|
||||||
dependencies:
|
dependencies:
|
||||||
detect-libc: 2.1.2
|
detect-libc: 2.1.2
|
||||||
@@ -15881,6 +15873,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
three: 0.179.1
|
three: 0.179.1
|
||||||
|
|
||||||
|
'@photo-sphere-viewer/equirectangular-tiles-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)':
|
||||||
|
dependencies:
|
||||||
|
'@photo-sphere-viewer/core': 5.14.1
|
||||||
|
|
||||||
'@photo-sphere-viewer/equirectangular-video-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))':
|
'@photo-sphere-viewer/equirectangular-video-adapter@5.14.1(@photo-sphere-viewer/core@5.14.1)(@photo-sphere-viewer/video-plugin@5.14.1(@photo-sphere-viewer/core@5.14.1))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@photo-sphere-viewer/core': 5.14.1
|
'@photo-sphere-viewer/core': 5.14.1
|
||||||
@@ -16516,14 +16512,14 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
svelte: 5.53.5
|
svelte: 5.53.5
|
||||||
|
|
||||||
'@testing-library/svelte@5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
'@testing-library/svelte@5.3.1(svelte@5.53.5)(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@testing-library/dom': 10.4.1
|
'@testing-library/dom': 10.4.1
|
||||||
'@testing-library/svelte-core': 1.0.0(svelte@5.53.5)
|
'@testing-library/svelte-core': 1.0.0(svelte@5.53.5)
|
||||||
svelte: 5.53.5
|
svelte: 5.53.5
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
'@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -17217,7 +17213,7 @@ snapshots:
|
|||||||
|
|
||||||
'@vercel/oidc@3.0.5': {}
|
'@vercel/oidc@3.0.5': {}
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
@@ -17232,11 +17228,11 @@ snapshots:
|
|||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
test-exclude: 7.0.1
|
test-exclude: 7.0.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@ampproject/remapping': 2.3.0
|
'@ampproject/remapping': 2.3.0
|
||||||
'@bcoe/v8-coverage': 1.0.2
|
'@bcoe/v8-coverage': 1.0.2
|
||||||
@@ -17251,7 +17247,7 @@ snapshots:
|
|||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
test-exclude: 7.0.1
|
test-exclude: 7.0.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -17978,16 +17974,6 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001774: {}
|
caniuse-lite@1.0.30001774: {}
|
||||||
|
|
||||||
canvas@2.11.2:
|
|
||||||
dependencies:
|
|
||||||
'@mapbox/node-pre-gyp': 1.0.11
|
|
||||||
nan: 2.25.0
|
|
||||||
simple-get: 3.1.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
canvas@2.11.2(encoding@0.1.13):
|
canvas@2.11.2(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
|
'@mapbox/node-pre-gyp': 1.0.11(encoding@0.1.13)
|
||||||
@@ -19585,10 +19571,10 @@ snapshots:
|
|||||||
|
|
||||||
extend@3.0.2: {}
|
extend@3.0.2: {}
|
||||||
|
|
||||||
fabric@7.2.0:
|
fabric@7.2.0(encoding@0.1.13):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
canvas: 2.11.2
|
canvas: 2.11.2(encoding@0.1.13)
|
||||||
jsdom: 26.1.0(canvas@2.11.2)
|
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- encoding
|
- encoding
|
||||||
@@ -20767,36 +20753,6 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
jsdom@26.1.0(canvas@2.11.2):
|
|
||||||
dependencies:
|
|
||||||
cssstyle: 4.6.0
|
|
||||||
data-urls: 5.0.0
|
|
||||||
decimal.js: 10.6.0
|
|
||||||
html-encoding-sniffer: 4.0.0
|
|
||||||
http-proxy-agent: 7.0.2
|
|
||||||
https-proxy-agent: 7.0.6
|
|
||||||
is-potential-custom-element-name: 1.0.1
|
|
||||||
nwsapi: 2.2.23
|
|
||||||
parse5: 7.3.0
|
|
||||||
rrweb-cssom: 0.8.0
|
|
||||||
saxes: 6.0.0
|
|
||||||
symbol-tree: 3.2.4
|
|
||||||
tough-cookie: 5.1.2
|
|
||||||
w3c-xmlserializer: 5.0.0
|
|
||||||
webidl-conversions: 7.0.0
|
|
||||||
whatwg-encoding: 3.1.1
|
|
||||||
whatwg-mimetype: 4.0.0
|
|
||||||
whatwg-url: 14.2.0
|
|
||||||
ws: 8.19.0
|
|
||||||
xml-name-validator: 5.0.0
|
|
||||||
optionalDependencies:
|
|
||||||
canvas: 2.11.2
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- bufferutil
|
|
||||||
- supports-color
|
|
||||||
- utf-8-validate
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
jsep@1.4.0: {}
|
jsep@1.4.0: {}
|
||||||
|
|
||||||
jsesc@3.1.0: {}
|
jsesc@3.1.0: {}
|
||||||
@@ -22042,11 +21998,6 @@ snapshots:
|
|||||||
emojilib: 2.4.0
|
emojilib: 2.4.0
|
||||||
skin-tone: 2.0.0
|
skin-tone: 2.0.0
|
||||||
|
|
||||||
node-fetch@2.7.0:
|
|
||||||
dependencies:
|
|
||||||
whatwg-url: 5.0.0
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
node-fetch@2.7.0(encoding@0.1.13):
|
node-fetch@2.7.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
whatwg-url: 5.0.0
|
whatwg-url: 5.0.0
|
||||||
@@ -25171,9 +25122,9 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vite: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
vitest-fetch-mock@0.4.5(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
|
|
||||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -25219,51 +25170,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.11.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2(encoding@0.1.13)))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
||||||
dependencies:
|
|
||||||
'@types/chai': 5.2.3
|
|
||||||
'@vitest/expect': 3.2.4
|
|
||||||
'@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
|
||||||
'@vitest/pretty-format': 3.2.4
|
|
||||||
'@vitest/runner': 3.2.4
|
|
||||||
'@vitest/snapshot': 3.2.4
|
|
||||||
'@vitest/spy': 3.2.4
|
|
||||||
'@vitest/utils': 3.2.4
|
|
||||||
chai: 5.3.3
|
|
||||||
debug: 4.4.3
|
|
||||||
expect-type: 1.3.0
|
|
||||||
magic-string: 0.30.21
|
|
||||||
pathe: 2.0.3
|
|
||||||
picomatch: 4.0.3
|
|
||||||
std-env: 3.10.0
|
|
||||||
tinybench: 2.9.0
|
|
||||||
tinyexec: 0.3.2
|
|
||||||
tinyglobby: 0.2.15
|
|
||||||
tinypool: 1.1.1
|
|
||||||
tinyrainbow: 2.0.0
|
|
||||||
vite: 7.3.1(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
|
||||||
vite-node: 3.2.4(@types/node@24.11.0)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
|
||||||
why-is-node-running: 2.3.0
|
|
||||||
optionalDependencies:
|
|
||||||
'@types/debug': 4.1.12
|
|
||||||
'@types/node': 24.11.0
|
|
||||||
happy-dom: 20.6.3
|
|
||||||
jsdom: 26.1.0(canvas@2.11.2)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- jiti
|
|
||||||
- less
|
|
||||||
- lightningcss
|
|
||||||
- msw
|
|
||||||
- sass
|
|
||||||
- sass-embedded
|
|
||||||
- stylus
|
|
||||||
- sugarss
|
|
||||||
- supports-color
|
|
||||||
- terser
|
|
||||||
- tsx
|
|
||||||
- yaml
|
|
||||||
|
|
||||||
vitest@3.2.4(@types/debug@4.1.12)(@types/node@25.3.0)(happy-dom@20.6.3)(jiti@2.6.1)(jsdom@26.1.0(canvas@2.11.2))(lightningcss@1.31.1)(sass@1.97.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2):
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.4
|
'@vitest/expect': 3.2.4
|
||||||
@@ -25292,7 +25199,7 @@ snapshots:
|
|||||||
'@types/debug': 4.1.12
|
'@types/debug': 4.1.12
|
||||||
'@types/node': 25.3.0
|
'@types/node': 25.3.0
|
||||||
happy-dom: 20.6.3
|
happy-dom: 20.6.3
|
||||||
jsdom: 26.1.0(canvas@2.11.2)
|
jsdom: 26.1.0(canvas@2.11.2(encoding@0.1.13))
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const LOGIN_URL = '/auth/login?autoLaunch=0';
|
|||||||
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
|
||||||
|
|
||||||
export const FACE_THUMBNAIL_SIZE = 250;
|
export const FACE_THUMBNAIL_SIZE = 250;
|
||||||
|
export const TILE_TARGET_SIZE = 1024;
|
||||||
|
|
||||||
type ModelInfo = { dimSize: number };
|
type ModelInfo = { dimSize: number };
|
||||||
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Next,
|
Next,
|
||||||
Param,
|
Param,
|
||||||
ParseFilePipe,
|
ParseFilePipe,
|
||||||
|
ParseIntPipe,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Query,
|
Query,
|
||||||
@@ -185,6 +186,29 @@ export class AssetMediaController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(':id/tiles/:level/:col/:row')
|
||||||
|
@FileResponse()
|
||||||
|
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||||
|
@Endpoint({
|
||||||
|
summary: 'View an asset tile',
|
||||||
|
description: 'Download a specific tile from an image at the specified level - must currently be 0 - and position',
|
||||||
|
history: new HistoryBuilder().added('v2.7.0').stable('v2.7.0'),
|
||||||
|
})
|
||||||
|
async viewAssetTile(
|
||||||
|
@Auth() auth: AuthDto,
|
||||||
|
@Param() { id }: UUIDParamDto,
|
||||||
|
@Param('level', ParseIntPipe) level: number,
|
||||||
|
@Param('col', ParseIntPipe) col: number,
|
||||||
|
@Param('row', ParseIntPipe) row: number,
|
||||||
|
@Res() res: Response,
|
||||||
|
@Next() next: NextFunction,
|
||||||
|
) {
|
||||||
|
if (level !== 0) {
|
||||||
|
throw new Error(`Invalid level ${level}`);
|
||||||
|
}
|
||||||
|
await sendFile(res, next, () => this.service.viewAssetTile(auth, id, level, col, row), this.logger);
|
||||||
|
}
|
||||||
|
|
||||||
@Get(':id/video/playback')
|
@Get(':id/video/playback')
|
||||||
@FileResponse()
|
@FileResponse()
|
||||||
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
@Authenticated({ permission: Permission.AssetView, sharedLink: true })
|
||||||
|
|||||||
@@ -120,6 +120,10 @@ export class StorageCore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getTilesFolder(asset: ThumbnailPathEntity) {
|
||||||
|
return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}_tiles`);
|
||||||
|
}
|
||||||
|
|
||||||
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
|
static getEncodedVideoPath(asset: ThumbnailPathEntity) {
|
||||||
return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`);
|
return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`);
|
||||||
}
|
}
|
||||||
@@ -153,6 +157,16 @@ export class StorageCore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async moveAssetTiles(asset: StorageAsset) {
|
||||||
|
const oldDir = getAssetFile(asset.files, AssetFileType.Tiles, { isEdited: false });
|
||||||
|
return this.moveFile({
|
||||||
|
entityId: asset.id,
|
||||||
|
pathType: AssetPathType.Tiles,
|
||||||
|
oldPath: oldDir?.path || null,
|
||||||
|
newPath: StorageCore.getTilesFolder(asset),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async moveAssetVideo(asset: StorageAsset) {
|
async moveAssetVideo(asset: StorageAsset) {
|
||||||
return this.moveFile({
|
return this.moveFile({
|
||||||
entityId: asset.id,
|
entityId: asset.id,
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export enum AssetFileType {
|
|||||||
Preview = 'preview',
|
Preview = 'preview',
|
||||||
Thumbnail = 'thumbnail',
|
Thumbnail = 'thumbnail',
|
||||||
Sidecar = 'sidecar',
|
Sidecar = 'sidecar',
|
||||||
|
/** Folder structure containing tiles of the image */
|
||||||
|
Tiles = 'tiles',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AlbumUserRole {
|
export enum AlbumUserRole {
|
||||||
@@ -371,6 +373,8 @@ export enum ManualJobName {
|
|||||||
|
|
||||||
export enum AssetPathType {
|
export enum AssetPathType {
|
||||||
Original = 'original',
|
Original = 'original',
|
||||||
|
/** Folder structure containing tiles of the image */
|
||||||
|
Tiles = 'tiles',
|
||||||
EncodedVideo = 'encoded_video',
|
EncodedVideo = 'encoded_video',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1128,6 +1128,19 @@ export class AssetRepository {
|
|||||||
.executeTakeFirstOrThrow();
|
.executeTakeFirstOrThrow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
|
async getForTiles(id: string) {
|
||||||
|
// TODO: we don't actually need original path and file name. Plain 'select asset_file.path from asset_file where type = tiles and assetId = id;'?
|
||||||
|
return this.db
|
||||||
|
.selectFrom('asset')
|
||||||
|
.where('asset.id', '=', id)
|
||||||
|
.leftJoin('asset_file', (join) =>
|
||||||
|
join.onRef('asset.id', '=', 'asset_file.assetId').on('asset_file.type', '=', AssetFileType.Tiles),
|
||||||
|
)
|
||||||
|
.select(['asset.originalPath', 'asset.originalFileName', 'asset_file.path as path'])
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getForVideo(id: string) {
|
async getForVideo(id: string) {
|
||||||
return this.db
|
return this.db
|
||||||
|
|||||||
@@ -182,6 +182,24 @@ export class MediaRepository {
|
|||||||
await decoded.toFile(output);
|
await decoded.toFile(output);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For output file path 'output.dz', this creates an 'output.dzi' file and 'output_files/0' directory containing tiles
|
||||||
|
*/
|
||||||
|
async generateTiles(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise<void> {
|
||||||
|
// size is intended tile size, don't resize input image.
|
||||||
|
const pipeline = await this.getImageDecodingPipeline(input, { ...options, size: undefined });
|
||||||
|
await pipeline
|
||||||
|
.toFormat(options.format) // TODO: set quality and chroma ss?
|
||||||
|
.tile({
|
||||||
|
depth: 'one',
|
||||||
|
size: options.size,
|
||||||
|
})
|
||||||
|
.toFile(output);
|
||||||
|
// TODO: move <uuid>_tiles_files/0 dir to <uuid>_tiles
|
||||||
|
// TODO: delete <uuid>_tiles_files/vips-properties.xml
|
||||||
|
// TODO: delete <uuid>_tiles.dzi
|
||||||
|
}
|
||||||
|
|
||||||
private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
private async getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) {
|
||||||
let pipeline = sharp(input, {
|
let pipeline = sharp(input, {
|
||||||
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
|
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
|
||||||
|
|||||||
@@ -720,6 +720,38 @@ describe(AssetMediaService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getAssetTile', () => {
|
||||||
|
it('should require asset.view permissions', async () => {
|
||||||
|
await expect(sut.viewAssetTile(authStub.admin, 'id', 0, 0, 0)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
|
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']), undefined);
|
||||||
|
expect(mocks.access.asset.checkAlbumAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||||
|
expect(mocks.access.asset.checkPartnerAccess).toHaveBeenCalledWith(userStub.admin.id, new Set(['id']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the asset tiles dir could not be found', async () => {
|
||||||
|
const asset = AssetFactory.create();
|
||||||
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
|
mocks.asset.getForTiles.mockResolvedValue({ ...asset, path: null });
|
||||||
|
|
||||||
|
await expect(sut.viewAssetTile(authStub.admin, asset.id, 0, 0, 0)).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get tile file', async () => {
|
||||||
|
const asset = AssetFactory.from().file({ type: AssetFileType.Tiles, path: '/path/to/asset_tiles' }).build();
|
||||||
|
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set([asset.id]));
|
||||||
|
mocks.asset.getForTiles.mockResolvedValue({ ...asset, path: asset.files[0].path });
|
||||||
|
await expect(sut.viewAssetTile(authStub.admin, asset.id, 0, 0, 0)).resolves.toEqual(
|
||||||
|
new ImmichFileResponse({
|
||||||
|
path: `${asset.files[0].path}_files/0/0_0.jpeg`,
|
||||||
|
cacheControl: CacheControl.PrivateWithCache,
|
||||||
|
contentType: 'image/jpeg',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(mocks.asset.getForTiles).toHaveBeenCalledWith(asset.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('playbackVideo', () => {
|
describe('playbackVideo', () => {
|
||||||
it('should require asset.view permissions', async () => {
|
it('should require asset.view permissions', async () => {
|
||||||
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.playbackVideo(authStub.admin, 'id')).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|||||||
@@ -262,6 +262,26 @@ export class AssetMediaService extends BaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async viewAssetTile(auth: AuthDto, id: string, level: number, col: number, row: number): Promise<ImmichFileResponse> {
|
||||||
|
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
||||||
|
|
||||||
|
// TODO: get tile info { width, height } and check against col, row to return NotFound instead of 500 when tile can't be found in sendFile.
|
||||||
|
const { path } = await this.assetRepository.getForTiles(id);
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
throw new NotFoundException('Asset tiles not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// By definition of the tiles format, it's always .jpeg; should ImageFormat.Jpeg be used?
|
||||||
|
const tilePath = `${path}_files/${level}/${col}_${row}.jpeg`;
|
||||||
|
|
||||||
|
return new ImmichFileResponse({
|
||||||
|
path: tilePath,
|
||||||
|
contentType: mimeTypes.lookup(tilePath),
|
||||||
|
cacheControl: CacheControl.PrivateWithCache,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
|
||||||
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
|
||||||
|
|
||||||
|
|||||||
@@ -1246,8 +1246,7 @@ describe(MediaService.name, () => {
|
|||||||
expect.any(String),
|
expect.any(String),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledTimes(2);
|
expect(mocks.media.copyTagGroup).toHaveBeenCalledExactlyOnceWith('XMP-GPano', asset.originalPath, expect.any(String));
|
||||||
expect(mocks.media.copyTagGroup).toHaveBeenCalledWith('XMP-GPano', asset.originalPath, expect.any(String));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should respect encoding options when generating full-size preview', async () => {
|
it('should respect encoding options when generating full-size preview', async () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
|
import { FACE_THUMBNAIL_SIZE, JOBS_ASSET_PAGINATION_SIZE, TILE_TARGET_SIZE } from 'src/constants';
|
||||||
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core';
|
import { ImagePathOptions, StorageCore, ThumbnailPathEntity } from 'src/cores/storage.core';
|
||||||
import { AssetFile, Exif } from 'src/database';
|
import { AssetFile, Exif } from 'src/database';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
DecodeToBufferOptions,
|
DecodeToBufferOptions,
|
||||||
GenerateThumbnailOptions,
|
GenerateThumbnailOptions,
|
||||||
ImageDimensions,
|
ImageDimensions,
|
||||||
|
ImageOptions,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobOf,
|
JobOf,
|
||||||
VideoFormat,
|
VideoFormat,
|
||||||
@@ -163,6 +164,7 @@ export class MediaService extends BaseService {
|
|||||||
await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format);
|
await this.storageCore.moveAssetImage(asset, AssetFileType.FullSize, image.fullsize.format);
|
||||||
await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format);
|
await this.storageCore.moveAssetImage(asset, AssetFileType.Preview, image.preview.format);
|
||||||
await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format);
|
await this.storageCore.moveAssetImage(asset, AssetFileType.Thumbnail, image.thumbnail.format);
|
||||||
|
await this.storageCore.moveAssetTiles(asset);
|
||||||
await this.storageCore.moveAssetVideo(asset);
|
await this.storageCore.moveAssetVideo(asset);
|
||||||
|
|
||||||
return JobStatus.Success;
|
return JobStatus.Success;
|
||||||
@@ -276,8 +278,8 @@ export class MediaService extends BaseService {
|
|||||||
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||||
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||||
const generateFullsize =
|
const generateFullsize =
|
||||||
((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') &&
|
(image.fullsize.enabled && !mimeTypes.isWebSupportedImage(asset.originalPath)) ||
|
||||||
!mimeTypes.isWebSupportedImage(asset.originalPath)) ||
|
asset.exifInfo.projectionType === 'EQUIRECTANGULAR' ||
|
||||||
useEdits;
|
useEdits;
|
||||||
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||||
|
|
||||||
@@ -383,23 +385,60 @@ export class MediaService extends BaseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: probably extract to helper method
|
||||||
|
// TODO: handle cropped panoramas. Tile as normal but save some offset?
|
||||||
|
let tileInfo: UpsertFileOptions | undefined;
|
||||||
|
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') {
|
||||||
|
// TODO: get uncropped width from asset (FullPanoWidthPixels if present). -> TODO find out why i wrote this down as a todo
|
||||||
|
const originalSize = asset.exifInfo.exifImageWidth!;
|
||||||
|
|
||||||
|
// Get the number of tiles at the exact target size, rounded up (to at least 1 tile).
|
||||||
|
const numTilesExact = Math.ceil(originalSize / TILE_TARGET_SIZE);
|
||||||
|
// Then round up to the nearest power of 2 (photo-sphere-viewer requirement).
|
||||||
|
const numTiles = Math.pow(2, Math.ceil(Math.log2(numTilesExact)));
|
||||||
|
const tileSize = Math.ceil(originalSize / numTiles);
|
||||||
|
|
||||||
|
tileInfo = {
|
||||||
|
assetId: asset.id,
|
||||||
|
type: AssetFileType.Tiles,
|
||||||
|
path: StorageCore.getTilesFolder(asset),
|
||||||
|
isEdited: false,
|
||||||
|
isProgressive: false,
|
||||||
|
isTransparent: false,
|
||||||
|
};
|
||||||
|
const tilesOptions = {
|
||||||
|
...baseOptions,
|
||||||
|
quality: image.preview.quality,
|
||||||
|
format: ImageFormat.Jpeg,
|
||||||
|
size: tileSize,
|
||||||
|
};
|
||||||
|
promises.push(this.mediaRepository.generateTiles(data, tilesOptions, tileInfo.path));
|
||||||
|
|
||||||
|
console.log('Tile info for DB:', {
|
||||||
|
width: originalSize,
|
||||||
|
cols: numTiles,
|
||||||
|
rows: numTiles / 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const outputs = await Promise.all(promises);
|
const outputs = await Promise.all(promises);
|
||||||
|
|
||||||
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') {
|
if (asset.exifInfo.projectionType === 'EQUIRECTANGULAR') {
|
||||||
const promises = [
|
await this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewFile.path);
|
||||||
this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, previewFile.path),
|
|
||||||
fullsizeFile
|
|
||||||
? this.mediaRepository.copyTagGroup('XMP-GPano', asset.originalPath, fullsizeFile.path)
|
|
||||||
: Promise.resolve(),
|
|
||||||
];
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decodedDimensions = { width: info.width, height: info.height };
|
const decodedDimensions = { width: info.width, height: info.height };
|
||||||
const fullsizeDimensions = useEdits ? getOutputDimensions(asset.edits, decodedDimensions) : decodedDimensions;
|
const fullsizeDimensions = useEdits ? getOutputDimensions(asset.edits, decodedDimensions) : decodedDimensions;
|
||||||
|
const files = [previewFile, thumbnailFile];
|
||||||
|
if (fullsizeFile) {
|
||||||
|
files.push(fullsizeFile);
|
||||||
|
}
|
||||||
|
if (tileInfo) {
|
||||||
|
files.push(tileInfo);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
files: fullsizeFile ? [previewFile, thumbnailFile, fullsizeFile] : [previewFile, thumbnailFile],
|
files,
|
||||||
thumbhash: outputs[0] as Buffer,
|
thumbhash: outputs[0] as Buffer,
|
||||||
fullsizeDimensions,
|
fullsizeDimensions,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const getAssetFiles = (files: AssetFile[]) => ({
|
|||||||
fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }),
|
fullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: false }),
|
||||||
previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }),
|
previewFile: getAssetFile(files, AssetFileType.Preview, { isEdited: false }),
|
||||||
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }),
|
thumbnailFile: getAssetFile(files, AssetFileType.Thumbnail, { isEdited: false }),
|
||||||
|
tilesPath: getAssetFile(files, AssetFileType.Tiles, { isEdited: false }),
|
||||||
sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }),
|
sidecarFile: getAssetFile(files, AssetFileType.Sidecar, { isEdited: false }),
|
||||||
|
|
||||||
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }),
|
editedFullsizeFile: getAssetFile(files, AssetFileType.FullSize, { isEdited: true }),
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export const newAssetRepositoryMock = (): Mocked<RepositoryInterface<AssetReposi
|
|||||||
getForOriginal: vitest.fn(),
|
getForOriginal: vitest.fn(),
|
||||||
getForOriginals: vitest.fn(),
|
getForOriginals: vitest.fn(),
|
||||||
getForThumbnail: vitest.fn(),
|
getForThumbnail: vitest.fn(),
|
||||||
|
getForTiles: vitest.fn(),
|
||||||
getForVideo: vitest.fn(),
|
getForVideo: vitest.fn(),
|
||||||
getForEdit: vitest.fn(),
|
getForEdit: vitest.fn(),
|
||||||
getForOcr: vitest.fn(),
|
getForOcr: vitest.fn(),
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Mocked, vitest } from 'vitest';
|
|||||||
export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaRepository>> => {
|
export const newMediaRepositoryMock = (): Mocked<RepositoryInterface<MediaRepository>> => {
|
||||||
return {
|
return {
|
||||||
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
|
generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
|
generateTiles: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
writeExif: vitest.fn().mockImplementation(() => Promise.resolve()),
|
writeExif: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
copyTagGroup: vitest.fn().mockImplementation(() => Promise.resolve()),
|
copyTagGroup: vitest.fn().mockImplementation(() => Promise.resolve()),
|
||||||
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
generateThumbhash: vitest.fn().mockResolvedValue(Buffer.from('')),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
"@mapbox/mapbox-gl-rtl-text": "0.3.0",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.14.0",
|
"@photo-sphere-viewer/core": "^5.14.0",
|
||||||
|
"@photo-sphere-viewer/equirectangular-tiles-adapter": "^5.14.1",
|
||||||
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
|
"@photo-sphere-viewer/equirectangular-video-adapter": "^5.14.0",
|
||||||
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
|
"@photo-sphere-viewer/markers-plugin": "^5.14.0",
|
||||||
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
"@photo-sphere-viewer/resolution-plugin": "^5.14.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { getAssetUrl } from '$lib/utils';
|
import { getAssetTileUrl, getAssetUrl } from '$lib/utils';
|
||||||
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { LoadingSpinner } from '@immich/ui';
|
import { LoadingSpinner } from '@immich/ui';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -14,17 +14,39 @@
|
|||||||
|
|
||||||
const assetId = $derived(asset.id);
|
const assetId = $derived(asset.id);
|
||||||
|
|
||||||
|
// TODO: get this via asset.tiles or whatever through the API
|
||||||
|
const tileconfig = $derived.by(() => {
|
||||||
|
// Get the number of tiles at the exact target size, rounded up (to at least 1 tile).
|
||||||
|
const numTilesExact = Math.ceil(asset.exifInfo?.exifImageWidth! / 1024);
|
||||||
|
// Then round up to the nearest power of 2 (photo-sphere-viewer requirement).
|
||||||
|
const numTiles = Math.pow(2, Math.ceil(Math.log2(numTilesExact)));
|
||||||
|
return {
|
||||||
|
width: asset.exifInfo?.exifImageWidth!,
|
||||||
|
cols: numTiles,
|
||||||
|
rows: numTiles / 2,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const loadAssetData = async (id: string) => {
|
const loadAssetData = async (id: string) => {
|
||||||
const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview });
|
const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview });
|
||||||
return URL.createObjectURL(data);
|
return URL.createObjectURL(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: determine whether to return null based on 1. if asset has tiles, 2. if tile is inside 'cropped' bounds.
|
||||||
|
const tileUrl = (col: number, row: number, level: number) =>
|
||||||
|
tileconfig ? getAssetTileUrl({ id: asset.id, level, col, row, cacheKey: asset.thumbhash }) : null;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||||
{#await Promise.all([loadAssetData(assetId), import('./photo-sphere-viewer-adapter.svelte')])}
|
{#await Promise.all([loadAssetData(assetId), import('./photo-sphere-viewer-adapter.svelte')])}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then [data, { default: PhotoSphereViewer }]}
|
{:then [data, { default: PhotoSphereViewer }]}
|
||||||
<PhotoSphereViewer panorama={data} originalPanorama={getAssetUrl({ asset, forceOriginal: true })} />
|
<PhotoSphereViewer
|
||||||
|
baseUrl={data}
|
||||||
|
{tileUrl}
|
||||||
|
{tileconfig}
|
||||||
|
originalPanorama={getAssetUrl({ asset, forceOriginal: true })}
|
||||||
|
/>
|
||||||
{:catch}
|
{:catch}
|
||||||
{$t('errors.failed_to_load_asset')}
|
{$t('errors.failed_to_load_asset')}
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
type PluginConstructor,
|
type PluginConstructor,
|
||||||
} from '@photo-sphere-viewer/core';
|
} from '@photo-sphere-viewer/core';
|
||||||
import '@photo-sphere-viewer/core/index.css';
|
import '@photo-sphere-viewer/core/index.css';
|
||||||
|
import { EquirectangularTilesAdapter } from '@photo-sphere-viewer/equirectangular-tiles-adapter';
|
||||||
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
|
import { MarkersPlugin } from '@photo-sphere-viewer/markers-plugin';
|
||||||
import '@photo-sphere-viewer/markers-plugin/index.css';
|
import '@photo-sphere-viewer/markers-plugin/index.css';
|
||||||
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
import { ResolutionPlugin } from '@photo-sphere-viewer/resolution-plugin';
|
||||||
@@ -29,6 +30,14 @@
|
|||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SHARED_VIEWER_CONFIG = {
|
||||||
|
touchmoveTwoFingers: false,
|
||||||
|
mousewheelCtrlKey: false,
|
||||||
|
minFov: 15,
|
||||||
|
maxFov: 90,
|
||||||
|
zoomSpeed: 0.5,
|
||||||
|
fisheye: false,
|
||||||
|
};
|
||||||
// Adapted as well as possible from classlist 'border-2 border-blue-500 bg-blue-500/10 hover:border-blue-600 hover:border-3'
|
// Adapted as well as possible from classlist 'border-2 border-blue-500 bg-blue-500/10 hover:border-blue-600 hover:border-3'
|
||||||
const OCR_BOX_SVG_STYLE = {
|
const OCR_BOX_SVG_STYLE = {
|
||||||
fill: 'var(--color-blue-500)',
|
fill: 'var(--color-blue-500)',
|
||||||
@@ -40,15 +49,31 @@
|
|||||||
const OCR_TOOLTIP_HTML_CLASS =
|
const OCR_TOOLTIP_HTML_CLASS =
|
||||||
'flex items-center justify-center text-white bg-black/50 cursor-text pointer-events-auto whitespace-pre-wrap wrap-break-word select-text';
|
'flex items-center justify-center text-white bg-black/50 cursor-text pointer-events-auto whitespace-pre-wrap wrap-break-word select-text';
|
||||||
|
|
||||||
|
type TileConfig = {
|
||||||
|
width: number;
|
||||||
|
cols: number;
|
||||||
|
rows: number;
|
||||||
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
panorama: string | { source: string };
|
baseUrl: string | { source: string };
|
||||||
|
tileUrl?: (col: number, row: number, level: number) => string | null;
|
||||||
|
tileconfig?: TileConfig;
|
||||||
originalPanorama?: string | { source: string };
|
originalPanorama?: string | { source: string };
|
||||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||||
navbar?: boolean;
|
navbar?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
let {
|
||||||
|
baseUrl,
|
||||||
|
tileUrl,
|
||||||
|
tileconfig,
|
||||||
|
originalPanorama,
|
||||||
|
adapter = EquirectangularAdapter,
|
||||||
|
plugins = [],
|
||||||
|
navbar = false,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
let container: HTMLDivElement | undefined = $state();
|
let container: HTMLDivElement | undefined = $state();
|
||||||
let viewer: Viewer;
|
let viewer: Viewer;
|
||||||
@@ -172,20 +197,32 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
viewer = new Viewer({
|
if (tileconfig) {
|
||||||
adapter,
|
viewer = new Viewer({
|
||||||
plugins: [
|
adapter: EquirectangularTilesAdapter,
|
||||||
MarkersPlugin,
|
panorama: {
|
||||||
SettingsPlugin,
|
...tileconfig,
|
||||||
[
|
baseUrl,
|
||||||
ResolutionPlugin,
|
tileUrl,
|
||||||
{
|
},
|
||||||
|
plugins: [MarkersPlugin, ...plugins],
|
||||||
|
container,
|
||||||
|
navbar,
|
||||||
|
...SHARED_VIEWER_CONFIG,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
viewer = new Viewer({
|
||||||
|
adapter,
|
||||||
|
plugins: [
|
||||||
|
MarkersPlugin,
|
||||||
|
SettingsPlugin,
|
||||||
|
ResolutionPlugin.withConfig({
|
||||||
defaultResolution: $alwaysLoadOriginalFile && originalPanorama ? 'original' : 'default',
|
defaultResolution: $alwaysLoadOriginalFile && originalPanorama ? 'original' : 'default',
|
||||||
resolutions: [
|
resolutions: [
|
||||||
{
|
{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
label: 'Default',
|
label: 'Default',
|
||||||
panorama,
|
panorama: baseUrl,
|
||||||
},
|
},
|
||||||
...(originalPanorama
|
...(originalPanorama
|
||||||
? [
|
? [
|
||||||
@@ -197,24 +234,25 @@
|
|||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
],
|
],
|
||||||
},
|
}),
|
||||||
|
...plugins,
|
||||||
],
|
],
|
||||||
...plugins,
|
container,
|
||||||
],
|
navbar,
|
||||||
container,
|
...SHARED_VIEWER_CONFIG,
|
||||||
touchmoveTwoFingers: false,
|
});
|
||||||
mousewheelCtrlKey: false,
|
}
|
||||||
navbar,
|
|
||||||
minFov: 15,
|
|
||||||
maxFov: 90,
|
|
||||||
zoomSpeed: 0.5,
|
|
||||||
fisheye: false,
|
|
||||||
});
|
|
||||||
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
||||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||||
// zoomLevel is 0-100
|
// zoomLevel is 0-100
|
||||||
assetViewerManager.zoom = zoomLevel / 50;
|
assetViewerManager.zoom = zoomLevel / 50;
|
||||||
|
|
||||||
|
if (!resolutionPlugin) {
|
||||||
|
hasChangedResolution = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Math.round(zoomLevel) >= 75 && !hasChangedResolution) {
|
if (Math.round(zoomLevel) >= 75 && !hasChangedResolution) {
|
||||||
// Replace the preview with the original
|
// Replace the preview with the original
|
||||||
void resolutionPlugin.setResolution('original');
|
void resolutionPlugin.setResolution('original');
|
||||||
|
|||||||
@@ -24,11 +24,10 @@
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then [PhotoSphereViewer, adapter, videoPlugin]}
|
{:then [PhotoSphereViewer, adapter, videoPlugin]}
|
||||||
<PhotoSphereViewer
|
<PhotoSphereViewer
|
||||||
panorama={{ source: getAssetPlaybackUrl({ id: asset.id }) }}
|
baseUrl={{ source: getAssetPlaybackUrl({ id: asset.id }) }}
|
||||||
originalPanorama={{ source: getAssetUrl({ asset, forceOriginal: true })! }}
|
originalPanorama={{ source: getAssetUrl({ asset, forceOriginal: true })! }}
|
||||||
plugins={[videoPlugin]}
|
plugins={[videoPlugin]}
|
||||||
{adapter}
|
{adapter}
|
||||||
navbar
|
|
||||||
/>
|
/>
|
||||||
{:catch}
|
{:catch}
|
||||||
{$t('errors.failed_to_load_asset')}
|
{$t('errors.failed_to_load_asset')}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getAssetOriginalPath,
|
getAssetOriginalPath,
|
||||||
getAssetPlaybackPath,
|
getAssetPlaybackPath,
|
||||||
getAssetThumbnailPath,
|
getAssetThumbnailPath,
|
||||||
|
getAssetTilePath,
|
||||||
getBaseUrl,
|
getBaseUrl,
|
||||||
getPeopleThumbnailPath,
|
getPeopleThumbnailPath,
|
||||||
getUserProfileImagePath,
|
getUserProfileImagePath,
|
||||||
@@ -237,6 +238,13 @@ export const getAssetPlaybackUrl = (options: AssetUrlOptions) => {
|
|||||||
return createUrl(getAssetPlaybackPath(id), { ...authManager.params, c });
|
return createUrl(getAssetPlaybackPath(id), { ...authManager.params, c });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type TileUrlOptions = { id: string, level: number, col: number, row: number, cacheKey?: string | null };
|
||||||
|
|
||||||
|
export const getAssetTileUrl = (options: TileUrlOptions) => {
|
||||||
|
const { id, level, col, row, cacheKey } = options;
|
||||||
|
return createUrl(getAssetTilePath(id, level, col, row), { ...authManager.params, c: cacheKey });
|
||||||
|
}
|
||||||
|
|
||||||
export const getProfileImageUrl = (user: UserResponseDto) =>
|
export const getProfileImageUrl = (user: UserResponseDto) =>
|
||||||
createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt });
|
createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user