diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 319deb2acfff3..4c5d7fc297689 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -1393,7 +1393,7 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) # **uploadFile** -> AssetFileUploadResponseDto uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration) +> AssetFileUploadResponseDto uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, isPanorama, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration) @@ -1424,6 +1424,7 @@ final deviceId = deviceId_example; // String | final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime | final fileModifiedAt = 2013-10-20T19:20:30+01:00; // DateTime | final isFavorite = true; // bool | +final isPanorama = true; // bool | final key = key_example; // String | final livePhotoData = BINARY_DATA_HERE; // MultipartFile | final sidecarData = BINARY_DATA_HERE; // MultipartFile | @@ -1433,7 +1434,7 @@ final isVisible = true; // bool | final duration = duration_example; // String | try { - final result = api_instance.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration); + final result = api_instance.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, isPanorama, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration); print(result); } catch (e) { print('Exception when calling AssetApi->uploadFile: $e\n'); @@ -1452,6 +1453,7 @@ Name | Type | Description | Notes **fileCreatedAt** | **DateTime**| | **fileModifiedAt** | **DateTime**| | **isFavorite** | **bool**| | + **isPanorama** | **bool**| | **key** | **String**| | [optional] **livePhotoData** | **MultipartFile**| | [optional] **sidecarData** | **MultipartFile**| | [optional] diff --git a/mobile/openapi/doc/AssetResponseDto.md b/mobile/openapi/doc/AssetResponseDto.md index 986ca1b53d060..f8914de38a6ae 100644 --- a/mobile/openapi/doc/AssetResponseDto.md +++ b/mobile/openapi/doc/AssetResponseDto.md @@ -30,6 +30,7 @@ Name | Type | Description | Notes **tags** | [**List**](TagResponseDto.md) | | [optional] [default to const []] **people** | [**List**](PersonResponseDto.md) | | [optional] [default to const []] **checksum** | **String** | base64 encoded sha1 hash | +**isPanorama** | **bool** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/ImportAssetDto.md b/mobile/openapi/doc/ImportAssetDto.md index da612b0abccf7..66ef5e72bf293 100644 --- a/mobile/openapi/doc/ImportAssetDto.md +++ b/mobile/openapi/doc/ImportAssetDto.md @@ -17,6 +17,7 @@ Name | Type | Description | Notes **fileCreatedAt** | [**DateTime**](DateTime.md) | | **fileModifiedAt** | [**DateTime**](DateTime.md) | | **isFavorite** | **bool** | | +**isPanorama** | **bool** | | **isArchived** | **bool** | | [optional] **isVisible** | **bool** | | [optional] **duration** | **String** | | [optional] diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index a73ec3b1ec224..3cab0a975799b 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -1375,6 +1375,8 @@ class AssetApi { /// /// * [bool] isFavorite (required): /// + /// * [bool] isPanorama (required): + /// /// * [String] key: /// /// * [MultipartFile] livePhotoData: @@ -1388,7 +1390,7 @@ class AssetApi { /// * [bool] isVisible: /// /// * [String] duration: - Future uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async { + Future uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, bool isPanorama, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async { // ignore: prefer_const_declarations final path = r'/asset/upload'; @@ -1454,6 +1456,10 @@ class AssetApi { hasFields = true; mp.fields[r'isFavorite'] = parameterToString(isFavorite); } + if (isPanorama != null) { + hasFields = true; + mp.fields[r'isPanorama'] = parameterToString(isPanorama); + } if (isArchived != null) { hasFields = true; mp.fields[r'isArchived'] = parameterToString(isArchived); @@ -1499,6 +1505,8 @@ class AssetApi { /// /// * [bool] isFavorite (required): /// + /// * [bool] isPanorama (required): + /// /// * [String] key: /// /// * [MultipartFile] livePhotoData: @@ -1512,8 +1520,8 @@ class AssetApi { /// * [bool] isVisible: /// /// * [String] duration: - Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async { - final response = await uploadFileWithHttpInfo(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, ); + Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, bool isPanorama, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async { + final response = await uploadFileWithHttpInfo(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, isPanorama, key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index cd74d57214606..a88f63415f5df 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -35,6 +35,7 @@ class AssetResponseDto { this.tags = const [], this.people = const [], required this.checksum, + required this.isPanorama, }); AssetTypeEnum type; @@ -95,6 +96,8 @@ class AssetResponseDto { /// base64 encoded sha1 hash String checksum; + bool isPanorama; + @override bool operator ==(Object other) => identical(this, other) || other is AssetResponseDto && other.type == type && @@ -118,7 +121,8 @@ class AssetResponseDto { other.livePhotoVideoId == livePhotoVideoId && other.tags == tags && other.people == people && - other.checksum == checksum; + other.checksum == checksum && + other.isPanorama == isPanorama; @override int get hashCode => @@ -144,10 +148,11 @@ class AssetResponseDto { (livePhotoVideoId == null ? 0 : livePhotoVideoId!.hashCode) + (tags.hashCode) + (people.hashCode) + - (checksum.hashCode); + (checksum.hashCode) + + (isPanorama.hashCode); @override - String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, thumbhash=$thumbhash, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum]'; + String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, thumbhash=$thumbhash, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum, isPanorama=$isPanorama]'; Map toJson() { final json = {}; @@ -193,6 +198,7 @@ class AssetResponseDto { json[r'tags'] = this.tags; json[r'people'] = this.people; json[r'checksum'] = this.checksum; + json[r'isPanorama'] = this.isPanorama; return json; } @@ -226,6 +232,7 @@ class AssetResponseDto { tags: TagResponseDto.listFromJson(json[r'tags']), people: PersonResponseDto.listFromJson(json[r'people']), checksum: mapValueOfType(json, r'checksum')!, + isPanorama: mapValueOfType(json, r'isPanorama')!, ); } return null; @@ -290,6 +297,7 @@ class AssetResponseDto { 'mimeType', 'duration', 'checksum', + 'isPanorama', }; } diff --git a/mobile/openapi/lib/model/import_asset_dto.dart b/mobile/openapi/lib/model/import_asset_dto.dart index dd67e89fb6273..f9e285a5f2372 100644 --- a/mobile/openapi/lib/model/import_asset_dto.dart +++ b/mobile/openapi/lib/model/import_asset_dto.dart @@ -22,6 +22,7 @@ class ImportAssetDto { required this.fileCreatedAt, required this.fileModifiedAt, required this.isFavorite, + required this.isPanorama, this.isArchived, this.isVisible, this.duration, @@ -51,6 +52,8 @@ class ImportAssetDto { bool isFavorite; + bool isPanorama; + /// /// 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 @@ -86,6 +89,7 @@ class ImportAssetDto { other.fileCreatedAt == fileCreatedAt && other.fileModifiedAt == fileModifiedAt && other.isFavorite == isFavorite && + other.isPanorama == isPanorama && other.isArchived == isArchived && other.isVisible == isVisible && other.duration == duration; @@ -102,12 +106,13 @@ class ImportAssetDto { (fileCreatedAt.hashCode) + (fileModifiedAt.hashCode) + (isFavorite.hashCode) + + (isPanorama.hashCode) + (isArchived == null ? 0 : isArchived!.hashCode) + (isVisible == null ? 0 : isVisible!.hashCode) + (duration == null ? 0 : duration!.hashCode); @override - String toString() => 'ImportAssetDto[assetType=$assetType, isReadOnly=$isReadOnly, assetPath=$assetPath, sidecarPath=$sidecarPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isFavorite=$isFavorite, isArchived=$isArchived, isVisible=$isVisible, duration=$duration]'; + String toString() => 'ImportAssetDto[assetType=$assetType, isReadOnly=$isReadOnly, assetPath=$assetPath, sidecarPath=$sidecarPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isFavorite=$isFavorite, isPanorama=$isPanorama, isArchived=$isArchived, isVisible=$isVisible, duration=$duration]'; Map toJson() { final json = {}; @@ -124,6 +129,7 @@ class ImportAssetDto { json[r'fileCreatedAt'] = this.fileCreatedAt.toUtc().toIso8601String(); json[r'fileModifiedAt'] = this.fileModifiedAt.toUtc().toIso8601String(); json[r'isFavorite'] = this.isFavorite; + json[r'isPanorama'] = this.isPanorama; if (this.isArchived != null) { json[r'isArchived'] = this.isArchived; } else { @@ -159,6 +165,7 @@ class ImportAssetDto { fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'')!, fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'')!, isFavorite: mapValueOfType(json, r'isFavorite')!, + isPanorama: mapValueOfType(json, r'isPanorama')!, isArchived: mapValueOfType(json, r'isArchived'), isVisible: mapValueOfType(json, r'isVisible'), duration: mapValueOfType(json, r'duration'), @@ -216,6 +223,7 @@ class ImportAssetDto { 'fileCreatedAt', 'fileModifiedAt', 'isFavorite', + 'isPanorama', }; } diff --git a/mobile/openapi/pubspec.yaml b/mobile/openapi/pubspec.yaml index e61130a38db8c..00043ba660cd8 100644 --- a/mobile/openapi/pubspec.yaml +++ b/mobile/openapi/pubspec.yaml @@ -10,7 +10,7 @@ environment: sdk: '>=2.12.0 <3.0.0' dependencies: http: '>=0.13.0 <0.14.0' - intl: '^0.18.0' + intl: '^0.17.0' meta: '^1.1.8' dev_dependencies: test: '>=1.16.0 <1.18.0' diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 1c5f08536b4b5..1c0c473162d51 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -151,7 +151,7 @@ void main() { // TODO }); - //Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isReadOnly, bool isArchived, bool isVisible, String duration }) async + //Future uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, bool isPanorama, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isReadOnly, bool isArchived, bool isVisible, String duration }) async test('test uploadFile', () async { // TODO }); diff --git a/mobile/openapi/test/asset_response_dto_test.dart b/mobile/openapi/test/asset_response_dto_test.dart index 0bbcde25711ef..487d56507d148 100644 --- a/mobile/openapi/test/asset_response_dto_test.dart +++ b/mobile/openapi/test/asset_response_dto_test.dart @@ -128,6 +128,11 @@ void main() { // TODO }); + // bool isPanorama + test('to test the property `isPanorama`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/import_asset_dto_test.dart b/mobile/openapi/test/import_asset_dto_test.dart index ca7526cc24342..ea8b48b7a893e 100644 --- a/mobile/openapi/test/import_asset_dto_test.dart +++ b/mobile/openapi/test/import_asset_dto_test.dart @@ -61,6 +61,11 @@ void main() { // TODO }); + // bool isPanorama + test('to test the property `isPanorama`', () async { + // TODO + }); + // bool isArchived test('to test the property `isArchived`', () async { // TODO diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 34f459842d12c..87043f5ebec3e 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -4892,6 +4892,9 @@ "checksum": { "type": "string", "description": "base64 encoded sha1 hash" + }, + "isPanorama": { + "type": "boolean" } }, "required": [ @@ -4911,7 +4914,8 @@ "isArchived", "mimeType", "duration", - "checksum" + "checksum", + "isPanorama" ] }, "AssetTypeEnum": { diff --git a/server/openapi-generator/templates/web/apiInner.mustache b/server/openapi-generator/templates/web/apiInner.mustache index 848b41b971d55..1f456b5f037f8 100644 --- a/server/openapi-generator/templates/web/apiInner.mustache +++ b/server/openapi-generator/templates/web/apiInner.mustache @@ -175,8 +175,7 @@ export const {{classname}}AxiosParamCreator = function (configuration?: Configur localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}} localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}} localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isEnum}}{{^isEnum}} - localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}} - }{{/isArray}} + localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}} }{{/isArray}} {{/formParams}}{{/vendorExtensions}} {{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}} localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';{{/multipartFormData}}{{#multipartFormData}} diff --git a/server/package-lock.json b/server/package-lock.json index 8b5cd7ec6b5af..1f99b6075ce82 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -29,7 +29,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "^19.0.0", + "exiftool-vendored": "^22.0.0", "exiftool-vendored.pl": "^12.54.0", "fluent-ffmpeg": "^2.1.2", "handlebars": "^4.7.7", @@ -55,8 +55,8 @@ "ua-parser-js": "^1.0.35" }, "bin": { - "immich": "./bin/cli.sh", - "immich-admin": "./bin/admin-cli.sh" + "immich": "bin/cli.sh", + "immich-admin": "bin/admin-cli.sh" }, "devDependencies": { "@nestjs/cli": "^9.1.8", @@ -4020,9 +4020,9 @@ } }, "node_modules/batch-cluster": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-11.0.0.tgz", - "integrity": "sha512-8iwqa+rKTaakOHkqdcXDT5L5117pa+FoP8/yAKpNdL44ZnC4V2NEA/sIg0ZO0O9NkpdjLk0A3efRFM5nVizqHw==", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz", + "integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg==", "engines": { "node": ">=14" } @@ -5830,25 +5830,25 @@ } }, "node_modules/exiftool-vendored": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-19.0.0.tgz", - "integrity": "sha512-Zes7TZrYWxts92mbF2Gs3drtWZucm4qsaeYaE6A+OOqmeD9UGaGisqIbyh9MilJrLi+ZHzWEJZtDj37QFf6xsA==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-22.0.0.tgz", + "integrity": "sha512-gBOQ4C2GLjxKPDPRuUbMOz91mG6IFA22L+Z/IQzFotFu20vc7YroqHALf/ophCbANA5sNSArbVDPijP7n/20Jg==", "dependencies": { "@photostructure/tz-lookup": "^7.0.0", - "@types/luxon": "^3.2.0", - "batch-cluster": "^11.0.0", + "@types/luxon": "^3.3.0", + "batch-cluster": "^12.1.0", "he": "^1.2.0", - "luxon": "^3.2.1" + "luxon": "^3.3.0" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.54.0", - "exiftool-vendored.pl": "12.54.0" + "exiftool-vendored.exe": "12.62.0", + "exiftool-vendored.pl": "12.62.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "12.54.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.54.0.tgz", - "integrity": "sha512-Dc4W6e0NtQfYuJIYK4piHfDJnd2jvA04e0aaq9R3Q1oO34KC5e+L1D2C7lFuZXqPQLYC1x3GYc/GVv5e+SkkrQ==", + "version": "12.62.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.62.0.tgz", + "integrity": "sha512-xNFkvmjwnMg6ivtmkc67w1qD23fIy06nRpMpGuBpTwTqAVatHV+vk7T75zyvLoXRRpd1rKID9XAVLGJCE/iiMQ==", "optional": true, "os": [ "win32" @@ -5862,15 +5862,6 @@ "!win32" ] }, - "node_modules/exiftool-vendored/node_modules/exiftool-vendored.pl": { - "version": "12.54.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.54.0.tgz", - "integrity": "sha512-RBBowsYcM6EvbWoBkg2dOqHpH3WIzN7bIzHc+o+LquqCTo3doZwECClD/6PNHVSMQsl2Z0fEf75sNq2msooMSg==", - "optional": true, - "os": [ - "!win32" - ] - }, "node_modules/exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -9079,7 +9070,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -9327,7 +9317,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8.6" }, @@ -12213,7 +12203,6 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, "engines": { "node": ">=10" }, @@ -15250,9 +15239,9 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, "batch-cluster": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-11.0.0.tgz", - "integrity": "sha512-8iwqa+rKTaakOHkqdcXDT5L5117pa+FoP8/yAKpNdL44ZnC4V2NEA/sIg0ZO0O9NkpdjLk0A3efRFM5nVizqHw==" + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz", + "integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg==" }, "bcrypt": { "version": "5.1.0", @@ -16593,31 +16582,23 @@ } }, "exiftool-vendored": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-19.0.0.tgz", - "integrity": "sha512-Zes7TZrYWxts92mbF2Gs3drtWZucm4qsaeYaE6A+OOqmeD9UGaGisqIbyh9MilJrLi+ZHzWEJZtDj37QFf6xsA==", + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-22.0.0.tgz", + "integrity": "sha512-gBOQ4C2GLjxKPDPRuUbMOz91mG6IFA22L+Z/IQzFotFu20vc7YroqHALf/ophCbANA5sNSArbVDPijP7n/20Jg==", "requires": { "@photostructure/tz-lookup": "^7.0.0", - "@types/luxon": "^3.2.0", - "batch-cluster": "^11.0.0", - "exiftool-vendored.exe": "12.54.0", - "exiftool-vendored.pl": "12.54.0", + "@types/luxon": "^3.3.0", + "batch-cluster": "^12.1.0", + "exiftool-vendored.exe": "12.62.0", + "exiftool-vendored.pl": "12.62.0", "he": "^1.2.0", - "luxon": "^3.2.1" - }, - "dependencies": { - "exiftool-vendored.pl": { - "version": "12.54.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.54.0.tgz", - "integrity": "sha512-RBBowsYcM6EvbWoBkg2dOqHpH3WIzN7bIzHc+o+LquqCTo3doZwECClD/6PNHVSMQsl2Z0fEf75sNq2msooMSg==", - "optional": true - } + "luxon": "^3.3.0" } }, "exiftool-vendored.exe": { - "version": "12.54.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.54.0.tgz", - "integrity": "sha512-Dc4W6e0NtQfYuJIYK4piHfDJnd2jvA04e0aaq9R3Q1oO34KC5e+L1D2C7lFuZXqPQLYC1x3GYc/GVv5e+SkkrQ==", + "version": "12.62.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.62.0.tgz", + "integrity": "sha512-xNFkvmjwnMg6ivtmkc67w1qD23fIy06nRpMpGuBpTwTqAVatHV+vk7T75zyvLoXRRpd1rKID9XAVLGJCE/iiMQ==", "optional": true }, "exiftool-vendored.pl": { @@ -19088,7 +19069,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -19271,7 +19251,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true + "devOptional": true }, "pirates": { "version": "4.0.5", @@ -21284,8 +21264,7 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" }, "zip-stream": { "version": "4.1.0", diff --git a/server/package.json b/server/package.json index e6b2e4b60b045..a0a3c6524a24f 100644 --- a/server/package.json +++ b/server/package.json @@ -59,11 +59,12 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "^19.0.0", + "exiftool-vendored": "^22.0.0", "exiftool-vendored.pl": "^12.54.0", "fluent-ffmpeg": "^2.1.2", "handlebars": "^4.7.7", "i18n-iso-countries": "^7.5.0", + "immich": "^0.39.0", "ioredis": "^5.3.1", "joi": "^17.5.0", "local-reverse-geocoder": "0.12.5", @@ -81,8 +82,7 @@ "thumbhash": "^0.1.1", "typeorm": "^0.3.11", "typesense": "^1.5.3", - "ua-parser-js": "^1.0.35", - "immich": "^0.39.0" + "ua-parser-js": "^1.0.35" }, "devDependencies": { "@nestjs/cli": "^9.1.8", diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index 1df6b47e8b811..d10e307e9c93b 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -32,6 +32,7 @@ export class AssetResponseDto { people?: PersonResponseDto[]; /**base64 encoded sha1 hash */ checksum!: string; + isPanorama!: boolean; } export function mapAsset(entity: AssetEntity): AssetResponseDto { @@ -58,6 +59,7 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto { tags: entity.tags?.map(mapTag), people: entity.faces?.map(mapFace), checksum: entity.checksum.toString('base64'), + isPanorama: entity.isPanorama, }; } @@ -85,5 +87,6 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto { tags: entity.tags?.map(mapTag), people: entity.faces?.map(mapFace), checksum: entity.checksum.toString('base64'), + isPanorama: entity.isPanorama, }; } diff --git a/server/src/immich/api-v1/asset/asset.core.ts b/server/src/immich/api-v1/asset/asset.core.ts index b68f6234cb77e..87c880dc23d6e 100644 --- a/server/src/immich/api-v1/asset/asset.core.ts +++ b/server/src/immich/api-v1/asset/asset.core.ts @@ -29,6 +29,7 @@ export class AssetCore { type: dto.assetType, isFavorite: dto.isFavorite, + isPanorama: false, isArchived: dto.isArchived ?? false, duration: dto.duration || null, isVisible: dto.isVisible ?? true, diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index de236ca5fe25a..21f2062e8192d 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -49,6 +49,7 @@ const _getAsset_1 = () => { asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_1.isFavorite = false; + asset_1.isPanorama = false; asset_1.isArchived = false; asset_1.mimeType = 'image/jpeg'; asset_1.webpPath = ''; @@ -74,6 +75,7 @@ const _getAsset_2 = () => { asset_2.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_2.updatedAt = new Date('2022-06-19T23:41:36.910Z'); asset_2.isFavorite = false; + asset_2.isPanorama = false; asset_2.isArchived = false; asset_2.mimeType = 'image/jpeg'; asset_2.webpPath = ''; diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts index c070b5cd107f2..5152ed9eb5a61 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/infra/entities/asset.entity.ts @@ -85,6 +85,9 @@ export class AssetEntity { @Index() checksum!: Buffer; // sha1 checksum + @Column({ type: 'boolean', default: false }) + isPanorama!: boolean; + @Column({ type: 'varchar', nullable: true }) duration!: string | null; diff --git a/server/src/infra/entities/exif.entity.ts b/server/src/infra/entities/exif.entity.ts index d4abd6e6d75cd..799760a2cbd36 100644 --- a/server/src/infra/entities/exif.entity.ts +++ b/server/src/infra/entities/exif.entity.ts @@ -43,6 +43,9 @@ export class ExifEntity { @Column({ type: 'float', nullable: true }) longitude!: number | null; + @Column({ type: 'varchar', nullable: true }) + projectionType!: string | null; + @Column({ type: 'varchar', nullable: true }) city!: string | null; diff --git a/server/src/infra/migrations/1688380066207-PanoramaViewer.ts b/server/src/infra/migrations/1688380066207-PanoramaViewer.ts new file mode 100644 index 0000000000000..ca9e501de97e5 --- /dev/null +++ b/server/src/infra/migrations/1688380066207-PanoramaViewer.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class PanoramaViewer1688380066207 implements MigrationInterface { + name = '1688379818PanoramaViewer1688380066207'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" ADD "isPanorama" boolean NOT NULL DEFAULT false`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "isPanorama"`); + } +} diff --git a/server/src/infra/migrations/1690120705075-ProjectionType.ts b/server/src/infra/migrations/1690120705075-ProjectionType.ts new file mode 100644 index 0000000000000..3ebad4f7dedc8 --- /dev/null +++ b/server/src/infra/migrations/1690120705075-ProjectionType.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class ProjectionType1690120705075 implements MigrationInterface { + name = 'ProjectionType1690120705075' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "exif" ADD "projectionType" character varying`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "projectionType"`); + } + +} diff --git a/server/src/microservices/processors/metadata-extraction.processor.ts b/server/src/microservices/processors/metadata-extraction.processor.ts index f23ea4833456c..38b38c155cefe 100644 --- a/server/src/microservices/processors/metadata-extraction.processor.ts +++ b/server/src/microservices/processors/metadata-extraction.processor.ts @@ -165,6 +165,8 @@ export class MetadataExtractionProcessor { newExif.longitude = longitude !== null ? parseLongitude(longitude) : null; newExif.livePhotoCID = getExifProperty('MediaGroupUUID'); + newExif.projectionType = getExifProperty('ProjectionType'); + if (newExif.livePhotoCID && !asset.livePhotoVideoId) { const motionAsset = await this.assetRepository.findLivePhotoMatch({ livePhotoCID: newExif.livePhotoCID, @@ -200,8 +202,18 @@ export class MetadataExtractionProcessor { } } + // Determine if the image is a panorama + let isPanorama = false; + if (newExif.exifImageHeight && newExif.exifImageWidth) { + isPanorama = newExif.projectionType == 'equirectangular'; //currently support only for 360 equirectangular panoramas + } + await this.exifRepository.upsert(newExif, { conflictPaths: ['assetId'] }); - await this.assetRepository.save({ id: asset.id, fileCreatedAt: fileCreatedAt || undefined }); + await this.assetRepository.save({ + id: asset.id, + fileCreatedAt: fileCreatedAt || undefined, + isPanorama, + }); return true; } diff --git a/server/test/fixtures.ts b/server/test/fixtures.ts index f1adb8a761baf..ff903fa3fca79 100644 --- a/server/test/fixtures.ts +++ b/server/test/fixtures.ts @@ -221,6 +221,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: true, + isPanorama: false, isArchived: false, duration: null, isVisible: true, @@ -251,6 +252,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: true, + isPanorama: false, isArchived: false, duration: null, isVisible: true, @@ -285,6 +287,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: true, + isPanorama: false, isArchived: false, isReadOnly: false, duration: null, @@ -316,6 +319,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: true, + isPanorama: false, isArchived: false, isReadOnly: false, duration: null, @@ -351,6 +355,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: true, + isPanorama: false, isArchived: false, isReadOnly: false, duration: null, @@ -412,6 +417,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: false, + isPanorama: false, isArchived: false, isReadOnly: false, duration: null, @@ -447,6 +453,7 @@ export const assetEntityStub = { updatedAt: new Date('2023-02-23T05:06:29.716Z'), mimeType: null, isFavorite: true, + isPanorama: false, isArchived: false, isReadOnly: false, duration: null, @@ -618,6 +625,7 @@ const assetResponse: AssetResponseDto = { fileCreatedAt: today, updatedAt: today, isFavorite: false, + isPanorama: false, isArchived: false, mimeType: 'image/jpeg', smartInfo: { @@ -905,6 +913,7 @@ export const sharedLinkStub = { createdAt: today, updatedAt: today, isFavorite: false, + isPanorama: false, isArchived: false, isReadOnly: false, mimeType: 'image/jpeg', @@ -949,6 +958,7 @@ export const sharedLinkStub = { fps: 100, asset: null as any, exifTextSearchableColumn: '', + projectionType: null, }, tags: [], sharedLinks: [], diff --git a/web/package-lock.json b/web/package-lock.json index 6290b2df8affd..5785118a7f5a4 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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.0", "axios": "^0.27.2", "buffer": "^6.0.3", @@ -1853,6 +1854,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", @@ -3834,6 +3876,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", @@ -6251,6 +6298,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", @@ -13201,6 +13253,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", @@ -14615,6 +14708,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", @@ -16329,6 +16427,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", diff --git a/web/package.json b/web/package.json index 85c63ce292443..c34f256a46471 100644 --- a/web/package.json +++ b/web/package.json @@ -58,6 +58,7 @@ }, "type": "module", "dependencies": { + "@egjs/svelte-view360": "^4.0.0-beta.7", "@zoom-image/svelte": "^0.1.0", "axios": "^0.27.2", "buffer": "^6.0.3", diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 412b0c383485f..858ce99c68c65 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -727,6 +727,12 @@ export interface AssetResponseDto { * @memberof AssetResponseDto */ 'checksum': string; + /** + * + * @type {boolean} + * @memberof AssetResponseDto + */ + 'isPanorama': boolean; } @@ -1385,6 +1391,12 @@ export interface ImportAssetDto { * @memberof ImportAssetDto */ 'isFavorite': boolean; + /** + * + * @type {boolean} + * @memberof ImportAssetDto + */ + 'isPanorama': boolean; /** * * @type {boolean} @@ -5627,6 +5639,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {string} fileCreatedAt * @param {string} fileModifiedAt * @param {boolean} isFavorite + * @param {boolean} isPanorama * @param {string} [key] * @param {File} [livePhotoData] * @param {File} [sidecarData] @@ -5637,7 +5650,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadFile: async (assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise => { + uploadFile: async (assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, isPanorama: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise => { // verify required parameter 'assetType' is not null or undefined assertParamExists('uploadFile', 'assetType', assetType) // verify required parameter 'assetData' is not null or undefined @@ -5654,6 +5667,8 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration assertParamExists('uploadFile', 'fileModifiedAt', fileModifiedAt) // verify required parameter 'isFavorite' is not null or undefined assertParamExists('uploadFile', 'isFavorite', isFavorite) + // verify required parameter 'isPanorama' is not null or undefined + assertParamExists('uploadFile', 'isPanorama', isPanorama) const localVarPath = `/asset/upload`; // use dummy base URL string because the URL constructor only accepts absolute URLs. const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); @@ -5725,6 +5740,10 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration localVarFormParams.append('isFavorite', isFavorite as any); } + if (isPanorama !== undefined) { + localVarFormParams.append('isPanorama', isPanorama as any); + } + if (isArchived !== undefined) { localVarFormParams.append('isArchived', isArchived as any); } @@ -6026,6 +6045,7 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {string} fileCreatedAt * @param {string} fileModifiedAt * @param {boolean} isFavorite + * @param {boolean} isPanorama * @param {string} [key] * @param {File} [livePhotoData] * @param {File} [sidecarData] @@ -6036,8 +6056,8 @@ export const AssetApiFp = function(configuration?: Configuration) { * @param {*} [options] Override http request option. * @throws {RequiredError} */ - async uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options); + async uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, isPanorama: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, isPanorama, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, } @@ -6292,6 +6312,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {string} fileCreatedAt * @param {string} fileModifiedAt * @param {boolean} isFavorite + * @param {boolean} isPanorama * @param {string} [key] * @param {File} [livePhotoData] * @param {File} [sidecarData] @@ -6302,8 +6323,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath * @param {*} [options] Override http request option. * @throws {RequiredError} */ - uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise { - return localVarFp.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath)); + uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, isPanorama: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise { + return localVarFp.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, isPanorama, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath)); }, }; }; @@ -6783,6 +6804,13 @@ export interface AssetApiUploadFileRequest { */ readonly isFavorite: boolean + /** + * + * @type {boolean} + * @memberof AssetApiUploadFile + */ + readonly isPanorama: boolean + /** * * @type {string} @@ -7107,7 +7135,7 @@ export class AssetApi extends BaseAPI { * @memberof AssetApi */ public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) { - return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.fileExtension, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath)); + return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.fileExtension, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.isPanorama, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath)); } } diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 7de3b2f5373ba..a36423e38d973 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -12,6 +12,7 @@ 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 ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import { assetStore } from '$lib/stores/assets.store'; @@ -293,6 +294,8 @@ on:close={closeViewer} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} /> + {:else if asset.isPanorama} + {:else} {/if} diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.css b/web/src/lib/components/asset-viewer/panorama-viewer.css new file mode 100644 index 0000000000000..75898aa8228f6 --- /dev/null +++ b/web/src/lib/components/asset-viewer/panorama-viewer.css @@ -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; +} diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte new file mode 100644 index 0000000000000..c315a70e451b3 --- /dev/null +++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte @@ -0,0 +1,44 @@ + + +
+ {#await loadAssetData()} + + {:then assetData} + {#if assetData} + + {:else} +

{errorMessage}

+ {/if} + {/await} +
diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index b49f5bacc13d2..1604047ac423c 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -12,6 +12,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(); @@ -128,6 +129,14 @@ {/if} + {#if asset.type === AssetTypeEnum.Image && asset.isPanorama} +
+ + + +
+ {/if} + {#if asset.resized}