Feature - Add upload functionality on Web (#231)

* Added file selector

* Extract metadata to upload files to the web

* Added request for uploading

* Generate jpeg/Webp thumbnail for asset uploaded without thumbnail data

* Added generating thumbnail for video and WebSocket broadcast after thumbnail is generated

* Added video length extraction

* Added Uploading Panel

* Added upload progress store and styling the uploaded asset

* Added condition to only show upload panel when there is upload in progress

* Remove asset from the upload list after successfully uploading

* Added WebSocket to listen to upload event on the web

* Added mechanism to check for existing assets before uploading on the web

* Added test workflow

* Update readme
This commit is contained in:
Alex
2022-06-19 08:16:35 -05:00
committed by GitHub
parent b7603fd150
commit 1e3464fe47
33 changed files with 859 additions and 220 deletions
+166 -129
View File
@@ -10,11 +10,12 @@
"dependencies": {
"axios": "^0.27.2",
"cookie": "^0.4.2",
"exifr": "^7.1.3",
"leaflet": "^1.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"markdown-it": "^13.0.1",
"moment": "^2.29.3",
"socket.io-client": "^4.5.1",
"svelte-material-icons": "^2.0.2"
},
"devDependencies": {
@@ -28,7 +29,7 @@
"@types/leaflet": "^1.7.10",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@types/markdown-it": "^12.2.3",
"@types/socket.io-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"autoprefixer": "^10.4.7",
@@ -140,6 +141,11 @@
"node": ">= 8.0.0"
}
},
"node_modules/@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"node_modules/@sveltejs/adapter-auto": {
"version": "1.0.0-next.40",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.40.tgz",
@@ -293,12 +299,6 @@
"@types/geojson": "*"
}
},
"node_modules/@types/linkify-it": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
"dev": true
},
"node_modules/@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
@@ -314,22 +314,6 @@
"@types/lodash": "*"
}
},
"node_modules/@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"dependencies": {
"@types/linkify-it": "*",
"@types/mdurl": "*"
}
},
"node_modules/@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
"dev": true
},
"node_modules/@types/node": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
@@ -351,6 +335,16 @@
"@types/node": "*"
}
},
"node_modules/@types/socket.io-client": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz",
"integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==",
"deprecated": "This is a stub types definition. socket.io-client provides its own type definitions, so you do not need this installed.",
"dev": true,
"dependencies": {
"socket.io-client": "*"
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "5.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz",
@@ -650,7 +644,8 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/array-union": {
"version": "2.1.0",
@@ -933,7 +928,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"dependencies": {
"ms": "2.1.2"
},
@@ -1043,15 +1037,24 @@
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==",
"dev": true
},
"node_modules/entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==",
"node_modules/engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"node_modules/engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg==",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
"node": ">=10.0.0"
}
},
"node_modules/es6-promise": {
@@ -1673,6 +1676,11 @@
"node": ">=0.10.0"
}
},
"node_modules/exifr": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2112,14 +2120,6 @@
"node": ">=10"
}
},
"node_modules/linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"dependencies": {
"uc.micro": "^1.0.1"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -2160,26 +2160,6 @@
"node": ">=12"
}
},
"node_modules/markdown-it": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
"dependencies": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
},
"bin": {
"markdown-it": "bin/markdown-it.js"
}
},
"node_modules/mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -2289,8 +2269,7 @@
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/nanoid": {
"version": "3.3.4",
@@ -2820,6 +2799,32 @@
"node": ">=8"
}
},
"node_modules/socket.io-client": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz",
"integrity": "sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.2.1",
"socket.io-parser": "~4.2.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/socket.io-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz",
"integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/sorcery": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
@@ -3187,11 +3192,6 @@
"node": ">=4.2.0"
}
},
"node_modules/uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -3293,6 +3293,34 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"node_modules/ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
@@ -3395,6 +3423,11 @@
"picomatch": "^2.2.2"
}
},
"@socket.io/component-emitter": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
},
"@sveltejs/adapter-auto": {
"version": "1.0.0-next.40",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.40.tgz",
@@ -3525,12 +3558,6 @@
"@types/geojson": "*"
}
},
"@types/linkify-it": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz",
"integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==",
"dev": true
},
"@types/lodash": {
"version": "4.14.182",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.182.tgz",
@@ -3546,22 +3573,6 @@
"@types/lodash": "*"
}
},
"@types/markdown-it": {
"version": "12.2.3",
"resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz",
"integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==",
"dev": true,
"requires": {
"@types/linkify-it": "*",
"@types/mdurl": "*"
}
},
"@types/mdurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz",
"integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==",
"dev": true
},
"@types/node": {
"version": "17.0.32",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.32.tgz",
@@ -3583,6 +3594,15 @@
"@types/node": "*"
}
},
"@types/socket.io-client": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/socket.io-client/-/socket.io-client-3.0.0.tgz",
"integrity": "sha512-s+IPvFoEIjKA3RdJz/Z2dGR4gLgysKi8owcnrVwNjgvc01Lk68LJDDsG2GRqegFITcxmvCMYM7bhMpwEMlHmDg==",
"dev": true,
"requires": {
"socket.io-client": "*"
}
},
"@typescript-eslint/eslint-plugin": {
"version": "5.23.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.23.0.tgz",
@@ -3762,7 +3782,8 @@
"argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"array-union": {
"version": "2.1.0",
@@ -3947,7 +3968,6 @@
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dev": true,
"requires": {
"ms": "2.1.2"
}
@@ -4028,10 +4048,22 @@
"integrity": "sha512-0Rcpald12O11BUogJagX3HsCN3FE83DSqWjgXoHo5a72KUKMSfI39XBgJpgNNxS9fuGzytaFjE06kZkiVFy2qA==",
"dev": true
},
"entities": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz",
"integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q=="
"engine.io-client": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.2.2.tgz",
"integrity": "sha512-8ZQmx0LQGRTYkHuogVZuGSpDqYZtCM/nv8zQ68VZ+JkOpazJ7ICdsSpaO6iXwvaU30oFg5QJOJWj8zWqhbKjkQ==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.0.3",
"ws": "~8.2.3",
"xmlhttprequest-ssl": "~2.0.0"
}
},
"engine.io-parser": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.4.tgz",
"integrity": "sha512-+nVFp+5z1E3HcToEnO7ZIj3g+3k9389DvWtvJZz0T6/eOCPIyyxehFcedoYrZQrp0LgQbD9pPXhpMBKMd5QURg=="
},
"es6-promise": {
"version": "3.3.1",
@@ -4399,6 +4431,11 @@
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
},
"exifr": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/exifr/-/exifr-7.1.3.tgz",
"integrity": "sha512-g/aje2noHivrRSLbAUtBPWFbxKdKhgj/xr1vATDdUXPOFYJlQ62Ft0oy+72V6XLIpDJfHs6gXLbBLAolqOXYRw=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -4729,14 +4766,6 @@
"integrity": "sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg==",
"dev": true
},
"linkify-it": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz",
"integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==",
"requires": {
"uc.micro": "^1.0.1"
}
},
"lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -4771,23 +4800,6 @@
"sourcemap-codec": "^1.4.8"
}
},
"markdown-it": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.1.tgz",
"integrity": "sha512-lTlxriVoy2criHP0JKRhO2VDG9c2ypWCsT237eDiLqi09rmbKoUetyGHq2uOIRoRS//kfoJckS0eUzzkDR+k2Q==",
"requires": {
"argparse": "^2.0.1",
"entities": "~3.0.1",
"linkify-it": "^4.0.1",
"mdurl": "^1.0.1",
"uc.micro": "^1.0.5"
}
},
"mdurl": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
},
"merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4867,8 +4879,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"nanoid": {
"version": "3.3.4",
@@ -5199,6 +5210,26 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"socket.io-client": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.5.1.tgz",
"integrity": "sha512-e6nLVgiRYatS+AHXnOnGi4ocOpubvOUCGhyWw8v+/FxW8saHkinG6Dfhi9TU0Kt/8mwJIAASxvw6eujQmjdZVA==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.2.1",
"socket.io-parser": "~4.2.0"
}
},
"socket.io-parser": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.0.tgz",
"integrity": "sha512-tLfmEwcEwnlQTxFB7jibL/q2+q8dlVQzj4JdRLJ/W/G1+Fu9VSxCx1Lo+n1HvXxKnM//dUuD0xgiA7tQf57Vng==",
"requires": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1"
}
},
"sorcery": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.10.0.tgz",
@@ -5436,11 +5467,6 @@
"integrity": "sha512-9ia/jWHIEbo49HfjrLGfKbZSuWo9iTMwXO+Ca3pRsSpbsMbc7/IU8NKdCZVRRBafVPGnoJeFL76ZOAA84I9fEg==",
"dev": true
},
"uc.micro": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
},
"uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -5506,6 +5532,17 @@
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"dev": true
},
"ws": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
"requires": {}
},
"xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
},
"xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+3
View File
@@ -23,6 +23,7 @@
"@types/leaflet": "^1.7.10",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@types/socket.io-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.10.1",
"@typescript-eslint/parser": "^5.10.1",
"autoprefixer": "^10.4.7",
@@ -43,10 +44,12 @@
"dependencies": {
"axios": "^0.27.2",
"cookie": "^0.4.2",
"exifr": "^7.1.3",
"leaflet": "^1.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"moment": "^2.29.3",
"socket.io-client": "^4.5.1",
"svelte-material-icons": "^2.0.2"
}
}
@@ -91,7 +91,7 @@
<Close size="24" color="#232323" />
</button>
<p class="text-black text-lg">Info</p>
<p class="text-immich-fg text-lg">Info</p>
</div>
<div class="px-4 py-4">
@@ -2,16 +2,19 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import type { ImmichUser } from '$lib/models/immich-user';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { createEventDispatcher, onMount } from 'svelte';
import { fade, fly, slide } from 'svelte/transition';
import { postRequest } from '../../api';
import { serverEndpoint } from '../../constants';
import TrayArrowUp from 'svelte-material-icons/TrayArrowUp.svelte';
import { clickOutside } from './click-outside';
export let user: ImmichUser;
let shouldShowAccountInfo = false;
let shouldShowProfileImage = false;
const dispatch = createEventDispatcher();
let shouldShowAccountInfoPanel = false;
onMount(async () => {
const res = await fetch(`${serverEndpoint}/user/profile-image/${user.id}`, { method: 'GET' });
@@ -41,7 +44,7 @@
</script>
<section id="dashboard-navbar" class="fixed w-screen z-[100] bg-immich-bg text-sm">
<div class="flex border place-items-center px-6 py-2 ">
<div class="flex border-b place-items-center px-6 py-2 ">
<a class="flex gap-2 place-items-center hover:cursor-pointer" href="/photos">
<img src="/immich-logo.svg" alt="immich logo" height="35" width="35" />
<h1 class="font-immich-title text-2xl text-immich-primary">IMMICH</h1>
@@ -49,13 +52,21 @@
<div class="flex-1 ml-24">
<input class="w-[50%] border rounded-md bg-gray-200 px-8 py-4" placeholder="Search - Coming soon" />
</div>
<section class="flex gap-6 place-items-center">
<!-- <div>Upload</div> -->
<section class="flex gap-4 place-items-center">
{#if $page.url.pathname !== '/admin'}
<button
in:fly={{ x: 50, duration: 250 }}
on:click={() => dispatch('uploadClicked')}
class="flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium"
>
<TrayArrowUp size="20" />
<span> Upload </span>
</button>
{/if}
{#if user.isAdmin}
<button
class={`hover:text-immich-primary font-medium ${
class={`flex place-items-center place-content-center gap-2 hover:bg-immich-primary/5 p-2 rounded-lg font-medium ${
$page.url.pathname == '/admin' && 'text-immich-primary underline'
}`}
on:click={navigateToAdmin}>Administration</button
@@ -0,0 +1,191 @@
<script lang="ts">
import { quartInOut } from 'svelte/easing';
import { scale, fade } from 'svelte/transition';
import { uploadAssetsStore } from '$lib/stores/upload';
import CloudUploadOutline from 'svelte-material-icons/CloudUploadOutline.svelte';
import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte';
import type { UploadAsset } from '$lib/models/upload-asset';
let showDetail = true;
let uploadLength = 0;
const showUploadImageThumbnail = async (a: UploadAsset) => {
const extension = a.fileExtension.toLowerCase();
if (extension == 'jpeg' || extension == 'jpg' || extension == 'png') {
try {
const imgData = await a.file.arrayBuffer();
const arrayBufferView = new Uint8Array(imgData);
const blob = new Blob([arrayBufferView], { type: 'image/jpeg' });
const urlCreator = window.URL || window.webkitURL;
const imageUrl = urlCreator.createObjectURL(blob);
const img: any = document.getElementById(`${a.id}`);
img.src = imageUrl;
} catch (e) {}
}
};
function getSizeInHumanReadableFormat(sizeInByte: number) {
const pepibyte = 1.126 * Math.pow(10, 15);
const tebibyte = 1.1 * Math.pow(10, 12);
const gibibyte = 1.074 * Math.pow(10, 9);
const mebibyte = 1.049 * Math.pow(10, 6);
const kibibyte = 1024;
// Pebibyte
if (sizeInByte >= pepibyte) {
// Pe
return `${(sizeInByte / pepibyte).toFixed(1)}PB`;
} else if (tebibyte <= sizeInByte && sizeInByte < pepibyte) {
// Te
return `${(sizeInByte / tebibyte).toFixed(1)}TB`;
} else if (gibibyte <= sizeInByte && sizeInByte < tebibyte) {
// Gi
return `${(sizeInByte / gibibyte).toFixed(1)}GB`;
} else if (mebibyte <= sizeInByte && sizeInByte < gibibyte) {
// Mega
return `${(sizeInByte / mebibyte).toFixed(1)}MB`;
} else if (kibibyte <= sizeInByte && sizeInByte < mebibyte) {
// Kibi
return `${(sizeInByte / kibibyte).toFixed(1)}KB`;
} else {
return `${sizeInByte}B`;
}
}
// Reactive action to get thumbnail image of upload asset whenever there is a new one added to the list
$: {
if ($uploadAssetsStore.length != uploadLength) {
$uploadAssetsStore.map((asset) => {
showUploadImageThumbnail(asset);
});
uploadLength = $uploadAssetsStore.length;
}
}
$: {
if (showDetail) {
$uploadAssetsStore.map((asset) => {
showUploadImageThumbnail(asset);
});
}
}
let isUploading = false;
uploadAssetsStore.isUploading.subscribe((value) => (isUploading = value));
</script>
{#if isUploading}
<div
in:fade={{ duration: 250 }}
out:fade={{ duration: 250, delay: 1000 }}
class="absolute right-6 bottom-6 z-[10000]"
>
{#if showDetail}
<div
in:scale={{ duration: 250, easing: quartInOut }}
class="bg-gray-200 p-4 text-sm w-[300px] rounded-lg shadow-sm border "
>
<div class="flex justify-between place-item-center mb-4">
<p class="text-xs text-gray-500">UPLOADING {$uploadAssetsStore.length}</p>
<button
on:click={() => (showDetail = false)}
class="w-[20px] h-[20px] bg-gray-50 rounded-full flex place-items-center place-content-center transition-colors hover:bg-gray-100"
>
<WindowMinimize />
</button>
</div>
<div id="upload-item-list" class="max-h-[400px] overflow-y-auto pr-2 rounded-lg">
{#each $uploadAssetsStore as uploadAsset}
<div
in:fade={{ duration: 250 }}
out:fade={{ duration: 100 }}
class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
>
<div class="relative">
<img
in:fade={{ duration: 250 }}
id={`${uploadAsset.id}`}
src="/immich-logo.svg"
alt=""
class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg "
/>
<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
<p
class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
>
.{uploadAsset.fileExtension}
</p>
</div>
</div>
<div class="p-2 pr-4 flex flex-col justify-between">
<input
disabled
class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
value={`[${getSizeInHumanReadableFormat(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
/>
<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
<div
class="bg-immich-primary h-[15px] rounded-md transition-all"
style={`width: ${uploadAsset.progress}%`}
/>
<p class="absolute h-full w-full text-center top-0 text-[10px] ">{uploadAsset.progress}/100</p>
</div>
</div>
</div>
{/each}
</div>
</div>
{:else}
<div class="rounded-full">
<button
in:scale={{ duration: 250, easing: quartInOut }}
on:click={() => (showDetail = true)}
class="absolute -top-4 -left-4 text-xs rounded-full w-10 h-10 p-5 flex place-items-center place-content-center bg-immich-primary text-gray-200"
>
{$uploadAssetsStore.length}
</button>
<button
in:scale={{ duration: 250, easing: quartInOut }}
on:click={() => (showDetail = true)}
class="bg-gray-300 p-5 rounded-full w-16 h-16 flex place-items-center place-content-center text-sm shadow-lg "
>
<div class="animate-pulse">
<CloudUploadOutline size="30" color="#4250af" />
</div>
</button>
</div>
{/if}
</div>
{/if}
<style>
/* width */
#upload-item-list::-webkit-scrollbar {
width: 5px;
}
/* Track */
#upload-item-list::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 16px;
}
/* Handle */
#upload-item-list::-webkit-scrollbar-thumb {
background: #4250af68;
border-radius: 16px;
}
/* Handle on hover */
#upload-item-list::-webkit-scrollbar-thumb:hover {
background: #4250afad;
border-radius: 16px;
}
</style>
+6
View File
@@ -0,0 +1,6 @@
export type UploadAsset = {
id: string;
file: File;
progress: number;
fileExtension: string;
};
+17 -21
View File
@@ -1,33 +1,29 @@
import { writable, derived } from 'svelte/store';
import { getRequest } from '$lib/api';
import type { ImmichAsset } from '$lib/models/immich-asset'
import type { ImmichAsset } from '$lib/models/immich-asset';
import lodash from 'lodash-es';
import moment from 'moment';
export const assets = writable<ImmichAsset[]>([]);
export const assetsGroupByDate = derived(assets, ($assets) => {
try {
return lodash.chain($assets)
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD'))
.sortBy((group) => $assets.indexOf(group[0]))
.value();
} catch (e) {
console.log("error deriving state assets", e)
return []
}
})
try {
return lodash
.chain($assets)
.groupBy((a) => moment(a.createdAt).format('ddd, MMM DD'))
.sortBy((group) => $assets.indexOf(group[0]))
.value();
} catch (e) {
console.log('error deriving state assets', e);
return [];
}
});
export const flattenAssetGroupByDate = derived(assetsGroupByDate, ($assetsGroupByDate) => {
return $assetsGroupByDate.flat();
})
return $assetsGroupByDate.flat();
});
export const getAssetsInfo = async (accessToken: string) => {
const res = await getRequest('asset', accessToken);
assets.set(res);
}
const res = await getRequest('asset', accessToken);
assets.set(res);
};
+45
View File
@@ -0,0 +1,45 @@
import { writable, derived } from 'svelte/store';
import type { UploadAsset } from '../models/upload-asset';
function createUploadStore() {
const uploadAssets = writable<Array<UploadAsset>>([]);
const { subscribe } = uploadAssets;
const isUploading = derived(uploadAssets, ($uploadAssets) => {
return $uploadAssets.length > 0 ? true : false;
});
const addNewUploadAsset = (newAsset: UploadAsset) => {
uploadAssets.update((currentSet) => [...currentSet, newAsset]);
};
const updateProgress = (id: string, progress: number) => {
uploadAssets.update((uploadingAssets) => {
return uploadingAssets.map((asset) => {
if (asset.id == id) {
return {
...asset,
progress: progress,
};
}
return asset;
});
});
};
const removeUploadAsset = (id: string) => {
uploadAssets.update((uploadingAsset) => uploadingAsset.filter((a) => a.id != id));
};
return {
subscribe,
isUploading,
addNewUploadAsset,
updateProgress,
removeUploadAsset,
};
}
export const uploadAssetsStore = createUploadStore();
+30
View File
@@ -0,0 +1,30 @@
import { Socket, io } from 'socket.io-client';
import { serverEndpoint } from '../constants';
import type { ImmichAsset } from '../models/immich-asset';
import { assets } from './assets';
export const openWebsocketConnection = (accessToken: string) => {
const websocket = io(serverEndpoint, {
transports: ['polling'],
reconnection: true,
forceNew: true,
autoConnect: true,
extraHeaders: {
Authorization: 'Bearer ' + accessToken,
},
});
listenToEvent(websocket);
};
const listenToEvent = (socket: Socket) => {
socket.on('on_upload_success', (data) => {
const newUploadedAsset: ImmichAsset = JSON.parse(data);
assets.update((assets) => [...assets, newUploadedAsset]);
});
socket.on('error', (e) => {
console.log('Websocket Error', e);
});
};
+113
View File
@@ -0,0 +1,113 @@
import * as exifr from 'exifr';
import { serverEndpoint } from '../constants';
import { uploadAssetsStore } from '$lib/stores/upload';
import type { UploadAsset } from '../models/upload-asset';
export async function fileUploader(asset: File, accessToken: string) {
const assetType = asset.type.split('/')[0].toUpperCase();
const temp = asset.name.split('.');
const fileExtension = temp[temp.length - 1];
const formData = new FormData();
try {
let exifData = null;
if (assetType !== 'VIDEO') {
exifData = await exifr.parse(asset);
}
const createdAt =
exifData && exifData.DateTimeOriginal != null
? new Date(exifData.DateTimeOriginal).toISOString()
: new Date(asset.lastModified).toISOString();
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
// Create and add Unique ID of asset on the device
formData.append('deviceAssetId', deviceAssetId);
// Get device id - for web -> use WEB
formData.append('deviceId', 'WEB');
// Get asset type
formData.append('assetType', assetType);
// Get Asset Created Date
formData.append('createdAt', createdAt);
// Get Asset Modified At
formData.append('modifiedAt', new Date(asset.lastModified).toISOString());
// Set Asset is Favorite to false
formData.append('isFavorite', 'false');
// Get asset duration
formData.append('duration', '0:00:00.000000');
// Get asset file extension
formData.append('fileExtension', '.' + fileExtension);
// Get asset binary data.
formData.append('assetData', asset);
// Check if asset upload on server before performing upload
const res = await fetch(serverEndpoint + '/asset/check', {
method: 'POST',
body: JSON.stringify({ deviceAssetId }),
headers: {
Authorization: 'Bearer ' + accessToken,
'Content-Type': 'application/json',
},
});
if (res.status === 200) {
const { isExist } = await res.json();
if (isExist) {
return;
}
}
const request = new XMLHttpRequest();
request.upload.onloadstart = () => {
const newUploadAsset: UploadAsset = {
id: deviceAssetId,
file: asset,
progress: 0,
fileExtension: fileExtension,
};
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
};
request.upload.onload = () => {
setTimeout(() => {
uploadAssetsStore.removeUploadAsset(deviceAssetId);
}, 2500);
};
// listen for `error` event
request.upload.onerror = () => {
uploadAssetsStore.removeUploadAsset(deviceAssetId);
};
// listen for `abort` event
request.upload.onabort = () => {
uploadAssetsStore.removeUploadAsset(deviceAssetId);
};
// listen for `progress` event
request.upload.onprogress = (event) => {
const percentComplete = Math.floor((event.loaded / event.total) * 100);
uploadAssetsStore.updateProgress(deviceAssetId, percentComplete);
};
request.open('POST', `${serverEndpoint}/asset/upload`);
request.setRequestHeader('Authorization', `Bearer ${accessToken}`);
request.send(formData);
} catch (e) {
console.log('error uploading file ', e);
}
}
+3 -3
View File
@@ -22,8 +22,8 @@
import { blur } from 'svelte/transition';
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
import FullScreenModal from '../lib/components/shared/full-screen-modal.svelte';
import AnnouncementBox from '../lib/components/shared/announcement-box.svelte';
import AnnouncementBox from '$lib/components/shared/announcement-box.svelte';
import UploadPanel from '$lib/components/shared/upload-panel.svelte';
export let url: string;
export let shouldShowAnnouncement: boolean;
@@ -36,7 +36,7 @@
<div transition:blur={{ duration: 250 }}>
<slot />
<DownloadPanel />
<UploadPanel />
{#if shouldShowAnnouncement}
<AnnouncementBox {localVersion} {remoteVersion} on:close={() => (shouldShowAnnouncement = false)} />
{/if}
+1
View File
@@ -35,6 +35,7 @@
<script lang="ts">
import { serverEndpoint } from '$lib/constants';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
export let isAdminUserExist: boolean;
+37 -6
View File
@@ -41,6 +41,8 @@
import AssetViewer from '../../lib/components/asset-viewer/asset-viewer.svelte';
import DownloadPanel from '../../lib/components/asset-viewer/download-panel.svelte';
import StatusBox from '../../lib/components/shared/status-box.svelte';
import { fileUploader } from '../../lib/utils/file-uploader';
import { openWebsocketConnection } from '../../lib/stores/websocket';
export let user: ImmichUser;
let selectedAction: AppSideBarSelection;
@@ -64,6 +66,8 @@
if ($session.user) {
await getAssetsInfo($session.user.accessToken);
openWebsocketConnection($session.user.accessToken);
}
});
@@ -79,7 +83,34 @@
currentViewAssetIndex = $flattenAssetGroupByDate.findIndex((a) => a.id == assetId);
currentSelectedAsset = $flattenAssetGroupByDate[currentViewAssetIndex];
isShowAsset = true;
// pushState(assetId);
};
const uploadClickedHandler = async () => {
if ($session.user) {
try {
let fileSelector = document.createElement('input');
fileSelector.type = 'file';
fileSelector.multiple = true;
fileSelector.accept = 'image/*,video/*,.heic,.heif';
fileSelector.onchange = async (e: any) => {
const files = Array.from<File>(e.target.files);
const acceptedFile = files.filter(
(e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image',
);
for (const asset of acceptedFile) {
await fileUploader(asset, $session.user!.accessToken);
}
};
fileSelector.click();
} catch (e) {
console.log('Error seelcting file', e);
}
}
};
</script>
@@ -88,10 +119,10 @@
</svelte:head>
<section>
<NavigationBar {user} />
<NavigationBar {user} on:uploadClicked={uploadClickedHandler} />
</section>
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen">
<section class="grid grid-cols-[250px_auto] relative pt-[72px] h-screen bg-immich-bg">
<!-- Sidebar -->
<section id="sidebar" class="flex flex-col gap-4 pt-8 pr-6">
<SideBarButton
@@ -111,7 +142,7 @@
<!-- Main Section -->
<section class="overflow-y-auto relative">
<section id="assets-content" class="relative pt-8 pl-4">
<section id="assets-content" class="relative pt-8 pl-4 mb-12 bg-immich-bg">
<section id="image-grid" class="flex flex-wrap gap-14">
{#each $assetsGroupByDate as assetsInDateGroup, groupIndex}
<!-- Asset Group By Date -->
@@ -121,7 +152,7 @@
on:mouseleave={() => (isMouseOverGroup = false)}
>
<!-- Date group title -->
<p class="font-medium text-sm text-black mb-2 flex place-items-center h-6">
<p class="font-medium text-sm text-immich-fg mb-2 flex place-items-center h-6">
{#if selectedGroupThumbnail === groupIndex && isMouseOverGroup}
<div
in:fly={{ x: -24, duration: 200, opacity: 0.5 }}
@@ -136,7 +167,7 @@
</p>
<!-- Image grid -->
<div class="flex flex-wrap gap-1">
<div class="flex flex-wrap gap-[2px]">
{#each assetsInDateGroup as asset}
<ImmichThumbnail
{asset}
+4
View File
@@ -5,6 +5,10 @@ module.exports = {
colors: {
'immich-primary': '#4250af',
'immich-bg': '#f6f8fe',
'immich-fg': 'black',
// 'immich-bg': '#121212',
// 'immich-fg': '#D0D0D0',
},
fontFamily: {
'immich-title': ['Snowburst One', 'cursive'],