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:
parent
337db1c508
commit
c28251b8b4
15 changed files with 527 additions and 225 deletions
|
@ -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=
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
317
server/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -39,7 +39,7 @@ export class AssetController {
|
|||
private wsCommunicateionGateway: CommunicationGateway,
|
||||
private assetService: AssetService,
|
||||
private backgroundTaskService: BackgroundTaskService,
|
||||
) {}
|
||||
) { }
|
||||
|
||||
@Post('upload')
|
||||
@UseInterceptors(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -13,4 +13,8 @@ export class ServeFileDto {
|
|||
@IsOptional()
|
||||
@IsBooleanString()
|
||||
isThumb: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBooleanString()
|
||||
isWeb: string;
|
||||
}
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
62
web/src/lib/components/photos/photo_viewer.svelte
Normal file
62
web/src/lib/components/photos/photo_viewer.svelte
Normal 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>
|
18
web/src/lib/components/shared/loading-spinner.svelte
Normal file
18
web/src/lib/components/shared/loading-spinner.svelte
Normal 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>
|
|
@ -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,
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue