1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

[WEB] View large images on web (#189)

* Added selection icon to thumbnail

* Added micro-interaction and video file indication

* Added page to add page

* Added image viewer

* navigate assets

* Added separate component for viewing the video file

* Added FFmpeg modules

* Added correct content-type header for serving image file

* Added loading spinner
This commit is contained in:
Alex 2022-05-27 14:02:06 -05:00 committed by GitHub
parent 337db1c508
commit c28251b8b4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 527 additions and 225 deletions

View file

@ -58,5 +58,6 @@ MAPBOX_KEY=
# know where can it make the request to.
# For example: If your server IP address is 10.1.11.50, the environment variable will
# be VITE_SERVER_ENDPOINT=http://10.1.11.50:2283
# !CAUTION! THERE IS NO FORWARD SLASH AT THE END
VITE_SERVER_ENDPOINT=

View file

@ -73,7 +73,7 @@ class BackupService {
});
// Build thumbnail multipart data
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(720, 1280));
var thumbnailData = await entity.thumbnailDataWithSize(const ThumbnailSize(1440, 2560));
if (thumbnailData != null) {
thumbnailUploadData = MultipartFile.fromBytes(
List.from(thumbnailData),

View file

@ -6,7 +6,7 @@ WORKDIR /usr/src/app
COPY package.json package-lock.json ./
RUN apk add --update-cache build-base python3 libheif vips-dev vips
RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
RUN npm install

317
server/package-lock.json generated
View file

@ -32,6 +32,7 @@
"diskusage": "^1.1.3",
"dotenv": "^14.2.0",
"exifr": "^7.1.3",
"fluent-ffmpeg": "^2.1.2",
"joi": "^17.5.0",
"lodash": "^4.17.21",
"passport": "^0.5.2",
@ -41,7 +42,7 @@
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"sharp": "^0.30.4",
"sharp": "^0.28.0",
"socket.io-redis": "^6.1.1",
"systeminformation": "^5.11.0",
"typeorm": "^0.2.41"
@ -3141,6 +3142,11 @@
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true
},
"node_modules/async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -3860,15 +3866,12 @@
"dev": true
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
}
},
"node_modules/color-convert": {
@ -3896,6 +3899,19 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dependencies": {
"color-name": "1.1.3"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/colors": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
@ -4231,28 +4247,14 @@
"dev": true
},
"node_modules/decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"dependencies": {
"mimic-response": "^3.1.0"
"mimic-response": "^2.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
"node": ">=8"
}
},
"node_modules/dedent": {
@ -5418,6 +5420,18 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true
},
"node_modules/fluent-ffmpeg": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz",
"integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=",
"dependencies": {
"async": ">=0.2.9",
"which": "^1.1.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
@ -6329,8 +6343,7 @@
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"node_modules/istanbul-lib-coverage": {
"version": "3.2.0",
@ -7959,14 +7972,19 @@
"dev": true
},
"node_modules/node-abi": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz",
"integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==",
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
"integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==",
"dependencies": {
"semver": "^7.3.5"
},
"engines": {
"node": ">=10"
"semver": "^5.4.1"
}
},
"node_modules/node-abi/node_modules/semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
"bin": {
"semver": "bin/semver"
}
},
"node_modules/node-addon-api": {
@ -8668,21 +8686,21 @@
}
},
"node_modules/prebuild-install": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz",
"integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==",
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz",
"integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==",
"dependencies": {
"detect-libc": "^2.0.0",
"detect-libc": "^1.0.3",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"node-abi": "^2.21.0",
"npmlog": "^4.0.1",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"simple-get": "^3.0.3",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
@ -8690,15 +8708,7 @@
"prebuild-install": "bin.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/prebuild-install/node_modules/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"engines": {
"node": ">=8"
"node": ">=6"
}
},
"node_modules/prelude-ls": {
@ -9460,40 +9470,27 @@
}
},
"node_modules/sharp": {
"version": "0.30.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.4.tgz",
"integrity": "sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==",
"version": "0.28.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.28.3.tgz",
"integrity": "sha512-21GEP45Rmr7q2qcmdnjDkNP04Ooh5v0laGS5FDpojOO84D1DJwUijLiSq8XNNM6e8aGXYtoYRh3sVNdm8NodMA==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.1",
"node-addon-api": "^4.3.0",
"prebuild-install": "^7.0.1",
"semver": "^7.3.7",
"simple-get": "^4.0.1",
"color": "^3.1.3",
"detect-libc": "^1.0.3",
"node-addon-api": "^3.2.0",
"prebuild-install": "^6.1.2",
"semver": "^7.3.5",
"simple-get": "^3.1.0",
"tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0"
},
"engines": {
"node": ">=12.13.0"
"node": ">=10"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/sharp/node_modules/detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==",
"engines": {
"node": ">=8"
}
},
"node_modules/sharp/node_modules/node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -9571,25 +9568,11 @@
]
},
"node_modules/simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
"dependencies": {
"decompress-response": "^6.0.0",
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
@ -11026,6 +11009,17 @@
"node": ">=10"
}
},
"node_modules/which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"which": "bin/which"
}
},
"node_modules/wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",
@ -13602,6 +13596,11 @@
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
"dev": true
},
"async": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz",
"integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@ -14145,12 +14144,27 @@
"dev": true
},
"color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz",
"integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==",
"requires": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
"color-convert": "^1.9.3",
"color-string": "^1.6.0"
},
"dependencies": {
"color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
"color-name": "1.1.3"
}
},
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
}
}
},
"color-convert": {
@ -14460,18 +14474,11 @@
"dev": true
},
"decompress-response": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz",
"integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==",
"requires": {
"mimic-response": "^3.1.0"
},
"dependencies": {
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="
}
"mimic-response": "^2.0.0"
}
},
"dedent": {
@ -15394,6 +15401,15 @@
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==",
"dev": true
},
"fluent-ffmpeg": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.2.tgz",
"integrity": "sha1-yVLeIkD4EuvaCqgAbXd27irPfXQ=",
"requires": {
"async": ">=0.2.9",
"which": "^1.1.1"
}
},
"follow-redirects": {
"version": "1.14.8",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz",
@ -16043,8 +16059,7 @@
"isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=",
"dev": true
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
},
"istanbul-lib-coverage": {
"version": "3.2.0",
@ -17335,11 +17350,18 @@
"dev": true
},
"node-abi": {
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.15.0.tgz",
"integrity": "sha512-Ic6z/j6I9RLm4ov7npo1I48UQr2BEyFCqh6p7S1dhEx9jPO0GPGq/e2Rb7x7DroQrmiVMz/Bw1vJm9sPAl2nxA==",
"version": "2.30.1",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.30.1.tgz",
"integrity": "sha512-/2D0wOQPgaUWzVSVgRMx+trKJRC2UG4SUc4oCJoXx9Uxjtp0Vy3/kt7zcbxHF8+Z/pK3UloLWzBISg72brfy1w==",
"requires": {
"semver": "^7.3.5"
"semver": "^5.4.1"
},
"dependencies": {
"semver": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
}
}
},
"node-addon-api": {
@ -17856,30 +17878,23 @@
}
},
"prebuild-install": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.0.tgz",
"integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==",
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-6.1.4.tgz",
"integrity": "sha512-Z4vpywnK1lBg+zdPCVCsKq0xO66eEV9rWo2zrROGGiRS4JtueBOdlB1FnY8lcy7JsUud/Q3ijUxyWN26Ika0vQ==",
"requires": {
"detect-libc": "^2.0.0",
"detect-libc": "^1.0.3",
"expand-template": "^2.0.3",
"github-from-package": "0.0.0",
"minimist": "^1.2.3",
"mkdirp-classic": "^0.5.3",
"napi-build-utils": "^1.0.1",
"node-abi": "^3.3.0",
"node-abi": "^2.21.0",
"npmlog": "^4.0.1",
"pump": "^3.0.0",
"rc": "^1.2.7",
"simple-get": "^4.0.0",
"simple-get": "^3.0.3",
"tar-fs": "^2.0.0",
"tunnel-agent": "^0.6.0"
},
"dependencies": {
"detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w=="
}
}
},
"prelude-ls": {
@ -18437,30 +18452,18 @@
}
},
"sharp": {
"version": "0.30.4",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.4.tgz",
"integrity": "sha512-3Onig53Y6lji4NIZo69s14mERXXY/GV++6CzOYx/Rd8bnTwbhFbL09WZd7Ag/CCnA0WxFID8tkY0QReyfL6v0Q==",
"version": "0.28.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.28.3.tgz",
"integrity": "sha512-21GEP45Rmr7q2qcmdnjDkNP04Ooh5v0laGS5FDpojOO84D1DJwUijLiSq8XNNM6e8aGXYtoYRh3sVNdm8NodMA==",
"requires": {
"color": "^4.2.3",
"detect-libc": "^2.0.1",
"node-addon-api": "^4.3.0",
"prebuild-install": "^7.0.1",
"semver": "^7.3.7",
"simple-get": "^4.0.1",
"color": "^3.1.3",
"detect-libc": "^1.0.3",
"node-addon-api": "^3.2.0",
"prebuild-install": "^6.1.2",
"semver": "^7.3.5",
"simple-get": "^3.1.0",
"tar-fs": "^2.1.1",
"tunnel-agent": "^0.6.0"
},
"dependencies": {
"detect-libc": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz",
"integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w=="
},
"node-addon-api": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz",
"integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="
}
}
},
"shebang-command": {
@ -18511,11 +18514,11 @@
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="
},
"simple-get": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz",
"integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==",
"requires": {
"decompress-response": "^6.0.0",
"decompress-response": "^4.2.0",
"once": "^1.3.1",
"simple-concat": "^1.0.0"
}
@ -19516,6 +19519,14 @@
"webidl-conversions": "^6.1.0"
}
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
"requires": {
"isexe": "^2.0.0"
}
},
"wide-align": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz",

View file

@ -45,6 +45,7 @@
"diskusage": "^1.1.3",
"dotenv": "^14.2.0",
"exifr": "^7.1.3",
"fluent-ffmpeg": "^2.1.2",
"joi": "^17.5.0",
"lodash": "^4.17.21",
"passport": "^0.5.2",

View file

@ -39,7 +39,7 @@ export class AssetController {
private wsCommunicateionGateway: CommunicationGateway,
private assetService: AssetService,
private backgroundTaskService: BackgroundTaskService,
) {}
) { }
@Post('upload')
@UseInterceptors(

View file

@ -1,19 +1,16 @@
import { BadRequestException, Injectable, Logger, StreamableFile } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { MoreThan, Repository } from 'typeorm';
import { Repository } from 'typeorm';
import { AuthUserDto } from '../../decorators/auth-user.decorator';
import { CreateAssetDto } from './dto/create-asset.dto';
import { AssetEntity, AssetType } from './entities/asset.entity';
import _ from 'lodash';
import { GetAllAssetQueryDto } from './dto/get-all-asset-query.dto';
import { GetAllAssetReponseDto } from './dto/get-all-asset-response.dto';
import { createReadStream, stat } from 'fs';
import { ServeFileDto } from './dto/serve-file.dto';
import { Response as Res } from 'express';
import { promisify } from 'util';
import { DeleteAssetDto } from './dto/delete-asset.dto';
import { SearchAssetDto } from './dto/search-asset.dto';
import path from 'path';
const fileInfo = promisify(stat);
@ -124,21 +121,43 @@ export class AssetService {
public async serveFile(authUser: AuthUserDto, query: ServeFileDto, res: Res, headers: any) {
let file = null;
const asset = await this.findOne(query.did, query.aid);
if (!asset) {
throw new BadRequestException('Asset does not exist');
}
// Handle Sending Images
if (asset.type == AssetType.IMAGE || query.isThumb == 'true') {
res.set({
'Content-Type': asset.mimeType,
});
/**
* Serve file viewer on the web
*/
if (query.isWeb) {
res.set({
'Content-Type': 'image/jpeg',
});
return new StreamableFile(createReadStream(asset.resizePath));
}
/**
* Serve thumbnail image for both web and mobile app
*/
if (query.isThumb === 'false' || !query.isThumb) {
res.set({
'Content-Type': asset.mimeType,
});
file = createReadStream(asset.originalPath);
} else {
if (asset.webpPath != '') {
res.set({
'Content-Type': 'image/webp',
});
file = createReadStream(asset.webpPath);
} else {
res.set({
'Content-Type': 'image/jpeg',
});
file = createReadStream(asset.resizePath);
}
}
@ -147,9 +166,11 @@ export class AssetService {
Logger.log(`Cannot create read stream ${error}`);
return new BadRequestException('Cannot Create Read Stream');
});
return new StreamableFile(file);
} else if (asset.type == AssetType.VIDEO) {
// Handle Handling Video
// Handle Video
const { size } = await fileInfo(asset.originalPath);
const range = headers.range;
@ -191,6 +212,8 @@ export class AssetService {
const videoStream = createReadStream(asset.originalPath, { start: start, end: end });
return new StreamableFile(videoStream);
} else {
res.set({
'Content-Type': asset.mimeType,

View file

@ -13,4 +13,8 @@ export class ServeFileDto {
@IsOptional()
@IsBooleanString()
isThumb: string;
@IsOptional()
@IsBooleanString()
isWeb: string;
}

View file

@ -11,29 +11,35 @@ export const handle: Handle = async ({ event, resolve, }) => {
return await resolve(event)
}
const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session);
try {
const { email, isAdmin, firstName, lastName, id, accessToken } = JSON.parse(cookies.session);
const res = await fetch(`${serverEndpoint}/auth/validateToken`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
const res = await fetch(`${serverEndpoint}/auth/validateToken`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
if (res.status === 201) {
event.locals.user = {
id,
accessToken,
firstName,
lastName,
isAdmin,
email
};
}
})
if (res.status === 201) {
event.locals.user = {
id,
accessToken,
firstName,
lastName,
isAdmin,
email
};
const response = await resolve(event);
return response;
} catch (error) {
console.log('Error parsing session', error);
return await resolve(event);
}
const response = await resolve(event);
return response;
};
export const getSession: GetSession = async ({ locals }) => {

View file

@ -1,13 +1,25 @@
<script lang="ts">
import type { ImmichAsset } from '../../models/immich-asset';
import { AssetType, type ImmichAsset } from '../../models/immich-asset';
import { session } from '$app/stores';
import { onDestroy } from 'svelte';
import { createEventDispatcher, onDestroy } from 'svelte';
import { fade } from 'svelte/transition';
import { serverEndpoint } from '../../constants';
import IntersectionObserver from '$lib/components/photos/intersection-observer.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import PlayCircleOutline from 'svelte-material-icons/PlayCircleOutline.svelte';
const dispatch = createEventDispatcher();
export let asset: ImmichAsset;
export let groupIndex: number;
let imageContent: string;
let mouseOver: boolean = false;
$: dispatch('mouseEvent', { isMouseOver: mouseOver, selectedGroupIndex: groupIndex });
let mouseOverIcon: boolean = false;
let videoPlayerNode: HTMLVideoElement;
const loadImageData = async () => {
if ($session.user) {
@ -24,11 +36,72 @@
}
};
const loadVideoData = async () => {
const videoUrl = `/asset/file?aid=${asset.deviceAssetId}&did=${asset.deviceId}`;
if ($session.user) {
const res = await fetch(serverEndpoint + videoUrl, {
method: 'GET',
headers: {
Authorization: 'bearer ' + $session.user.accessToken,
},
});
const videoData = URL.createObjectURL(await res.blob());
videoPlayerNode.src = videoData;
videoPlayerNode.load();
videoPlayerNode.oncanplay = () => {
console.log('Can play video');
};
return videoData;
}
};
const parseVideoDuration = (duration: string) => {
const timePart = duration.split(':');
const hours = timePart[0];
const minutes = timePart[1];
const seconds = timePart[2];
if (hours != '0') {
return `${hours}:${minutes}`;
} else {
return `${minutes}:${seconds.split('.')[0]}`;
}
};
onDestroy(() => URL.revokeObjectURL(imageContent));
</script>
<IntersectionObserver once={true} let:intersecting>
<div class="h-[200px] w-[200px] bg-gray-100">
<div
class="h-[200px] w-[200px] bg-gray-100 relative hover:cursor-pointer"
on:mouseenter={() => (mouseOver = true)}
on:mouseleave={() => (mouseOver = false)}
on:click={() => dispatch('viewAsset', { assetId: asset.id, deviceId: asset.deviceId })}
>
{#if mouseOver}
<div
in:fade={{ duration: 200 }}
class="w-full h-full bg-gradient-to-b from-gray-800/50 via-white/0 to-white/0 absolute p-2"
>
<div
on:mouseenter={() => (mouseOverIcon = true)}
on:mouseleave={() => (mouseOverIcon = false)}
class="inline-block"
>
<CheckCircle size="24" color={mouseOverIcon ? 'white' : '#d8dadb'} />
</div>
</div>
{/if}
{#if asset.type === AssetType.VIDEO}
<div class="absolute right-2 top-2 text-white text-xs font-medium flex gap-1 place-items-center">
{parseVideoDuration(asset.duration)}
<PlayCircleOutline size="24" />
</div>
{/if}
{#if intersecting}
{#await loadImageData()}
<div class="bg-immich-primary/10 h-[200px] w-[200px] flex place-items-center place-content-center">...</div>
@ -37,10 +110,18 @@
in:fade={{ duration: 200 }}
src={imageData}
alt={asset.id}
class="object-cover h-[200px] w-[200px] transition-all duration-100"
class="object-cover h-[200px] w-[200px] transition-all duration-100 z-0"
loading="lazy"
/>
{/await}
{/if}
<!-- {#if mouseOver && asset.type === AssetType.VIDEO}
<div class="absolute w-full h-full top-0" on:mouseenter={loadVideoData}>
<video autoplay class="border-2 h-[200px]" width="250px" bind:this={videoPlayerNode}>
<track kind="captions" />
</video>
</div>
{/if} -->
</div>
</IntersectionObserver>

View file

@ -0,0 +1,62 @@
<script lang="ts">
import { session } from '$app/stores';
import { serverEndpoint } from '$lib/constants';
import { fade } from 'svelte/transition';
import type { ImmichAsset } from '$lib/models/immich-asset';
import { createEventDispatcher, onMount } from 'svelte';
import LoadingSpinner from '../shared/loading-spinner.svelte';
export let assetId: string;
export let deviceId: string;
let assetInfo: ImmichAsset;
const dispatch = createEventDispatcher();
onMount(async () => {
if ($session.user) {
const res = await fetch(serverEndpoint + '/asset/assetById/' + assetId, {
headers: {
Authorization: 'bearer ' + $session.user.accessToken,
},
});
assetInfo = await res.json();
}
});
const loadAssetData = async () => {
const assetUrl = `/asset/file?aid=${assetInfo.deviceAssetId}&did=${deviceId}&isWeb=true`;
if ($session.user) {
const res = await fetch(serverEndpoint + assetUrl, {
method: 'GET',
headers: {
Authorization: 'bearer ' + $session.user.accessToken,
},
});
const assetData = URL.createObjectURL(await res.blob());
return assetData;
}
};
</script>
<div on:click={() => dispatch('close')} class="h-screen">
{#if assetInfo}
{#await loadAssetData()}
<div class="flex place-items-center place-content-center h-full">
<LoadingSpinner />
</div>
{:then assetData}
<div class="flex place-items-center place-content-center h-full">
<img
in:fade={{ duration: 200 }}
src={assetData}
alt={assetId}
class="object-cover h-full transition-all duration-100 z-0"
loading="lazy"
/>
</div>
{/await}
{/if}
</div>

View file

@ -0,0 +1,18 @@
<div>
<svg
role="status"
class="w-8 h-8 mr-2 text-gray-400 animate-spin dark:text-gray-600 fill-immich-primary"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>

View file

@ -4,9 +4,9 @@ import type { ImmichAsset } from '$lib/models/immich-asset'
import lodash from 'lodash-es';
import moment from 'moment';
const assets = writable<ImmichAsset[]>([]);
export const assets = writable<ImmichAsset[]>([]);
const assetsGroupByDate = derived(assets, ($assets) => {
export const assetsGroupByDate = derived(assets, ($assets) => {
try {
return lodash.chain($assets)
@ -20,14 +20,14 @@ const assetsGroupByDate = derived(assets, ($assets) => {
})
const getAssetsInfo = async (accessToken: string) => {
export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupByDate) => {
return $assetsGroupByDate.flat();
})
export const getAssetsInfo = async (accessToken: string) => {
const res = await getRequest('asset', accessToken);
assets.set(res);
}
export default {
assets,
assetsGroupByDate,
getAssetsInfo,
}

View file

@ -11,7 +11,7 @@
const response = await getRequest('server-info/ping', '');
if (response.res === 'pong') isServerOk = true;
if (response.statusCode === 404) isServerOk = false;
else isServerOk = false;
}, 10000);
onDestroy(() => clearInterval(pingServerInterval));

View file

@ -2,8 +2,9 @@
export const prerender = false;
import type { Load } from '@sveltejs/kit';
import { getAssetsInfo } from '$lib/stores/assets';
export const load: Load = ({ session }) => {
export const load: Load = async ({ session }) => {
if (!session.user) {
return {
status: 302,
@ -25,24 +26,36 @@
import NavigationBar from '../../lib/components/shared/navigation-bar.svelte';
import SideBarButton from '$lib/components/shared/side-bar-button.svelte';
import Magnify from 'svelte-material-icons/Magnify.svelte';
import CheckCircle from 'svelte-material-icons/CheckCircle.svelte';
import ChevronRight from 'svelte-material-icons/ChevronRight.svelte';
import ChevronLeft from 'svelte-material-icons/ChevronLeft.svelte';
import ImageOutline from 'svelte-material-icons/ImageOutline.svelte';
import { AppSideBarSelection } from '$lib/models/admin-sidebar-selection';
import { onDestroy, onMount } from 'svelte';
import { onMount } from 'svelte';
import { fade, fly } from 'svelte/transition';
import { session } from '$app/stores';
import assetStore from '$lib/stores/assets';
import type { ImmichAsset } from '../../lib/models/immich-asset';
import { assetsGroupByDate, flattenAssetGroupByDate } from '$lib/stores/assets';
import ImmichThumbnail from '../../lib/components/photos/immich-thumbnail.svelte';
import moment from 'moment';
import PhotoViewer from '../../lib/components/photos/photo_viewer.svelte';
import type { ImmichAsset } from '../../lib/models/immich-asset';
import { AssetType } from '../../lib/models/immich-asset';
import LoadingSpinner from '../../lib/components/shared/loading-spinner.svelte';
export let user: ImmichUser;
let selectedAction: AppSideBarSelection;
let assets: ImmichAsset[] = [];
let assetsGroupByDate: ImmichAsset[][];
// Subscribe to store values
const assetsSub = assetStore.assets.subscribe((newAssets) => (assets = newAssets));
const assetsGroupByDateSub = assetStore.assetsGroupByDate.subscribe((value) => (assetsGroupByDate = value));
let selectedGroupThumbnail: number | null;
let isMouseOverGroup: boolean;
$: if (isMouseOverGroup == false) {
selectedGroupThumbnail = null;
}
let isShowAsset = false;
let viewDeviceId: string = '';
let viewAssetId: string = '';
let currentViewAssetIndex = 0;
let currentSelectedAsset: ImmichAsset;
const onButtonClicked = (buttonType: CustomEvent) => {
selectedAction = buttonType.detail['actionType'] as AppSideBarSelection;
@ -50,15 +63,46 @@
onMount(async () => {
selectedAction = AppSideBarSelection.PHOTOS;
if ($session.user) {
await assetStore.getAssetsInfo($session.user.accessToken);
await getAssetsInfo($session.user.accessToken);
}
});
onDestroy(() => {
assetsSub();
assetsGroupByDateSub();
});
const thumbnailMouseEventHandler = (event: CustomEvent) => {
const { selectedGroupIndex }: { selectedGroupIndex: number } = event.detail;
selectedGroupThumbnail = selectedGroupIndex;
};
const viewAssetHandler = (event: CustomEvent) => {
const { assetId, deviceId }: { assetId: string; deviceId: string } = event.detail;
viewDeviceId = deviceId;
viewAssetId = assetId;
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
isShowAsset = true;
};
const navigateAssetForward = () => {
const nextAsset = $flattenAssetGroupByDate[currentViewAssetIndex + 1];
viewDeviceId = nextAsset.deviceId;
viewAssetId = nextAsset.id;
currentViewAssetIndex = currentViewAssetIndex + 1;
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
};
const navigateAssetBackward = () => {
const lastAsset = $flattenAssetGroupByDate[currentViewAssetIndex - 1];
viewDeviceId = lastAsset.deviceId;
viewAssetId = lastAsset.id;
currentViewAssetIndex = currentViewAssetIndex - 1;
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
};
</script>
<svelte:head>
@ -70,6 +114,7 @@
</section>
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
<!-- Sidebar -->
<section id="admin-sidebar" class="flex flex-col gap-4 pt-8 pr-6">
<SideBarButton
title="Photos"
@ -78,27 +123,43 @@
isSelected={selectedAction === AppSideBarSelection.PHOTOS}
on:selected={onButtonClicked}
/>
<!-- <SideBarButton
title="Explore"
logo={Magnify}
actionType={AppSideBarSelection.EXPLORE}
isSelected={selectedAction === AppSideBarSelection.EXPLORE}
on:selected={onButtonClicked}
/> -->
</section>
<!-- Main Section -->
<section class="overflow-y-auto relative">
<section id="assets-content" class="relative pt-8">
<section id="image-grid" class="flex flex-wrap gap-8">
{#each assetsGroupByDate as assetsInDateGroup}
<div class="flex flex-col">
<p class="font-medium text-sm text-gray-500 mb-2">
<section id="assets-content" class="relative pt-8 pl-4">
<section id="image-grid" class="flex flex-wrap gap-14">
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
<!-- Asset Group By Date -->
<div
class="flex flex-col"
on:mouseenter={() => (isMouseOverGroup = true)}
on:mouseleave={() => (isMouseOverGroup = false)}
>
<!-- Date group title -->
<p class="font-medium text-sm text-immich-primary mb-2 flex place-items-center h-6">
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
<div
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
out:fly={{ x: -24, duration: 200 }}
class="inline-block px-2 hover:cursor-pointer"
>
<CheckCircle size="24" color="#757575" />
</div>
{/if}
{moment(assetsInDateGroup[0].createdAt).format('ddd, MMM DD YYYY')}
</p>
<div class=" flex flex-wrap gap-2">
<!-- Image grid -->
<div class="flex flex-wrap gap-2">
{#each assetsInDateGroup as asset}
<ImmichThumbnail {asset} />
<ImmichThumbnail
{asset}
on:mouseEvent={thumbnailMouseEventHandler}
on:viewAsset={viewAssetHandler}
{groupIndex}
/>
{/each}
</div>
</div>
@ -107,3 +168,37 @@
</section>
</section>
</section>
<!-- Overlay Asset Viewer -->
{#if isShowAsset}
<section
class="absolute w-screen h-screen top-0 overflow-y-hidden bg-black z-[9999] flex justify-between place-items-center"
>
<button
class="rounded-full p-4 hover:bg-gray-500 hover:text-gray-700 text-gray-500 mx-4"
on:click={navigateAssetBackward}
>
<ChevronLeft size="48" />
</button>
{#key currentViewAssetIndex}
{#if currentSelectedAsset.type == AssetType.IMAGE}
<PhotoViewer assetId={viewAssetId} deviceId={viewDeviceId} on:close={() => (isShowAsset = false)} />
{:else}
<div
class="w-full h-full bg-immich-primary/10 flex flex-col place-items-center place-content-center "
on:click={() => (isShowAsset = false)}
>
<h1 class="animate-pulse font-bold text-4xl">Video viewer is under construction</h1>
</div>
{/if}
{/key}
<button
class="rounded-full p-4 hover:bg-gray-500 hover:text-gray-700 bg-black text-gray-500 mx-4"
on:click={navigateAssetForward}
>
<ChevronRight size="48" />
</button>
</section>
{/if}