Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f1cf44717 | ||
|
|
2d83ac4125 | ||
|
|
7147486b6a | ||
|
|
89ddbac8bc | ||
|
|
e071b82e8a | ||
|
|
13b2b2fc4e | ||
|
|
fe9ef1a3ea | ||
|
|
afb0d0f54d | ||
|
|
26085ff82b | ||
|
|
2872886e77 | ||
|
|
a21112e4ab | ||
|
|
f3edf43158 | ||
|
|
1c5926553a | ||
|
|
05fa3092bf | ||
|
|
7d3ec8af37 | ||
|
|
8db008ef0b | ||
|
|
e493e05e99 | ||
|
|
b83e535010 | ||
|
|
111372edc1 | ||
|
|
625a899f64 | ||
|
|
aaf0496f74 | ||
|
|
4977926c88 | ||
|
|
f41e1159d1 | ||
|
|
670107373b | ||
|
|
e660f05c31 | ||
|
|
baf1ea313e | ||
|
|
ed64c91da6 | ||
|
|
c40aa4399b | ||
|
|
8f08100a30 | ||
|
|
337cd33042 | ||
|
|
1e8fc7266c | ||
|
|
7f35583c2c | ||
|
|
ace755f264 | ||
|
|
7b25c9d0a7 | ||
|
|
c0bee2a6b7 | ||
|
|
b48d5cab22 | ||
|
|
4f59e6c7ab | ||
|
|
82a5d54d2c | ||
|
|
5e6d830ecd | ||
|
|
f700f3427b | ||
|
|
0c07c0ba4e | ||
|
|
bc885f3644 | ||
|
|
6668964d92 |
4
.github/workflows/docker-cleanup.yml
vendored
4
.github/workflows/docker-cleanup.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
||||
-
|
||||
name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.1.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.2.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
-
|
||||
name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.1.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.2.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
preset: 'ts-jest',
|
||||
setupFilesAfterEnv: ['jest-extended/all'],
|
||||
};
|
||||
|
||||
export default config;
|
||||
222
cli/package-lock.json
generated
222
cli/package-lock.json
generated
@@ -8,9 +8,14 @@
|
||||
"name": "immich-cli",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"byte-size": "^8.1.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^11.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"systeminformation": "^5.18.4"
|
||||
"glob": "^10.3.1",
|
||||
"picomatch": "^2.3.1",
|
||||
"systeminformation": "^5.18.4",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/byte-size": "^8.1.0",
|
||||
@@ -22,28 +27,23 @@
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"byte-size": "^8.1.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"chai": "^4.3.7",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^11.0.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"jest": "^29.5.0",
|
||||
"jest-extended": "^4.0.0",
|
||||
"jest-message-util": "^29.5.0",
|
||||
"jest-mock-axios": "^4.7.2",
|
||||
"jest-when": "^3.5.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"picomatch": "^2.3.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^4.9.4",
|
||||
"yaml": "^2.3.1"
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -111,9 +111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -154,9 +154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -772,7 +772,6 @@
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
@@ -789,7 +788,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
|
||||
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -801,7 +799,6 @@
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
|
||||
"integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -812,14 +809,12 @@
|
||||
"node_modules/@isaacs/cliui/node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"dev": true
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
|
||||
},
|
||||
"node_modules/@isaacs/cliui/node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
@@ -836,7 +831,6 @@
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
|
||||
"integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
@@ -851,7 +845,6 @@
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
@@ -1347,7 +1340,6 @@
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
@@ -1664,7 +1656,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.60.1.tgz",
|
||||
"integrity": "sha512-pHWlc3alg2oSMGwsU/Is8hbm3XFbcrb6P5wIxcQW9NsYBfnrubl/GhVVD/Jm/t8HXhA2WncoIRfBtnCgRGV96Q==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.60.1",
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
@@ -1692,7 +1683,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.60.1.tgz",
|
||||
"integrity": "sha512-Dn/LnN7fEoRD+KspEOV0xDMynEmR3iSHdgNsarlXNLGGtcUok8L4N71dxUgt3YvlO8si7E+BJ5Fe3wb5yUw7DQ==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"@typescript-eslint/visitor-keys": "5.60.1"
|
||||
@@ -1710,7 +1700,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.60.1.tgz",
|
||||
"integrity": "sha512-zDcDx5fccU8BA0IDZc71bAtYIcG9PowaOwaD8rjYbqwK7dpe/UMQl3inJ4UtUK42nOCT41jTSCwg76E62JpMcg==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
},
|
||||
@@ -1724,7 +1713,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.60.1.tgz",
|
||||
"integrity": "sha512-hkX70J9+2M2ZT6fhti5Q2FoU9zb+GeZK2SLP1WZlvUDqdMbEKhexZODD1WodNRyO8eS+4nScvT0dts8IdaBzfw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"@typescript-eslint/visitor-keys": "5.60.1",
|
||||
@@ -1752,7 +1740,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.60.1.tgz",
|
||||
"integrity": "sha512-xEYIxKcultP6E/RMKqube11pGjXH1DCo60mQoWhVYyKfLkwbIVVjYxmOenNMxILx0TjCujPTjjnTIVzm09TXIw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "5.60.1",
|
||||
"eslint-visitor-keys": "^3.3.0"
|
||||
@@ -2036,7 +2023,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -2045,7 +2031,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
@@ -2211,8 +2196,7 @@
|
||||
"node_modules/balanced-match": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
@@ -2311,7 +2295,6 @@
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz",
|
||||
"integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=12.17"
|
||||
}
|
||||
@@ -2464,7 +2447,6 @@
|
||||
"version": "3.12.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
|
||||
"integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.3"
|
||||
},
|
||||
@@ -2506,7 +2488,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
@@ -2517,8 +2498,7 @@
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
@@ -2535,7 +2515,6 @@
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz",
|
||||
"integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
@@ -2562,7 +2541,6 @@
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
@@ -2675,8 +2653,7 @@
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.440",
|
||||
@@ -2699,8 +2676,7 @@
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
|
||||
},
|
||||
"node_modules/error-ex": {
|
||||
"version": "1.3.2",
|
||||
@@ -2801,9 +2777,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-jest": {
|
||||
"version": "27.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.2.tgz",
|
||||
"integrity": "sha512-euzbp06F934Z7UDl5ZUaRPLAc9MKjh0rMPERrHT7UhlCEwgb25kBj37TvMgWeHZVkR5I9CayswrpoaqZU1RImw==",
|
||||
"version": "27.2.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.2.3.tgz",
|
||||
"integrity": "sha512-sRLlSCpICzWuje66Gl9zvdF6mwD5X86I4u55hJyFBsxYOsBCmT5+kSUjf+fkFWVMMgpzNEupjW8WzUqi83hJAQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/utils": "^5.10.0"
|
||||
@@ -2812,7 +2788,7 @@
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0",
|
||||
"eslint": "^7.0.0 || ^8.0.0",
|
||||
"jest": "*"
|
||||
},
|
||||
@@ -3203,7 +3179,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz",
|
||||
"integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.0",
|
||||
"signal-exit": "^4.0.1"
|
||||
@@ -3219,7 +3194,6 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz",
|
||||
"integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
@@ -3318,7 +3292,6 @@
|
||||
"version": "10.3.1",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz",
|
||||
"integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.1.0",
|
||||
"jackspeak": "^2.0.3",
|
||||
@@ -3352,7 +3325,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
@@ -3361,7 +3333,6 @@
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz",
|
||||
"integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -3458,6 +3429,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
@@ -3597,7 +3574,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -3656,8 +3632,7 @@
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
|
||||
},
|
||||
"node_modules/istanbul-lib-coverage": {
|
||||
"version": "3.2.0",
|
||||
@@ -3685,9 +3660,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/istanbul-lib-instrument/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -3750,7 +3725,6 @@
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz",
|
||||
"integrity": "sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
@@ -3933,24 +3907,6 @@
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-config/node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-diff": {
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.5.0.tgz",
|
||||
@@ -4571,9 +4527,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/make-dir/node_modules/semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
||||
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -4675,7 +4631,6 @@
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-6.0.2.tgz",
|
||||
"integrity": "sha512-MzWSV5nYVT7mVyWCwn2o7JH13w2TBRmmSqSRCKzTw+lmft9X4z+3wjvs06Tzijo5z4W/kahUCDpRXTF+ZrmF/w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
@@ -4719,6 +4674,27 @@
|
||||
"integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-package-data/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
@@ -4838,6 +4814,24 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -4860,7 +4854,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -4875,7 +4868,6 @@
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.0.tgz",
|
||||
"integrity": "sha512-tZFEaRQbMLjwrsmidsGJ6wDMv0iazJWk6SfIKnY4Xru8auXgmJkOBa5DUbYFcFD2Rzk2+KDlIiF0GVXNCbgC7g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"lru-cache": "^9.1.1 || ^10.0.0",
|
||||
"minipass": "^5.0.0 || ^6.0.2"
|
||||
@@ -4891,7 +4883,6 @@
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz",
|
||||
"integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "14 || >=16.14"
|
||||
}
|
||||
@@ -4924,7 +4915,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
@@ -5239,51 +5229,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg/node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/read-pkg/node_modules/normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg/node_modules/parse-json": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
|
||||
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.0.0",
|
||||
"error-ex": "^1.3.1",
|
||||
"json-parse-even-better-errors": "^2.3.0",
|
||||
"lines-and-columns": "^1.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg/node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg/node_modules/type-fest": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
||||
@@ -5502,7 +5447,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
@@ -5514,7 +5458,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
@@ -5635,7 +5578,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -5650,7 +5592,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
@@ -5664,7 +5605,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -5677,7 +5617,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
@@ -6111,7 +6050,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
@@ -6123,9 +6061,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
|
||||
"integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==",
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz",
|
||||
"integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -6153,7 +6091,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
@@ -6204,7 +6141,6 @@
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz",
|
||||
"integrity": "sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
"name": "immich-cli",
|
||||
"dependencies": {
|
||||
"axios": "^1.4.0",
|
||||
"byte-size": "^8.1.1",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^11.0.0",
|
||||
"form-data": "^4.0.0",
|
||||
"mime-types": "^2.1.35",
|
||||
"systeminformation": "^5.18.4"
|
||||
"glob": "^10.3.1",
|
||||
"picomatch": "^2.3.1",
|
||||
"systeminformation": "^5.18.4",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/byte-size": "^8.1.0",
|
||||
@@ -16,34 +21,48 @@
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.60.1",
|
||||
"byte-size": "^8.1.1",
|
||||
"@typescript-eslint/parser": "^5.48.1",
|
||||
"chai": "^4.3.7",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^11.0.0",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-jest": "^27.2.2",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-unicorn": "^47.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"jest": "^29.5.0",
|
||||
"jest-extended": "^4.0.0",
|
||||
"jest-message-util": "^29.5.0",
|
||||
"jest-mock-axios": "^4.7.2",
|
||||
"jest-when": "^3.5.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"picomatch": "^2.3.1",
|
||||
"ts-jest": "^29.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^4.9.4",
|
||||
"yaml": "^2.3.1"
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --project tsconfig.build.json",
|
||||
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
|
||||
"prepack": "yarn build ",
|
||||
"test": "jest",
|
||||
"test:cov": "jest --coverage",
|
||||
"format": "prettier --check ."
|
||||
},
|
||||
"jest": {
|
||||
"clearMocks": true,
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.ts$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.(t|j)s"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
||||
140
cli/src/api/open-api/api.ts
generated
140
cli/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -1304,6 +1304,12 @@ export interface ExifResponseDto {
|
||||
* @memberof ExifResponseDto
|
||||
*/
|
||||
'description'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ExifResponseDto
|
||||
*/
|
||||
'projectionType'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -1802,6 +1808,50 @@ export interface PeopleResponseDto {
|
||||
*/
|
||||
'people': Array<PersonResponseDto>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PeopleUpdateDto
|
||||
*/
|
||||
export interface PeopleUpdateDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<PeopleUpdateItem>}
|
||||
* @memberof PeopleUpdateDto
|
||||
*/
|
||||
'people': Array<PeopleUpdateItem>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PeopleUpdateItem
|
||||
*/
|
||||
export interface PeopleUpdateItem {
|
||||
/**
|
||||
* Person id.
|
||||
* @type {string}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
* Person name.
|
||||
* @type {string}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'name'?: string;
|
||||
/**
|
||||
* Asset is used to get the feature face thumbnail.
|
||||
* @type {string}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'featureFaceAssetId'?: string;
|
||||
/**
|
||||
* Person visibility
|
||||
* @type {boolean}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'isHidden'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -8896,6 +8946,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updatePeople: async (peopleUpdateDto: PeopleUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'peopleUpdateDto' is not null or undefined
|
||||
assertParamExists('updatePeople', 'peopleUpdateDto', peopleUpdateDto)
|
||||
const localVarPath = `/person`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(peopleUpdateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -9005,6 +9099,16 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async updatePeople(peopleUpdateDto: PeopleUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updatePeople(peopleUpdateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -9071,6 +9175,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
|
||||
mergePerson(requestParameters: PersonApiMergePersonRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updatePeople(requestParameters: PersonApiUpdatePeopleRequest, options?: AxiosRequestConfig): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.updatePeople(requestParameters.peopleUpdateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePersonRequest} requestParameters Request parameters.
|
||||
@@ -9160,6 +9273,20 @@ export interface PersonApiMergePersonRequest {
|
||||
readonly mergePersonDto: MergePersonDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePeople operation in PersonApi.
|
||||
* @export
|
||||
* @interface PersonApiUpdatePeopleRequest
|
||||
*/
|
||||
export interface PersonApiUpdatePeopleRequest {
|
||||
/**
|
||||
*
|
||||
* @type {PeopleUpdateDto}
|
||||
* @memberof PersonApiUpdatePeople
|
||||
*/
|
||||
readonly peopleUpdateDto: PeopleUpdateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePerson operation in PersonApi.
|
||||
* @export
|
||||
@@ -9243,6 +9370,17 @@ export class PersonApi extends BaseAPI {
|
||||
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public updatePeople(requestParameters: PersonApiUpdatePeopleRequest, options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).updatePeople(requestParameters.peopleUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePersonRequest} requestParameters Request parameters.
|
||||
|
||||
2
cli/src/api/open-api/base.ts
generated
2
cli/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/common.ts
generated
2
cli/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/configuration.ts
generated
2
cli/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
cli/src/api/open-api/index.ts
generated
2
cli/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -70,11 +70,13 @@ export default class Upload extends BaseCommand {
|
||||
if (options.import) {
|
||||
const importData = {
|
||||
assetPath: asset.path,
|
||||
sidecarPath: asset.sidecarPath,
|
||||
deviceAssetId: asset.deviceAssetId,
|
||||
deviceId: this.deviceId,
|
||||
fileCreatedAt: asset.fileCreatedAt,
|
||||
fileModifiedAt: asset.fileModifiedAt,
|
||||
isFavorite: false,
|
||||
isReadOnly: options.readOnly,
|
||||
};
|
||||
|
||||
if (!this.dryRun) {
|
||||
|
||||
@@ -5,4 +5,5 @@ export class UploadOptionsDto {
|
||||
skipHash = false;
|
||||
delete = false;
|
||||
import = false;
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ program
|
||||
.default(false),
|
||||
)
|
||||
.addOption(new Option('-i, --ignore [paths...]', 'Paths to ignore').env('IMMICH_IGNORE_PATHS').default(false))
|
||||
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||
.addOption(new Option('--no-read-only', 'Import files without read-only protection, allowing Immich to manage them'))
|
||||
.argument('[paths...]', 'One or more paths to assets to be imported')
|
||||
.action((paths, options) => {
|
||||
options.import = true;
|
||||
options.excludePatterns = options.ignore;
|
||||
new Upload().run(paths, options);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"extends": "../tsconfig",
|
||||
"compilerOptions": {
|
||||
"noEmit": true
|
||||
},
|
||||
"references": [{ "path": ".." }]
|
||||
}
|
||||
4
cli/tsconfig.build.json
Normal file
4
cli/tsconfig.build.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"exclude": ["dist", "node_modules", "upload", "test", "**/*spec.ts"]
|
||||
}
|
||||
@@ -31,7 +31,6 @@ services:
|
||||
build:
|
||||
context: ../machine-learning
|
||||
dockerfile: Dockerfile
|
||||
command: python main.py
|
||||
ports:
|
||||
- 3003:3003
|
||||
volumes:
|
||||
@@ -116,7 +115,6 @@ services:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
PG_DATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
|
||||
@@ -85,7 +85,6 @@ services:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
PG_DATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
@@ -37,7 +37,6 @@ services:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
PG_DATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
networks:
|
||||
|
||||
@@ -69,7 +69,6 @@ services:
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
PG_DATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Database
|
||||
|
||||
:::caution
|
||||
Immich saves [file paths in the database](https://github.com/immich-app/immich/discussions/3299), it does not scan the library folder to update the database so backups are crucial.
|
||||
:::
|
||||
|
||||
:::info
|
||||
Refer to the official [postgres documentation](https://www.postgresql.org/docs/current/backup.html) for details about backing up and restoring a postgres database.
|
||||
:::
|
||||
|
||||
@@ -184,3 +184,24 @@ Typesense URL example JSON before encoding:
|
||||
| `MACHINE_LEARNING_CLASSIFICATION_MODEL` | Classification Model | `microsoft/resnet-50` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | ML Cache Location | `/cache` | machine learning |
|
||||
| `TRANSFORMERS_CACHE` | ML Transformers Cache Location | `/cache` | machine learning |
|
||||
|
||||
## Docker Secrets
|
||||
|
||||
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
|
||||
|
||||
To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of
|
||||
the `_FILE` variable should be set to the path of a file containing the variable value.
|
||||
|
||||
| Regular Variable | Equivalent Docker Secrets '\_FILE' Variable |
|
||||
| :----------------: | :-----------------------------------------: |
|
||||
| `DB_HOSTNAME` | `DB_HOSTNAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_DATABASE_NAME` | `DB_DATABASE_NAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_USERNAME` | `DB_USERNAME_FILE`<sup>\*1</sup> |
|
||||
| `DB_PASSWORD` | `DB_PASSWORD_FILE`<sup>\*1</sup> |
|
||||
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
|
||||
|
||||
\*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for
|
||||
details on how to use Docker Secrets in the Postgres image.
|
||||
|
||||
\*2: See [this comment](https://github.com/docker-library/redis/issues/46#issuecomment-335326234) for an example of how
|
||||
to use use a Docker secret for the password in the Redis container.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
Immich allows the admin user to set the uploaded filename pattern. Both at the directory and filename level.
|
||||
|
||||
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text.
|
||||
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename.
|
||||
|
||||
```bash title="Default template"
|
||||
Year/Year-Month-Day/Filename.Extension
|
||||
@@ -8,4 +8,4 @@ Year/Year-Month-Day/Filename.Extension
|
||||
|
||||
<img src={require('./img/storage-template.png').default} width="100%" title="Storage Template Setting" />
|
||||
|
||||
Immich also provides a mechanism to migrate between template so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job in the Job page.
|
||||
Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page.
|
||||
|
||||
@@ -15,6 +15,8 @@ RUN poetry install --sync --no-interaction --no-ansi --no-root --only main
|
||||
|
||||
FROM python:3.11.4-slim-bullseye@sha256:91d194f58f50594cda71dcd2e8fdefd90e7ecc57d07823813b67c8521e565dcd
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends tini && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
@@ -25,4 +27,5 @@ ENV NODE_ENV=production \
|
||||
|
||||
COPY --from=builder /opt/venv /opt/venv
|
||||
COPY app .
|
||||
ENTRYPOINT ["python", "-m", "app.main"]
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["python", "-m", "app.main"]
|
||||
|
||||
1170
machine-learning/poetry.lock
generated
1170
machine-learning/poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "machine-learning"
|
||||
version = "1.68.0"
|
||||
version = "1.71.0"
|
||||
description = ""
|
||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||
readme = "README.md"
|
||||
@@ -22,8 +22,6 @@ fastapi = "^0.95.2"
|
||||
uvicorn = {extras = ["standard"], version = "^0.22.0"}
|
||||
pydantic = "^1.10.8"
|
||||
aiocache = "^0.12.1"
|
||||
pytest-cov = "^4.1.0"
|
||||
ruff = "^0.0.272"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
mypy = "^1.3.0"
|
||||
@@ -33,6 +31,8 @@ locust = "^2.15.1"
|
||||
gunicorn = "^20.1.0"
|
||||
httpx = "^0.24.1"
|
||||
pytest-asyncio = "^0.21.0"
|
||||
pytest-cov = "^4.1.0"
|
||||
ruff = "^0.0.272"
|
||||
|
||||
[[tool.poetry.source]]
|
||||
name = "pytorch-cpu"
|
||||
|
||||
2
mobile/android/.gitignore
vendored
2
mobile/android/.gitignore
vendored
@@ -13,4 +13,4 @@ key.properties
|
||||
**/*.jks
|
||||
|
||||
# Fastlane
|
||||
/fastlane/report.xml
|
||||
fastlane/report.xml
|
||||
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 91,
|
||||
"android.injected.version.name" => "1.68.0",
|
||||
"android.injected.version.code" => 94,
|
||||
"android.injected.version.name" => "1.71.0",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000296">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000239">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="64.042552">
|
||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="68.788432">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.676557">
|
||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.76592">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -186,6 +186,7 @@
|
||||
"login_form_save_login": "Stay logged in",
|
||||
"login_form_server_empty": "Enter a server URL.",
|
||||
"login_form_server_error": "Could not connect to server.",
|
||||
"login_disabled": "Login has been disabled",
|
||||
"monthly_title_text_date_format": "MMMM y",
|
||||
"motion_photos_page_title": "Motion Photos",
|
||||
"notification_permission_dialog_cancel": "Cancel",
|
||||
@@ -290,4 +291,4 @@
|
||||
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
||||
}
|
||||
}
|
||||
|
||||
4
mobile/ios/.gitignore
vendored
4
mobile/ios/.gitignore
vendored
@@ -31,4 +31,6 @@ Runner/GeneratedPluginRegistrant.*
|
||||
!default.mode1v3
|
||||
!default.mode2v3
|
||||
!default.pbxuser
|
||||
!default.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
fastlane/report.xml
|
||||
|
||||
@@ -157,4 +157,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.12.1
|
||||
|
||||
@@ -379,7 +379,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 97;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -515,7 +515,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 97;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -543,7 +543,7 @@
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 97;
|
||||
CURRENT_PROJECT_VERSION = 110;
|
||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -59,11 +59,11 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.57.0</string>
|
||||
<string>1.70.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>97</string>
|
||||
<string>110</string>
|
||||
<key>FLTEnableImpeller</key>
|
||||
<true />
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
|
||||
@@ -19,7 +19,7 @@ platform :ios do
|
||||
desc "iOS Beta"
|
||||
lane :beta do
|
||||
increment_version_number(
|
||||
version_number: "1.68.0"
|
||||
version_number: "1.71.0"
|
||||
)
|
||||
increment_build_number(
|
||||
build_number: latest_testflight_build_number + 1,
|
||||
|
||||
@@ -5,32 +5,32 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000407">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000211">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.988375">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="2.108738">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="45.42439">
|
||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="28.952846">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="2.381359">
|
||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="1.821481">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="94.653021">
|
||||
<testcase classname="fastlane.lanes" name="4: build_app" time="99.212621">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="58.237354">
|
||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="68.366701">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart'
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/notification_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
@@ -156,6 +157,8 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
||||
|
||||
ref.read(iOSBackgroundSettingsProvider.notifier).refresh();
|
||||
|
||||
ref.invalidate(memoryFutureProvider);
|
||||
|
||||
break;
|
||||
|
||||
case AppLifecycleState.inactive:
|
||||
|
||||
@@ -499,6 +499,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
PhotoViewGallery.builder(
|
||||
scaleStateChangedCallback: (state) {
|
||||
isZoomed.value = state != PhotoViewScaleState.initial;
|
||||
ref.read(showControlsProvider.notifier).show = !isZoomed.value;
|
||||
},
|
||||
pageController: controller,
|
||||
scrollPhysics: isZoomed.value
|
||||
|
||||
@@ -341,8 +341,8 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
backgroundColor: Theme.of(context).hintColor,
|
||||
labelTextBuilder: _labelBuilder,
|
||||
labelConstraints: const BoxConstraints(maxHeight: 28),
|
||||
scrollbarAnimationDuration: const Duration(seconds: 1),
|
||||
scrollbarTimeToFade: const Duration(seconds: 4),
|
||||
scrollbarAnimationDuration: const Duration(milliseconds: 300),
|
||||
scrollbarTimeToFade: const Duration(milliseconds: 1000),
|
||||
child: listWidget,
|
||||
)
|
||||
: listWidget;
|
||||
|
||||
@@ -37,6 +37,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final _log = Logger("AuthenticationNotifier");
|
||||
|
||||
Future<bool> login(
|
||||
String email,
|
||||
@@ -145,38 +146,66 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
||||
Future<bool> setSuccessLoginInfo({
|
||||
required String accessToken,
|
||||
required String serverUrl,
|
||||
bool offlineLogin = false,
|
||||
}) async {
|
||||
_apiService.setAccessToken(accessToken);
|
||||
UserResponseDto? userResponseDto;
|
||||
try {
|
||||
userResponseDto = await _apiService.userApi.getMyUserInfo();
|
||||
} on ApiException catch (e) {
|
||||
if (e.innerException is SocketException) {
|
||||
state = state.copyWith(isAuthenticated: true);
|
||||
|
||||
// Get the deviceid from the store if it exists, otherwise generate a new one
|
||||
String deviceId =
|
||||
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
|
||||
|
||||
bool shouldChangePassword = false;
|
||||
User? user;
|
||||
|
||||
bool retResult = false;
|
||||
User? offlineUser = Store.tryGet(StoreKey.currentUser);
|
||||
|
||||
// If the user is offline and there is a user saved on the device,
|
||||
// if not try an online login
|
||||
if (offlineLogin && offlineUser != null) {
|
||||
user = offlineUser;
|
||||
retResult = false;
|
||||
} else {
|
||||
UserResponseDto? userResponseDto;
|
||||
try {
|
||||
userResponseDto = await _apiService.userApi.getMyUserInfo();
|
||||
} on ApiException catch (e) {
|
||||
if (e.innerException is SocketException) {
|
||||
state = state.copyWith(isAuthenticated: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (userResponseDto != null) {
|
||||
Store.put(StoreKey.deviceId, deviceId);
|
||||
Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
|
||||
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
|
||||
Store.put(StoreKey.serverUrl, serverUrl);
|
||||
Store.put(StoreKey.accessToken, accessToken);
|
||||
|
||||
shouldChangePassword = userResponseDto.shouldChangePassword;
|
||||
user = User.fromDto(userResponseDto);
|
||||
|
||||
retResult = true;
|
||||
}
|
||||
else {
|
||||
_log.severe("Unable to get user information from the server.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (userResponseDto != null) {
|
||||
final deviceId = await FlutterUdid.consistentUdid;
|
||||
Store.put(StoreKey.deviceId, deviceId);
|
||||
Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
|
||||
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
|
||||
Store.put(StoreKey.serverUrl, serverUrl);
|
||||
Store.put(StoreKey.accessToken, accessToken);
|
||||
state = state.copyWith(
|
||||
isAuthenticated: true,
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
profileImagePath: user.profileImagePath,
|
||||
isAdmin: user.isAdmin,
|
||||
shouldChangePassword: shouldChangePassword,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
isAuthenticated: true,
|
||||
userId: userResponseDto.id,
|
||||
userEmail: userResponseDto.email,
|
||||
firstName: userResponseDto.firstName,
|
||||
lastName: userResponseDto.lastName,
|
||||
profileImagePath: userResponseDto.profileImagePath,
|
||||
isAdmin: userResponseDto.isAdmin,
|
||||
shouldChangePassword: userResponseDto.shouldChangePassword,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
return retResult;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
final isLoading = useState<bool>(false);
|
||||
final isLoadingServer = useState<bool>(false);
|
||||
final isOauthEnable = useState<bool>(false);
|
||||
final isPasswordLoginEnable = useState<bool>(false);
|
||||
final oAuthButtonLabel = useState<String>('OAuth');
|
||||
final logoAnimationController = useAnimationController(
|
||||
duration: const Duration(seconds: 60),
|
||||
@@ -69,9 +70,11 @@ class LoginForm extends HookConsumerWidget {
|
||||
|
||||
if (loginConfig != null) {
|
||||
isOauthEnable.value = loginConfig.enabled;
|
||||
isPasswordLoginEnable.value = loginConfig.passwordLoginEnabled;
|
||||
oAuthButtonLabel.value = loginConfig.buttonText ?? 'OAuth';
|
||||
} else {
|
||||
isOauthEnable.value = false;
|
||||
isPasswordLoginEnable.value = true;
|
||||
}
|
||||
|
||||
serverEndpoint.value = endpoint;
|
||||
@@ -82,6 +85,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
isOauthEnable.value = false;
|
||||
isPasswordLoginEnable.value = true;
|
||||
isLoadingServer.value = false;
|
||||
return false;
|
||||
} catch (e) {
|
||||
@@ -91,6 +95,7 @@ class LoginForm extends HookConsumerWidget {
|
||||
toastType: ToastType.error,
|
||||
);
|
||||
isOauthEnable.value = false;
|
||||
isPasswordLoginEnable.value = true;
|
||||
isLoadingServer.value = false;
|
||||
return false;
|
||||
}
|
||||
@@ -262,18 +267,20 @@ class LoginForm extends HookConsumerWidget {
|
||||
style: Theme.of(context).textTheme.displaySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
EmailInput(
|
||||
controller: usernameController,
|
||||
focusNode: emailFocusNode,
|
||||
onSubmit: passwordFocusNode.requestFocus,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PasswordInput(
|
||||
controller: passwordController,
|
||||
focusNode: passwordFocusNode,
|
||||
onSubmit: login,
|
||||
),
|
||||
if (isPasswordLoginEnable.value) ...[
|
||||
const SizedBox(height: 18),
|
||||
EmailInput(
|
||||
controller: usernameController,
|
||||
focusNode: emailFocusNode,
|
||||
onSubmit: passwordFocusNode.requestFocus,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
PasswordInput(
|
||||
controller: passwordController,
|
||||
focusNode: passwordFocusNode,
|
||||
onSubmit: login,
|
||||
),
|
||||
],
|
||||
|
||||
// Note: This used to have an AnimatedSwitcher, but was removed
|
||||
// because of https://github.com/flutter/flutter/issues/120874
|
||||
@@ -295,19 +302,21 @@ class LoginForm extends HookConsumerWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 18),
|
||||
LoginButton(onPressed: login),
|
||||
if (isPasswordLoginEnable.value)
|
||||
LoginButton(onPressed: login),
|
||||
if (isOauthEnable.value) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
if (isPasswordLoginEnable.value)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
),
|
||||
child: Divider(
|
||||
color: Brightness.dark ==
|
||||
Theme.of(context).brightness
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
child: Divider(
|
||||
color:
|
||||
Brightness.dark == Theme.of(context).brightness
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
OAuthLoginButton(
|
||||
serverEndpointController: serverEndpointController,
|
||||
buttonLabel: oAuthButtonLabel.value,
|
||||
@@ -317,6 +326,10 @@ class LoginForm extends HookConsumerWidget {
|
||||
],
|
||||
],
|
||||
),
|
||||
if (!isOauthEnable.value && !isPasswordLoginEnable.value)
|
||||
Center(
|
||||
child: const Text('login_disabled').tr(),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton.icon(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
|
||||
@@ -5,7 +5,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/memories/models/memory.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:openapi/api.dart' as api;
|
||||
|
||||
class MemoryPage extends HookConsumerWidget {
|
||||
final List<Memory> memories;
|
||||
@@ -22,6 +25,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
final memoryPageController = usePageController(initialPage: memoryIndex);
|
||||
final memoryAssetPageController = usePageController();
|
||||
final currentMemory = useState(memories[memoryIndex]);
|
||||
final previousMemoryIndex = useState(memoryIndex);
|
||||
final currentAssetPage = useState(0);
|
||||
final assetProgress = useState(
|
||||
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}",
|
||||
@@ -36,11 +40,16 @@ class MemoryPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
toNextAsset(int currentAssetIndex) {
|
||||
(currentAssetIndex + 1 < currentMemory.value.assets.length)
|
||||
? memoryAssetPageController.jumpToPage(
|
||||
(currentAssetIndex + 1),
|
||||
)
|
||||
: toNextMemory();
|
||||
if (currentAssetIndex + 1 < currentMemory.value.assets.length) {
|
||||
// Go to the next asset
|
||||
memoryAssetPageController.nextPage(
|
||||
curve: Curves.easeInOut,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
);
|
||||
} else {
|
||||
// Go to the next memory since we are at the end of our assets
|
||||
toNextMemory();
|
||||
}
|
||||
}
|
||||
|
||||
updateProgressText() {
|
||||
@@ -48,21 +57,71 @@ class MemoryPage extends HookConsumerWidget {
|
||||
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}";
|
||||
}
|
||||
|
||||
onMemoryChanged(int otherIndex) {
|
||||
HapticFeedback.mediumImpact();
|
||||
currentMemory.value = memories[otherIndex];
|
||||
currentAssetPage.value = 0;
|
||||
updateProgressText();
|
||||
/// Downloads and caches the image for the asset at this [currentMemory]'s index
|
||||
precacheAsset(int index) async {
|
||||
// Guard index out of range
|
||||
if (index < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
late Asset asset;
|
||||
if (index < currentMemory.value.assets.length) {
|
||||
// Uses the next asset in this current memory
|
||||
asset = currentMemory.value.assets[index];
|
||||
} else {
|
||||
// Precache the first asset in the next memory if available
|
||||
final currentMemoryIndex = memories.indexOf(currentMemory.value);
|
||||
|
||||
// Guard no memory found
|
||||
if (currentMemoryIndex == -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
final nextMemoryIndex = currentMemoryIndex + 1;
|
||||
// Guard no next memory
|
||||
if (nextMemoryIndex >= memories.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the first asset from the next memory
|
||||
asset = memories[nextMemoryIndex].assets.first;
|
||||
}
|
||||
|
||||
// Gets the thumbnail url and precaches it
|
||||
final precaches = <Future<dynamic>>[];
|
||||
|
||||
precaches.add(
|
||||
ImmichImage.precacheAsset(
|
||||
asset,
|
||||
context,
|
||||
type: api.ThumbnailFormat.WEBP,
|
||||
),
|
||||
);
|
||||
precaches.add(
|
||||
ImmichImage.precacheAsset(
|
||||
asset,
|
||||
context,
|
||||
type: api.ThumbnailFormat.JPEG,
|
||||
),
|
||||
);
|
||||
|
||||
await Future.wait(precaches);
|
||||
}
|
||||
|
||||
// Precache the next page right away if we are on the first page
|
||||
if (currentAssetPage.value == 0) {
|
||||
Future.delayed(const Duration(milliseconds: 200))
|
||||
.then((_) => precacheAsset(1));
|
||||
}
|
||||
|
||||
onAssetChanged(int otherIndex) {
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
currentAssetPage.value = otherIndex;
|
||||
precacheAsset(otherIndex + 1);
|
||||
updateProgressText();
|
||||
}
|
||||
|
||||
buildBottomInfo() {
|
||||
buildBottomInfo(Memory memory) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Row(
|
||||
@@ -71,7 +130,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
currentMemory.value.title,
|
||||
memory.title,
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
fontSize: 11.0,
|
||||
@@ -80,7 +139,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
),
|
||||
Text(
|
||||
DateFormat.yMMMMd().format(
|
||||
currentMemory.value.assets[0].fileCreatedAt,
|
||||
memory.assets[0].fileCreatedAt,
|
||||
),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
@@ -95,44 +154,66 @@ class MemoryPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
body: SafeArea(
|
||||
child: PageView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: memoryPageController,
|
||||
onPageChanged: onMemoryChanged,
|
||||
itemCount: memories.length,
|
||||
itemBuilder: (context, mIndex) {
|
||||
// Build horizontal page
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: memoryAssetPageController,
|
||||
onPageChanged: onAssetChanged,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: memories[mIndex].assets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final asset = memories[mIndex].assets[index];
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: MemoryCard(
|
||||
asset: asset,
|
||||
onTap: () => toNextAsset(index),
|
||||
onClose: () => AutoRouter.of(context).pop(),
|
||||
rightCornerText: assetProgress.value,
|
||||
title: memories[mIndex].title,
|
||||
showTitle: index == 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
|
||||
* when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final
|
||||
* page during the end of scroll is different than the current page
|
||||
*/
|
||||
return NotificationListener<ScrollNotification>(
|
||||
onNotification: (ScrollNotification notification) {
|
||||
if (notification.depth == 0) {
|
||||
var currentPageNumber = memoryPageController.page!.toInt();
|
||||
currentMemory.value = memories[currentPageNumber];
|
||||
if (notification is ScrollStartNotification) {
|
||||
assetProgress.value = "";
|
||||
} else if (notification is ScrollEndNotification) {
|
||||
HapticFeedback.mediumImpact();
|
||||
if (currentPageNumber != previousMemoryIndex.value) {
|
||||
currentAssetPage.value = 0;
|
||||
previousMemoryIndex.value = currentPageNumber;
|
||||
}
|
||||
updateProgressText();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: bgColor,
|
||||
body: SafeArea(
|
||||
child: PageView.builder(
|
||||
scrollDirection: Axis.vertical,
|
||||
controller: memoryPageController,
|
||||
itemCount: memories.length,
|
||||
itemBuilder: (context, mIndex) {
|
||||
// Build horizontal page
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
controller: memoryAssetPageController,
|
||||
onPageChanged: onAssetChanged,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: memories[mIndex].assets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final asset = memories[mIndex].assets[index];
|
||||
return Container(
|
||||
color: Colors.black,
|
||||
child: MemoryCard(
|
||||
asset: asset,
|
||||
onTap: () => toNextAsset(index),
|
||||
onClose: () => AutoRouter.of(context).pop(),
|
||||
rightCornerText: assetProgress.value,
|
||||
title: memories[mIndex].title,
|
||||
showTitle: index == 0,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
buildBottomInfo(),
|
||||
],
|
||||
);
|
||||
},
|
||||
buildBottomInfo(memories[mIndex]),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,29 +4,40 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class AuthGuard extends AutoRouteGuard {
|
||||
final ApiService _apiService;
|
||||
final _log = Logger("AuthGuard");
|
||||
AuthGuard(this._apiService);
|
||||
@override
|
||||
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
||||
|
||||
resolver.next(true);
|
||||
|
||||
try {
|
||||
var res = await _apiService.authenticationApi.validateAccessToken();
|
||||
if (res != null && res.authStatus) {
|
||||
resolver.next(true);
|
||||
} else {
|
||||
if (res == null || res.authStatus != true) {
|
||||
// If the access token is invalid, take user back to login
|
||||
_log.fine("User token is invalid. Redirecting to login");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
}
|
||||
} on ApiException catch (e) {
|
||||
if (e.code == HttpStatus.badRequest &&
|
||||
e.innerException is SocketException) {
|
||||
// offline?
|
||||
resolver.next(true);
|
||||
_log.fine(
|
||||
"Unable to validate user token. User may be offline and offline browsing is allowed.",
|
||||
);
|
||||
} else {
|
||||
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error [onNavigation] ${e.toString()}");
|
||||
router.replaceAll([const LoginRoute()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ class User {
|
||||
required this.isAdmin,
|
||||
this.isPartnerSharedBy = false,
|
||||
this.isPartnerSharedWith = false,
|
||||
this.profileImagePath = '',
|
||||
});
|
||||
|
||||
Id get isarId => fastHash(id);
|
||||
@@ -28,6 +29,7 @@ class User {
|
||||
lastName = dto.lastName,
|
||||
isPartnerSharedBy = false,
|
||||
isPartnerSharedWith = false,
|
||||
profileImagePath = dto.profileImagePath,
|
||||
isAdmin = dto.isAdmin;
|
||||
|
||||
@Index(unique: true, replace: false, type: IndexType.hash)
|
||||
@@ -39,6 +41,7 @@ class User {
|
||||
bool isPartnerSharedBy;
|
||||
bool isPartnerSharedWith;
|
||||
bool isAdmin;
|
||||
String profileImagePath;
|
||||
@Backlink(to: 'owner')
|
||||
final IsarLinks<Album> albums = IsarLinks<Album>();
|
||||
@Backlink(to: 'sharedUsers')
|
||||
@@ -54,6 +57,7 @@ class User {
|
||||
lastName == other.lastName &&
|
||||
isPartnerSharedBy == other.isPartnerSharedBy &&
|
||||
isPartnerSharedWith == other.isPartnerSharedWith &&
|
||||
profileImagePath == other.profileImagePath &&
|
||||
isAdmin == other.isAdmin;
|
||||
}
|
||||
|
||||
@@ -67,5 +71,6 @@ class User {
|
||||
lastName.hashCode ^
|
||||
isPartnerSharedBy.hashCode ^
|
||||
isPartnerSharedWith.hashCode ^
|
||||
profileImagePath.hashCode ^
|
||||
isAdmin.hashCode;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
@@ -62,6 +64,10 @@ class ApiService {
|
||||
Future<String> _resolveEndpoint(String serverUrl) async {
|
||||
final url = sanitizeUrl(serverUrl);
|
||||
|
||||
if (!await _isEndpointAvailable(serverUrl)) {
|
||||
throw ApiException(503, "Server is not reachable");
|
||||
}
|
||||
|
||||
// Check for /.well-known/immich
|
||||
final wellKnownEndpoint = await _getWellKnownEndpoint(url);
|
||||
if (wellKnownEndpoint.isNotEmpty) return wellKnownEndpoint;
|
||||
@@ -70,6 +76,29 @@ class ApiService {
|
||||
return url;
|
||||
}
|
||||
|
||||
Future<bool> _isEndpointAvailable(String serverUrl) async {
|
||||
final Client client = Client();
|
||||
|
||||
if (!serverUrl.endsWith('/api')) {
|
||||
serverUrl += '/api';
|
||||
}
|
||||
|
||||
// Throw Socket or Timeout exceptions,
|
||||
// we do not care if the endpoints hits an HTTP error
|
||||
try {
|
||||
await client
|
||||
.get(
|
||||
Uri.parse(serverUrl),
|
||||
)
|
||||
.timeout(const Duration(seconds: 5));
|
||||
} on TimeoutException catch (_) {
|
||||
return false;
|
||||
} on SocketException catch (_) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<String> _getWellKnownEndpoint(String baseUrl) async {
|
||||
final Client client = Client();
|
||||
|
||||
|
||||
@@ -16,11 +16,13 @@ class ImmichImage extends StatelessWidget {
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.useGrayBoxPlaceholder = false,
|
||||
this.useProgressIndicator = false,
|
||||
this.type = api.ThumbnailFormat.WEBP,
|
||||
super.key,
|
||||
});
|
||||
final Asset? asset;
|
||||
final bool useGrayBoxPlaceholder;
|
||||
final bool useProgressIndicator;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit fit;
|
||||
@@ -58,17 +60,23 @@ class ImmichImage extends StatelessWidget {
|
||||
if (wasSynchronouslyLoaded || frame != null) {
|
||||
return child;
|
||||
}
|
||||
return (useGrayBoxPlaceholder
|
||||
? const SizedBox.square(
|
||||
|
||||
// Show loading if desired
|
||||
return Stack(
|
||||
children: [
|
||||
if (useGrayBoxPlaceholder)
|
||||
const SizedBox.square(
|
||||
dimension: 250,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: Transform.scale(
|
||||
scale: 0.2,
|
||||
child: const CircularProgressIndicator(),
|
||||
));
|
||||
),
|
||||
if (useProgressIndicator)
|
||||
const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
if (error is PlatformException &&
|
||||
@@ -102,16 +110,27 @@ class ImmichImage extends StatelessWidget {
|
||||
fit: fit,
|
||||
fadeInDuration: const Duration(milliseconds: 250),
|
||||
progressIndicatorBuilder: (context, url, downloadProgress) {
|
||||
if (useGrayBoxPlaceholder) {
|
||||
return const DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
);
|
||||
}
|
||||
return Transform.scale(
|
||||
scale: 0.2,
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
value: downloadProgress.progress,
|
||||
),
|
||||
// Show loading if desired
|
||||
return Stack(
|
||||
children: [
|
||||
if (useGrayBoxPlaceholder)
|
||||
const SizedBox.square(
|
||||
dimension: 250,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(color: Colors.grey),
|
||||
),
|
||||
),
|
||||
if (useProgressIndicator)
|
||||
Transform.scale(
|
||||
scale: 2,
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 1,
|
||||
value: downloadProgress.progress,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
errorWidget: (context, url, error) {
|
||||
@@ -128,4 +147,46 @@ class ImmichImage extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Precaches this asset for instant load the next time it is shown
|
||||
static Future<void> precacheAsset(
|
||||
Asset asset,
|
||||
BuildContext context, {
|
||||
type = api.ThumbnailFormat.WEBP,
|
||||
}) {
|
||||
final authToken = 'Bearer ${Store.get(StoreKey.accessToken)}';
|
||||
|
||||
if (type == api.ThumbnailFormat.WEBP) {
|
||||
final thumbnailUrl = getThumbnailUrl(asset);
|
||||
final thumbnailCacheKey = getThumbnailCacheKey(asset);
|
||||
final thumbnailProvider = CachedNetworkImageProvider(
|
||||
thumbnailUrl,
|
||||
cacheKey: thumbnailCacheKey,
|
||||
headers: {"Authorization": authToken},
|
||||
);
|
||||
return precacheImage(thumbnailProvider, context);
|
||||
}
|
||||
// Precache the local image
|
||||
if (!asset.isRemote &&
|
||||
(asset.isLocal || !Store.get(StoreKey.preferRemoteImage, false))) {
|
||||
final provider = AssetEntityImageProvider(
|
||||
asset.local!,
|
||||
isOriginal: false,
|
||||
thumbnailSize: const ThumbnailSize.square(250), // like server thumbs
|
||||
);
|
||||
return precacheImage(provider, context);
|
||||
} else {
|
||||
// Precache the remote image since we are not using local images
|
||||
final url = getThumbnailUrl(asset, type: api.ThumbnailFormat.JPEG);
|
||||
final cacheKey =
|
||||
getThumbnailCacheKey(asset, type: api.ThumbnailFormat.JPEG);
|
||||
final provider = CachedNetworkImageProvider(
|
||||
url,
|
||||
cacheKey: cacheKey,
|
||||
headers: {"Authorization": authToken},
|
||||
);
|
||||
|
||||
return precacheImage(provider, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.pr
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/api.provider.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
class SplashScreenPage extends HookConsumerWidget {
|
||||
const SplashScreenPage({Key? key}) : super(key: key);
|
||||
@@ -17,24 +19,41 @@ class SplashScreenPage extends HookConsumerWidget {
|
||||
final apiService = ref.watch(apiServiceProvider);
|
||||
final serverUrl = Store.tryGet(StoreKey.serverUrl);
|
||||
final accessToken = Store.tryGet(StoreKey.accessToken);
|
||||
final log = Logger("SplashScreenPage");
|
||||
|
||||
void performLoggingIn() async {
|
||||
bool isSuccess = false;
|
||||
bool deviceIsOffline = false;
|
||||
if (accessToken != null && serverUrl != null) {
|
||||
try {
|
||||
// Resolve API server endpoint from user provided serverUrl
|
||||
await apiService.resolveAndSetEndpoint(serverUrl);
|
||||
} catch (e) {
|
||||
} on ApiException catch (e) {
|
||||
// okay, try to continue anyway if offline
|
||||
if (e.code == 503) {
|
||||
deviceIsOffline = true;
|
||||
log.fine("Device seems to be offline upon launch");
|
||||
} else {
|
||||
log.severe(e);
|
||||
}
|
||||
} catch (e) {
|
||||
log.severe(e);
|
||||
}
|
||||
|
||||
isSuccess =
|
||||
await ref.read(authenticationProvider.notifier).setSuccessLoginInfo(
|
||||
accessToken: accessToken,
|
||||
serverUrl: serverUrl,
|
||||
offlineLogin: deviceIsOffline,
|
||||
);
|
||||
}
|
||||
if (isSuccess) {
|
||||
|
||||
// If the device is offline and there is a currentUser stored locallly
|
||||
// Proceed into the app
|
||||
if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) {
|
||||
AutoRouter.of(context).replace(const TabControllerRoute());
|
||||
} else if (isSuccess) {
|
||||
// If device was able to login through the internet successfully
|
||||
final hasPermission =
|
||||
await ref.read(galleryPermissionNotifier.notifier).hasPermission;
|
||||
if (hasPermission) {
|
||||
@@ -43,6 +62,7 @@ class SplashScreenPage extends HookConsumerWidget {
|
||||
}
|
||||
AutoRouter.of(context).replace(const TabControllerRoute());
|
||||
} else {
|
||||
// User was unable to login through either offline or online methods
|
||||
AutoRouter.of(context).replace(const LoginRoute());
|
||||
}
|
||||
}
|
||||
|
||||
6
mobile/openapi/.openapi-generator/FILES
generated
6
mobile/openapi/.openapi-generator/FILES
generated
@@ -72,6 +72,8 @@ doc/OAuthConfigDto.md
|
||||
doc/OAuthConfigResponseDto.md
|
||||
doc/PartnerApi.md
|
||||
doc/PeopleResponseDto.md
|
||||
doc/PeopleUpdateDto.md
|
||||
doc/PeopleUpdateItem.md
|
||||
doc/PersonApi.md
|
||||
doc/PersonResponseDto.md
|
||||
doc/PersonUpdateDto.md
|
||||
@@ -210,6 +212,8 @@ lib/model/o_auth_callback_dto.dart
|
||||
lib/model/o_auth_config_dto.dart
|
||||
lib/model/o_auth_config_response_dto.dart
|
||||
lib/model/people_response_dto.dart
|
||||
lib/model/people_update_dto.dart
|
||||
lib/model/people_update_item.dart
|
||||
lib/model/person_response_dto.dart
|
||||
lib/model/person_update_dto.dart
|
||||
lib/model/queue_status_dto.dart
|
||||
@@ -325,6 +329,8 @@ test/o_auth_config_dto_test.dart
|
||||
test/o_auth_config_response_dto_test.dart
|
||||
test/partner_api_test.dart
|
||||
test/people_response_dto_test.dart
|
||||
test/people_update_dto_test.dart
|
||||
test/people_update_item_test.dart
|
||||
test/person_api_test.dart
|
||||
test/person_response_dto_test.dart
|
||||
test/person_update_dto_test.dart
|
||||
|
||||
5
mobile/openapi/README.md
generated
5
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.68.0
|
||||
- API version: 1.71.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
## Requirements
|
||||
@@ -134,6 +134,7 @@ Class | Method | HTTP request | Description
|
||||
*PersonApi* | [**getPersonAssets**](doc//PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
|
||||
*PersonApi* | [**getPersonThumbnail**](doc//PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
|
||||
*PersonApi* | [**mergePerson**](doc//PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
|
||||
*PersonApi* | [**updatePeople**](doc//PersonApi.md#updatepeople) | **PUT** /person |
|
||||
*PersonApi* | [**updatePerson**](doc//PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
|
||||
*SearchApi* | [**getSearchConfig**](doc//SearchApi.md#getsearchconfig) | **GET** /search/config |
|
||||
@@ -239,6 +240,8 @@ Class | Method | HTTP request | Description
|
||||
- [OAuthConfigDto](doc//OAuthConfigDto.md)
|
||||
- [OAuthConfigResponseDto](doc//OAuthConfigResponseDto.md)
|
||||
- [PeopleResponseDto](doc//PeopleResponseDto.md)
|
||||
- [PeopleUpdateDto](doc//PeopleUpdateDto.md)
|
||||
- [PeopleUpdateItem](doc//PeopleUpdateItem.md)
|
||||
- [PersonResponseDto](doc//PersonResponseDto.md)
|
||||
- [PersonUpdateDto](doc//PersonUpdateDto.md)
|
||||
- [QueueStatusDto](doc//QueueStatusDto.md)
|
||||
|
||||
1
mobile/openapi/doc/ExifResponseDto.md
generated
1
mobile/openapi/doc/ExifResponseDto.md
generated
@@ -28,6 +28,7 @@ Name | Type | Description | Notes
|
||||
**state** | **String** | | [optional]
|
||||
**country** | **String** | | [optional]
|
||||
**description** | **String** | | [optional]
|
||||
**projectionType** | **String** | | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
15
mobile/openapi/doc/PeopleUpdateDto.md
generated
Normal file
15
mobile/openapi/doc/PeopleUpdateDto.md
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
# openapi.model.PeopleUpdateDto
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**people** | [**List<PeopleUpdateItem>**](PeopleUpdateItem.md) | | [default to const []]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
18
mobile/openapi/doc/PeopleUpdateItem.md
generated
Normal file
18
mobile/openapi/doc/PeopleUpdateItem.md
generated
Normal file
@@ -0,0 +1,18 @@
|
||||
# openapi.model.PeopleUpdateItem
|
||||
|
||||
## Load the model package
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
```
|
||||
|
||||
## Properties
|
||||
Name | Type | Description | Notes
|
||||
------------ | ------------- | ------------- | -------------
|
||||
**id** | **String** | Person id. |
|
||||
**name** | **String** | Person name. | [optional]
|
||||
**featureFaceAssetId** | **String** | Asset is used to get the feature face thumbnail. | [optional]
|
||||
**isHidden** | **bool** | Person visibility | [optional]
|
||||
|
||||
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)
|
||||
|
||||
|
||||
56
mobile/openapi/doc/PersonApi.md
generated
56
mobile/openapi/doc/PersonApi.md
generated
@@ -14,6 +14,7 @@ Method | HTTP request | Description
|
||||
[**getPersonAssets**](PersonApi.md#getpersonassets) | **GET** /person/{id}/assets |
|
||||
[**getPersonThumbnail**](PersonApi.md#getpersonthumbnail) | **GET** /person/{id}/thumbnail |
|
||||
[**mergePerson**](PersonApi.md#mergeperson) | **POST** /person/{id}/merge |
|
||||
[**updatePeople**](PersonApi.md#updatepeople) | **PUT** /person |
|
||||
[**updatePerson**](PersonApi.md#updateperson) | **PUT** /person/{id} |
|
||||
|
||||
|
||||
@@ -294,6 +295,61 @@ Name | Type | Description | Notes
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **updatePeople**
|
||||
> List<BulkIdResponseDto> updatePeople(peopleUpdateDto)
|
||||
|
||||
|
||||
|
||||
### Example
|
||||
```dart
|
||||
import 'package:openapi/api.dart';
|
||||
// TODO Configure API key authorization: cookie
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('cookie').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure API key authorization: api_key
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKey = 'YOUR_API_KEY';
|
||||
// uncomment below to setup prefix (e.g. Bearer) for API key, if needed
|
||||
//defaultApiClient.getAuthentication<ApiKeyAuth>('api_key').apiKeyPrefix = 'Bearer';
|
||||
// TODO Configure HTTP Bearer authorization: bearer
|
||||
// Case 1. Use String Token
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken('YOUR_ACCESS_TOKEN');
|
||||
// Case 2. Use Function which generate token.
|
||||
// String yourTokenGeneratorFunction() { ... }
|
||||
//defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
|
||||
|
||||
final api_instance = PersonApi();
|
||||
final peopleUpdateDto = PeopleUpdateDto(); // PeopleUpdateDto |
|
||||
|
||||
try {
|
||||
final result = api_instance.updatePeople(peopleUpdateDto);
|
||||
print(result);
|
||||
} catch (e) {
|
||||
print('Exception when calling PersonApi->updatePeople: $e\n');
|
||||
}
|
||||
```
|
||||
|
||||
### Parameters
|
||||
|
||||
Name | Type | Description | Notes
|
||||
------------- | ------------- | ------------- | -------------
|
||||
**peopleUpdateDto** | [**PeopleUpdateDto**](PeopleUpdateDto.md)| |
|
||||
|
||||
### Return type
|
||||
|
||||
[**List<BulkIdResponseDto>**](BulkIdResponseDto.md)
|
||||
|
||||
### Authorization
|
||||
|
||||
[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer)
|
||||
|
||||
### HTTP request headers
|
||||
|
||||
- **Content-Type**: application/json
|
||||
- **Accept**: application/json
|
||||
|
||||
[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
|
||||
|
||||
# **updatePerson**
|
||||
> PersonResponseDto updatePerson(id, personUpdateDto)
|
||||
|
||||
|
||||
2
mobile/openapi/lib/api.dart
generated
2
mobile/openapi/lib/api.dart
generated
@@ -105,6 +105,8 @@ part 'model/o_auth_callback_dto.dart';
|
||||
part 'model/o_auth_config_dto.dart';
|
||||
part 'model/o_auth_config_response_dto.dart';
|
||||
part 'model/people_response_dto.dart';
|
||||
part 'model/people_update_dto.dart';
|
||||
part 'model/people_update_item.dart';
|
||||
part 'model/person_response_dto.dart';
|
||||
part 'model/person_update_dto.dart';
|
||||
part 'model/queue_status_dto.dart';
|
||||
|
||||
50
mobile/openapi/lib/api/person_api.dart
generated
50
mobile/openapi/lib/api/person_api.dart
generated
@@ -269,6 +269,56 @@ class PersonApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /person' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [PeopleUpdateDto] peopleUpdateDto (required):
|
||||
Future<Response> updatePeopleWithHttpInfo(PeopleUpdateDto peopleUpdateDto,) async {
|
||||
// ignore: prefer_const_declarations
|
||||
final path = r'/person';
|
||||
|
||||
// ignore: prefer_final_locals
|
||||
Object? postBody = peopleUpdateDto;
|
||||
|
||||
final queryParams = <QueryParam>[];
|
||||
final headerParams = <String, String>{};
|
||||
final formParams = <String, String>{};
|
||||
|
||||
const contentTypes = <String>['application/json'];
|
||||
|
||||
|
||||
return apiClient.invokeAPI(
|
||||
path,
|
||||
'PUT',
|
||||
queryParams,
|
||||
postBody,
|
||||
headerParams,
|
||||
formParams,
|
||||
contentTypes.isEmpty ? null : contentTypes.first,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [PeopleUpdateDto] peopleUpdateDto (required):
|
||||
Future<List<BulkIdResponseDto>?> updatePeople(PeopleUpdateDto peopleUpdateDto,) async {
|
||||
final response = await updatePeopleWithHttpInfo(peopleUpdateDto,);
|
||||
if (response.statusCode >= HttpStatus.badRequest) {
|
||||
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
|
||||
}
|
||||
// When a remote server returns no body with a status of 204, we shall not decode it.
|
||||
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
|
||||
// FormatException when trying to decode an empty string.
|
||||
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
|
||||
final responseBody = await _decodeBodyBytes(response);
|
||||
return (await apiClient.deserializeAsync(responseBody, 'List<BulkIdResponseDto>') as List)
|
||||
.cast<BulkIdResponseDto>()
|
||||
.toList();
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'PUT /person/{id}' operation and returns the [Response].
|
||||
/// Parameters:
|
||||
///
|
||||
|
||||
4
mobile/openapi/lib/api_client.dart
generated
4
mobile/openapi/lib/api_client.dart
generated
@@ -305,6 +305,10 @@ class ApiClient {
|
||||
return OAuthConfigResponseDto.fromJson(value);
|
||||
case 'PeopleResponseDto':
|
||||
return PeopleResponseDto.fromJson(value);
|
||||
case 'PeopleUpdateDto':
|
||||
return PeopleUpdateDto.fromJson(value);
|
||||
case 'PeopleUpdateItem':
|
||||
return PeopleUpdateItem.fromJson(value);
|
||||
case 'PersonResponseDto':
|
||||
return PersonResponseDto.fromJson(value);
|
||||
case 'PersonUpdateDto':
|
||||
|
||||
17
mobile/openapi/lib/model/exif_response_dto.dart
generated
17
mobile/openapi/lib/model/exif_response_dto.dart
generated
@@ -33,6 +33,7 @@ class ExifResponseDto {
|
||||
this.state,
|
||||
this.country,
|
||||
this.description,
|
||||
this.projectionType,
|
||||
});
|
||||
|
||||
int? fileSizeInByte;
|
||||
@@ -75,6 +76,8 @@ class ExifResponseDto {
|
||||
|
||||
String? description;
|
||||
|
||||
String? projectionType;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto &&
|
||||
other.fileSizeInByte == fileSizeInByte &&
|
||||
@@ -96,7 +99,8 @@ class ExifResponseDto {
|
||||
other.city == city &&
|
||||
other.state == state &&
|
||||
other.country == country &&
|
||||
other.description == description;
|
||||
other.description == description &&
|
||||
other.projectionType == projectionType;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
@@ -120,10 +124,11 @@ class ExifResponseDto {
|
||||
(city == null ? 0 : city!.hashCode) +
|
||||
(state == null ? 0 : state!.hashCode) +
|
||||
(country == null ? 0 : country!.hashCode) +
|
||||
(description == null ? 0 : description!.hashCode);
|
||||
(description == null ? 0 : description!.hashCode) +
|
||||
(projectionType == null ? 0 : projectionType!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country, description=$description]';
|
||||
String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country, description=$description, projectionType=$projectionType]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
@@ -227,6 +232,11 @@ class ExifResponseDto {
|
||||
} else {
|
||||
// json[r'description'] = null;
|
||||
}
|
||||
if (this.projectionType != null) {
|
||||
json[r'projectionType'] = this.projectionType;
|
||||
} else {
|
||||
// json[r'projectionType'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
@@ -272,6 +282,7 @@ class ExifResponseDto {
|
||||
state: mapValueOfType<String>(json, r'state'),
|
||||
country: mapValueOfType<String>(json, r'country'),
|
||||
description: mapValueOfType<String>(json, r'description'),
|
||||
projectionType: mapValueOfType<String>(json, r'projectionType'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
|
||||
98
mobile/openapi/lib/model/people_update_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/people_update_dto.dart
generated
Normal file
@@ -0,0 +1,98 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PeopleUpdateDto {
|
||||
/// Returns a new [PeopleUpdateDto] instance.
|
||||
PeopleUpdateDto({
|
||||
this.people = const [],
|
||||
});
|
||||
|
||||
List<PeopleUpdateItem> people;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateDto &&
|
||||
other.people == people;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(people.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PeopleUpdateDto[people=$people]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'people'] = this.people;
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PeopleUpdateDto] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PeopleUpdateDto? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PeopleUpdateDto(
|
||||
people: PeopleUpdateItem.listFromJson(json[r'people']),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PeopleUpdateDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PeopleUpdateDto>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PeopleUpdateDto.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PeopleUpdateDto> mapFromJson(dynamic json) {
|
||||
final map = <String, PeopleUpdateDto>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PeopleUpdateDto.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PeopleUpdateDto-objects as value to a dart map
|
||||
static Map<String, List<PeopleUpdateDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PeopleUpdateDto>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PeopleUpdateDto.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'people',
|
||||
};
|
||||
}
|
||||
|
||||
153
mobile/openapi/lib/model/people_update_item.dart
generated
Normal file
153
mobile/openapi/lib/model/people_update_item.dart
generated
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
part of openapi.api;
|
||||
|
||||
class PeopleUpdateItem {
|
||||
/// Returns a new [PeopleUpdateItem] instance.
|
||||
PeopleUpdateItem({
|
||||
required this.id,
|
||||
this.name,
|
||||
this.featureFaceAssetId,
|
||||
this.isHidden,
|
||||
});
|
||||
|
||||
/// Person id.
|
||||
String id;
|
||||
|
||||
/// Person name.
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? name;
|
||||
|
||||
/// Asset is used to get the feature face thumbnail.
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
String? featureFaceAssetId;
|
||||
|
||||
/// Person visibility
|
||||
///
|
||||
/// Please note: This property should have been non-nullable! Since the specification file
|
||||
/// does not include a default value (using the "default:" property), however, the generated
|
||||
/// source code must fall back to having a nullable type.
|
||||
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||
///
|
||||
bool? isHidden;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => identical(this, other) || other is PeopleUpdateItem &&
|
||||
other.id == id &&
|
||||
other.name == name &&
|
||||
other.featureFaceAssetId == featureFaceAssetId &&
|
||||
other.isHidden == isHidden;
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
// ignore: unnecessary_parenthesis
|
||||
(id.hashCode) +
|
||||
(name == null ? 0 : name!.hashCode) +
|
||||
(featureFaceAssetId == null ? 0 : featureFaceAssetId!.hashCode) +
|
||||
(isHidden == null ? 0 : isHidden!.hashCode);
|
||||
|
||||
@override
|
||||
String toString() => 'PeopleUpdateItem[id=$id, name=$name, featureFaceAssetId=$featureFaceAssetId, isHidden=$isHidden]';
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final json = <String, dynamic>{};
|
||||
json[r'id'] = this.id;
|
||||
if (this.name != null) {
|
||||
json[r'name'] = this.name;
|
||||
} else {
|
||||
// json[r'name'] = null;
|
||||
}
|
||||
if (this.featureFaceAssetId != null) {
|
||||
json[r'featureFaceAssetId'] = this.featureFaceAssetId;
|
||||
} else {
|
||||
// json[r'featureFaceAssetId'] = null;
|
||||
}
|
||||
if (this.isHidden != null) {
|
||||
json[r'isHidden'] = this.isHidden;
|
||||
} else {
|
||||
// json[r'isHidden'] = null;
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
/// Returns a new [PeopleUpdateItem] instance and imports its values from
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static PeopleUpdateItem? fromJson(dynamic value) {
|
||||
if (value is Map) {
|
||||
final json = value.cast<String, dynamic>();
|
||||
|
||||
return PeopleUpdateItem(
|
||||
id: mapValueOfType<String>(json, r'id')!,
|
||||
name: mapValueOfType<String>(json, r'name'),
|
||||
featureFaceAssetId: mapValueOfType<String>(json, r'featureFaceAssetId'),
|
||||
isHidden: mapValueOfType<bool>(json, r'isHidden'),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static List<PeopleUpdateItem> listFromJson(dynamic json, {bool growable = false,}) {
|
||||
final result = <PeopleUpdateItem>[];
|
||||
if (json is List && json.isNotEmpty) {
|
||||
for (final row in json) {
|
||||
final value = PeopleUpdateItem.fromJson(row);
|
||||
if (value != null) {
|
||||
result.add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toList(growable: growable);
|
||||
}
|
||||
|
||||
static Map<String, PeopleUpdateItem> mapFromJson(dynamic json) {
|
||||
final map = <String, PeopleUpdateItem>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||
for (final entry in json.entries) {
|
||||
final value = PeopleUpdateItem.fromJson(entry.value);
|
||||
if (value != null) {
|
||||
map[entry.key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
// maps a json object with a list of PeopleUpdateItem-objects as value to a dart map
|
||||
static Map<String, List<PeopleUpdateItem>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||
final map = <String, List<PeopleUpdateItem>>{};
|
||||
if (json is Map && json.isNotEmpty) {
|
||||
// ignore: parameter_assignments
|
||||
json = json.cast<String, dynamic>();
|
||||
for (final entry in json.entries) {
|
||||
map[entry.key] = PeopleUpdateItem.listFromJson(entry.value, growable: growable,);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
/// The list of required keys that must be present in a JSON.
|
||||
static const requiredKeys = <String>{
|
||||
'id',
|
||||
};
|
||||
}
|
||||
|
||||
5
mobile/openapi/test/exif_response_dto_test.dart
generated
5
mobile/openapi/test/exif_response_dto_test.dart
generated
@@ -116,6 +116,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// String projectionType
|
||||
test('to test the property `projectionType`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
||||
27
mobile/openapi/test/people_update_dto_test.dart
generated
Normal file
27
mobile/openapi/test/people_update_dto_test.dart
generated
Normal file
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for PeopleUpdateDto
|
||||
void main() {
|
||||
// final instance = PeopleUpdateDto();
|
||||
|
||||
group('test PeopleUpdateDto', () {
|
||||
// List<PeopleUpdateItem> people (default value: const [])
|
||||
test('to test the property `people`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
46
mobile/openapi/test/people_update_item_test.dart
generated
Normal file
46
mobile/openapi/test/people_update_item_test.dart
generated
Normal file
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||
//
|
||||
// @dart=2.12
|
||||
|
||||
// ignore_for_file: unused_element, unused_import
|
||||
// ignore_for_file: always_put_required_named_parameters_first
|
||||
// ignore_for_file: constant_identifier_names
|
||||
// ignore_for_file: lines_longer_than_80_chars
|
||||
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
// tests for PeopleUpdateItem
|
||||
void main() {
|
||||
// final instance = PeopleUpdateItem();
|
||||
|
||||
group('test PeopleUpdateItem', () {
|
||||
// Person id.
|
||||
// String id
|
||||
test('to test the property `id`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// Person name.
|
||||
// String name
|
||||
test('to test the property `name`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// Asset is used to get the feature face thumbnail.
|
||||
// String featureFaceAssetId
|
||||
test('to test the property `featureFaceAssetId`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
// Person visibility
|
||||
// bool isHidden
|
||||
test('to test the property `isHidden`', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
5
mobile/openapi/test/person_api_test.dart
generated
5
mobile/openapi/test/person_api_test.dart
generated
@@ -42,6 +42,11 @@ void main() {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<List<BulkIdResponseDto>> updatePeople(PeopleUpdateDto peopleUpdateDto) async
|
||||
test('test updatePeople', () async {
|
||||
// TODO
|
||||
});
|
||||
|
||||
//Future<PersonResponseDto> updatePerson(String id, PersonUpdateDto personUpdateDto) async
|
||||
test('test updatePerson', () async {
|
||||
// TODO
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: "none"
|
||||
version: 1.68.0+91
|
||||
version: 1.71.0+94
|
||||
isar_version: &isar_version 3.1.0+1
|
||||
|
||||
environment:
|
||||
|
||||
@@ -22,7 +22,7 @@ ENV NODE_ENV=production
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
RUN apk add --no-cache ffmpeg imagemagick-dev libraw-dev perl vips-dev vips-heif vips-jxl vips-magick
|
||||
RUN apk add --no-cache ffmpeg imagemagick-dev libraw-dev perl tini vips-dev vips-heif vips-jxl vips-magick
|
||||
|
||||
COPY --from=prod /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=prod /usr/src/app/dist ./dist
|
||||
@@ -39,4 +39,4 @@ VOLUME /usr/src/app/upload
|
||||
|
||||
EXPOSE 3001
|
||||
|
||||
ENTRYPOINT ["/bin/sh"]
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
|
||||
@@ -2546,6 +2546,49 @@
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"operationId": "updatePeople",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/PeopleUpdateDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BulkIdResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Person"
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"bearer": []
|
||||
},
|
||||
{
|
||||
"cookie": []
|
||||
},
|
||||
{
|
||||
"api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/person/{id}": {
|
||||
@@ -4396,7 +4439,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "1.68.0",
|
||||
"version": "1.71.0",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [],
|
||||
@@ -5028,13 +5071,13 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"error": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"duplicate",
|
||||
"no_permission",
|
||||
"not_found",
|
||||
"unknown"
|
||||
]
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -5510,6 +5553,11 @@
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
},
|
||||
"projectionType": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -5906,6 +5954,44 @@
|
||||
"people"
|
||||
]
|
||||
},
|
||||
"PeopleUpdateDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"people": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PeopleUpdateItem"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"people"
|
||||
]
|
||||
},
|
||||
"PeopleUpdateItem": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Person id."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Person name."
|
||||
},
|
||||
"featureFaceAssetId": {
|
||||
"type": "string",
|
||||
"description": "Asset is used to get the feature face thumbnail."
|
||||
},
|
||||
"isHidden": {
|
||||
"type": "boolean",
|
||||
"description": "Person visibility"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"PersonResponseDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
93
server/package-lock.json
generated
93
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.68.0",
|
||||
"version": "1.71.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "immich",
|
||||
"version": "1.68.0",
|
||||
"version": "1.71.0",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.13",
|
||||
@@ -28,12 +28,12 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"exiftool-vendored": "^19.0.0",
|
||||
"exiftool-vendored": "^22.0.0",
|
||||
"exiftool-vendored.pl": "^12.54.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"immich": "^0.39.0",
|
||||
"immich": "^0.40.1",
|
||||
"ioredis": "^5.3.1",
|
||||
"joi": "^17.5.0",
|
||||
"local-reverse-geocoder": "0.12.5",
|
||||
@@ -4064,9 +4064,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/batch-cluster": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-11.0.0.tgz",
|
||||
"integrity": "sha512-8iwqa+rKTaakOHkqdcXDT5L5117pa+FoP8/yAKpNdL44ZnC4V2NEA/sIg0ZO0O9NkpdjLk0A3efRFM5nVizqHw==",
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz",
|
||||
"integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
@@ -5976,25 +5976,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-19.0.0.tgz",
|
||||
"integrity": "sha512-Zes7TZrYWxts92mbF2Gs3drtWZucm4qsaeYaE6A+OOqmeD9UGaGisqIbyh9MilJrLi+ZHzWEJZtDj37QFf6xsA==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-22.0.0.tgz",
|
||||
"integrity": "sha512-gBOQ4C2GLjxKPDPRuUbMOz91mG6IFA22L+Z/IQzFotFu20vc7YroqHALf/ophCbANA5sNSArbVDPijP7n/20Jg==",
|
||||
"dependencies": {
|
||||
"@photostructure/tz-lookup": "^7.0.0",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"batch-cluster": "^11.0.0",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"batch-cluster": "^12.1.0",
|
||||
"he": "^1.2.0",
|
||||
"luxon": "^3.2.1"
|
||||
"luxon": "^3.3.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"exiftool-vendored.exe": "12.54.0",
|
||||
"exiftool-vendored.pl": "12.54.0"
|
||||
"exiftool-vendored.exe": "12.62.0",
|
||||
"exiftool-vendored.pl": "12.62.0"
|
||||
}
|
||||
},
|
||||
"node_modules/exiftool-vendored.exe": {
|
||||
"version": "12.54.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.54.0.tgz",
|
||||
"integrity": "sha512-Dc4W6e0NtQfYuJIYK4piHfDJnd2jvA04e0aaq9R3Q1oO34KC5e+L1D2C7lFuZXqPQLYC1x3GYc/GVv5e+SkkrQ==",
|
||||
"version": "12.62.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.62.0.tgz",
|
||||
"integrity": "sha512-xNFkvmjwnMg6ivtmkc67w1qD23fIy06nRpMpGuBpTwTqAVatHV+vk7T75zyvLoXRRpd1rKID9XAVLGJCE/iiMQ==",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
@@ -6008,15 +6008,6 @@
|
||||
"!win32"
|
||||
]
|
||||
},
|
||||
"node_modules/exiftool-vendored/node_modules/exiftool-vendored.pl": {
|
||||
"version": "12.54.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.54.0.tgz",
|
||||
"integrity": "sha512-RBBowsYcM6EvbWoBkg2dOqHpH3WIzN7bIzHc+o+LquqCTo3doZwECClD/6PNHVSMQsl2Z0fEf75sNq2msooMSg==",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"!win32"
|
||||
]
|
||||
},
|
||||
"node_modules/exit": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
|
||||
@@ -7003,9 +6994,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immich": {
|
||||
"version": "0.39.0",
|
||||
"resolved": "https://registry.npmjs.org/immich/-/immich-0.39.0.tgz",
|
||||
"integrity": "sha512-FoIj/ZV7QrjuBC7F6o6YZ8jqLZDJCZwrr80CxkzERPI7qX8YrSjR1GM4ocA/9oT7p7iA+dIxT//BF5MKNPkn4g==",
|
||||
"version": "0.40.1",
|
||||
"resolved": "https://registry.npmjs.org/immich/-/immich-0.40.1.tgz",
|
||||
"integrity": "sha512-pU0Ua+FAsOiqrPC8NbSA521QW0k56Sw0GZ5rrPyqEMb2dcYPDOqEFcEk/1INqoQpPxy+CF9ZOCHNWxsEc7L1Rw==",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.0",
|
||||
"chalk": "^2.4.1",
|
||||
@@ -15584,9 +15575,9 @@
|
||||
"integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="
|
||||
},
|
||||
"batch-cluster": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-11.0.0.tgz",
|
||||
"integrity": "sha512-8iwqa+rKTaakOHkqdcXDT5L5117pa+FoP8/yAKpNdL44ZnC4V2NEA/sIg0ZO0O9NkpdjLk0A3efRFM5nVizqHw=="
|
||||
"version": "12.1.0",
|
||||
"resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz",
|
||||
"integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg=="
|
||||
},
|
||||
"bcrypt": {
|
||||
"version": "5.1.0",
|
||||
@@ -17011,31 +17002,23 @@
|
||||
}
|
||||
},
|
||||
"exiftool-vendored": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-19.0.0.tgz",
|
||||
"integrity": "sha512-Zes7TZrYWxts92mbF2Gs3drtWZucm4qsaeYaE6A+OOqmeD9UGaGisqIbyh9MilJrLi+ZHzWEJZtDj37QFf6xsA==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-22.0.0.tgz",
|
||||
"integrity": "sha512-gBOQ4C2GLjxKPDPRuUbMOz91mG6IFA22L+Z/IQzFotFu20vc7YroqHALf/ophCbANA5sNSArbVDPijP7n/20Jg==",
|
||||
"requires": {
|
||||
"@photostructure/tz-lookup": "^7.0.0",
|
||||
"@types/luxon": "^3.2.0",
|
||||
"batch-cluster": "^11.0.0",
|
||||
"exiftool-vendored.exe": "12.54.0",
|
||||
"exiftool-vendored.pl": "12.54.0",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"batch-cluster": "^12.1.0",
|
||||
"exiftool-vendored.exe": "12.62.0",
|
||||
"exiftool-vendored.pl": "12.62.0",
|
||||
"he": "^1.2.0",
|
||||
"luxon": "^3.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"exiftool-vendored.pl": {
|
||||
"version": "12.54.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.54.0.tgz",
|
||||
"integrity": "sha512-RBBowsYcM6EvbWoBkg2dOqHpH3WIzN7bIzHc+o+LquqCTo3doZwECClD/6PNHVSMQsl2Z0fEf75sNq2msooMSg==",
|
||||
"optional": true
|
||||
}
|
||||
"luxon": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"exiftool-vendored.exe": {
|
||||
"version": "12.54.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.54.0.tgz",
|
||||
"integrity": "sha512-Dc4W6e0NtQfYuJIYK4piHfDJnd2jvA04e0aaq9R3Q1oO34KC5e+L1D2C7lFuZXqPQLYC1x3GYc/GVv5e+SkkrQ==",
|
||||
"version": "12.62.0",
|
||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.62.0.tgz",
|
||||
"integrity": "sha512-xNFkvmjwnMg6ivtmkc67w1qD23fIy06nRpMpGuBpTwTqAVatHV+vk7T75zyvLoXRRpd1rKID9XAVLGJCE/iiMQ==",
|
||||
"optional": true
|
||||
},
|
||||
"exiftool-vendored.pl": {
|
||||
@@ -17786,9 +17769,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"immich": {
|
||||
"version": "0.39.0",
|
||||
"resolved": "https://registry.npmjs.org/immich/-/immich-0.39.0.tgz",
|
||||
"integrity": "sha512-FoIj/ZV7QrjuBC7F6o6YZ8jqLZDJCZwrr80CxkzERPI7qX8YrSjR1GM4ocA/9oT7p7iA+dIxT//BF5MKNPkn4g==",
|
||||
"version": "0.40.1",
|
||||
"resolved": "https://registry.npmjs.org/immich/-/immich-0.40.1.tgz",
|
||||
"integrity": "sha512-pU0Ua+FAsOiqrPC8NbSA521QW0k56Sw0GZ5rrPyqEMb2dcYPDOqEFcEk/1INqoQpPxy+CF9ZOCHNWxsEc7L1Rw==",
|
||||
"requires": {
|
||||
"axios": "^0.26.0",
|
||||
"chalk": "^2.4.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.68.0",
|
||||
"version": "1.71.0",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -58,12 +58,12 @@
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"exiftool-vendored": "^19.0.0",
|
||||
"exiftool-vendored": "^22.0.0",
|
||||
"exiftool-vendored.pl": "^12.54.0",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
"handlebars": "^4.7.7",
|
||||
"i18n-iso-countries": "^7.5.0",
|
||||
"immich": "^0.39.0",
|
||||
"immich": "^0.40.1",
|
||||
"ioredis": "^5.3.1",
|
||||
"joi": "^17.5.0",
|
||||
"local-reverse-geocoder": "0.12.5",
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface AssetStatsOptions {
|
||||
export interface AssetSearchOptions {
|
||||
isVisible?: boolean;
|
||||
type?: AssetType;
|
||||
order?: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
export interface LivePhotoSearchOptions {
|
||||
|
||||
@@ -24,6 +24,7 @@ export class ExifResponseDto {
|
||||
state?: string | null = null;
|
||||
country?: string | null = null;
|
||||
description?: string | null = null;
|
||||
projectionType?: string | null = null;
|
||||
}
|
||||
|
||||
export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||
@@ -48,5 +49,6 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
||||
state: entity.state,
|
||||
country: entity.country,
|
||||
description: entity.description,
|
||||
projectionType: entity.projectionType,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ export class FacialRecognitionService {
|
||||
async handleQueueRecognizeFaces({ force }: IBaseJob) {
|
||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||
return force
|
||||
? this.assetRepository.getAll(pagination)
|
||||
? this.assetRepository.getAll(pagination, { order: 'DESC' })
|
||||
: this.assetRepository.getWithout(pagination, WithoutProperty.FACES);
|
||||
});
|
||||
|
||||
|
||||
@@ -172,6 +172,8 @@ export class JobService {
|
||||
if (asset) {
|
||||
if (asset.type === AssetType.VIDEO) {
|
||||
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: item.data });
|
||||
} else if (asset.livePhotoVideoId) {
|
||||
await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: asset.livePhotoVideoId } });
|
||||
}
|
||||
this.communicationRepository.send(CommunicationEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsOptional, IsString } from 'class-validator';
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import { IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
import { toBoolean, ValidateUUID } from '../domain.util';
|
||||
|
||||
export class PersonUpdateDto {
|
||||
@@ -26,6 +26,43 @@ export class PersonUpdateDto {
|
||||
isHidden?: boolean;
|
||||
}
|
||||
|
||||
export class PeopleUpdateDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => PeopleUpdateItem)
|
||||
people!: PeopleUpdateItem[];
|
||||
}
|
||||
|
||||
export class PeopleUpdateItem {
|
||||
/**
|
||||
* Person id.
|
||||
*/
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
/**
|
||||
* Person name.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
/**
|
||||
* Asset is used to get the feature face thumbnail.
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
featureFaceAssetId?: string;
|
||||
|
||||
/**
|
||||
* Person visibility
|
||||
*/
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isHidden?: boolean;
|
||||
}
|
||||
|
||||
export class MergePersonDto {
|
||||
@ValidateUUID({ each: true })
|
||||
ids!: string[];
|
||||
|
||||
@@ -188,6 +188,16 @@ describe(PersonService.name, () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateAll', () => {
|
||||
it('should throw an error when personId is invalid', async () => {
|
||||
personMock.getById.mockResolvedValue(null);
|
||||
await expect(
|
||||
sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }),
|
||||
).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]);
|
||||
expect(personMock.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('handlePersonCleanup', () => {
|
||||
it('should delete people without faces', async () => {
|
||||
personMock.getAllWithoutFaces.mockResolvedValue([personStub.noName]);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
mapPerson,
|
||||
MergePersonDto,
|
||||
PeopleResponseDto,
|
||||
PeopleUpdateDto,
|
||||
PersonResponseDto,
|
||||
PersonSearchDto,
|
||||
PersonUpdateDto,
|
||||
@@ -96,6 +97,24 @@ export class PersonService {
|
||||
return mapPerson(person);
|
||||
}
|
||||
|
||||
async updatePeople(authUser: AuthUserDto, dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
const results: BulkIdResponseDto[] = [];
|
||||
for (const person of dto.people) {
|
||||
try {
|
||||
await this.update(authUser, person.id, {
|
||||
isHidden: person.isHidden,
|
||||
name: person.name,
|
||||
featureFaceAssetId: person.featureFaceAssetId,
|
||||
}),
|
||||
results.push({ id: person.id, success: true });
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Unable to update ${person.id} : ${error}`, error?.stack);
|
||||
results.push({ id: person.id, success: false, error: BulkIdErrorReason.UNKNOWN });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
async handlePersonCleanup() {
|
||||
const people = await this.repository.getAllWithoutFaces();
|
||||
for (const person of people) {
|
||||
@@ -145,6 +164,9 @@ export class PersonService {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-index all faces in typesense for up-to-date search results
|
||||
await this.jobRepository.queue({ name: JobName.SEARCH_INDEX_FACES });
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@ export class SearchService {
|
||||
if (!this.enabled) {
|
||||
return false;
|
||||
}
|
||||
await this.searchRepository.deleteAllFaces();
|
||||
|
||||
// TODO: do this in batches based on searchIndexVersion
|
||||
const faces = this.patchFaces(await this.faceRepository.getAll());
|
||||
|
||||
@@ -36,7 +36,7 @@ export class AlbumRepository implements IAlbumRepository {
|
||||
},
|
||||
order: {
|
||||
assets: {
|
||||
fileCreatedAt: 'ASC',
|
||||
fileCreatedAt: 'DESC',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -207,12 +207,13 @@ export class AssetService {
|
||||
|
||||
const allowExif = this.getExifPermission(authUser);
|
||||
const asset = await this._assetRepository.getById(assetId);
|
||||
const data = allowExif ? mapAsset(asset) : mapAssetWithoutExif(asset);
|
||||
|
||||
if (allowExif) {
|
||||
return mapAsset(asset);
|
||||
} else {
|
||||
return mapAssetWithoutExif(asset);
|
||||
if (data.ownerId !== authUser.id) {
|
||||
data.people = [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public async updateAsset(authUser: AuthUserDto, assetId: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ImmichReadStream,
|
||||
MergePersonDto,
|
||||
PeopleResponseDto,
|
||||
PeopleUpdateDto,
|
||||
PersonResponseDto,
|
||||
PersonSearchDto,
|
||||
PersonService,
|
||||
@@ -32,6 +33,11 @@ export class PersonController {
|
||||
return this.service.getAll(authUser, withHidden);
|
||||
}
|
||||
|
||||
@Put()
|
||||
updatePeople(@AuthUser() authUser: AuthUserDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
|
||||
return this.service.updatePeople(authUser, dto);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
getPerson(@AuthUser() authUser: AuthUserDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
|
||||
return this.service.getById(authUser, id);
|
||||
|
||||
@@ -31,5 +31,5 @@ export async function bootstrap() {
|
||||
const server = await app.listen(port);
|
||||
server.requestTimeout = 30 * 60 * 1000;
|
||||
|
||||
logger.log(`Immich Server is listening on ${port} [v${SERVER_VERSION}] [${envName}] `);
|
||||
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${SERVER_VERSION}] [${envName}] `);
|
||||
}
|
||||
|
||||
@@ -43,6 +43,9 @@ export class ExifEntity {
|
||||
@Column({ type: 'float', nullable: true })
|
||||
longitude!: number | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
projectionType!: string | null;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
city!: string | null;
|
||||
|
||||
|
||||
13
server/src/infra/migrations/1690469489288-Panoramas.ts
Normal file
13
server/src/infra/migrations/1690469489288-Panoramas.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class Panoramas1690217088596 implements MigrationInterface {
|
||||
name = 'Panoramas1690217088596';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "projectionType" character varying`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "projectionType"`);
|
||||
}
|
||||
}
|
||||
@@ -116,7 +116,7 @@ export class AssetRepository implements IAssetRepository {
|
||||
},
|
||||
order: {
|
||||
// Ensures correct order when paginating
|
||||
createdAt: 'ASC',
|
||||
createdAt: options.order ?? 'ASC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,5 +17,5 @@ export async function bootstrap() {
|
||||
await app.get(AppService).init();
|
||||
await app.listen(port);
|
||||
|
||||
logger.log(`Immich Microservices is listening on ${port} [v${SERVER_VERSION}] [${envName}] `);
|
||||
logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${SERVER_VERSION}] [${envName}] `);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import {
|
||||
IAssetRepository,
|
||||
IBaseJob,
|
||||
ICryptoRepository,
|
||||
IEntityJob,
|
||||
IGeocodingRepository,
|
||||
IJobRepository,
|
||||
IStorageRepository,
|
||||
JobName,
|
||||
JOBS_ASSET_PAGINATION_SIZE,
|
||||
QueueName,
|
||||
StorageCore,
|
||||
StorageFolder,
|
||||
usePagination,
|
||||
WithoutProperty,
|
||||
} from '@app/domain';
|
||||
@@ -19,6 +23,7 @@ import { exiftool, Tags } from 'exiftool-vendored';
|
||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||
import { Duration } from 'luxon';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import sharp from 'sharp';
|
||||
import { Repository } from 'typeorm/repository/Repository';
|
||||
import { promisify } from 'util';
|
||||
@@ -29,18 +34,35 @@ import { toNumberOrNull } from '../utils/numbers';
|
||||
|
||||
const ffprobe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
|
||||
|
||||
interface DirectoryItem {
|
||||
Length?: number;
|
||||
Mime: string;
|
||||
Padding?: number;
|
||||
Semantic?: string;
|
||||
}
|
||||
|
||||
interface DirectoryEntry {
|
||||
Item: DirectoryItem;
|
||||
}
|
||||
|
||||
interface ImmichTags extends Tags {
|
||||
ContentIdentifier?: string;
|
||||
MotionPhoto?: number;
|
||||
MotionPhotoVersion?: number;
|
||||
MotionPhotoPresentationTimestampUs?: number;
|
||||
}
|
||||
|
||||
export class MetadataExtractionProcessor {
|
||||
private logger = new Logger(MetadataExtractionProcessor.name);
|
||||
private reverseGeocodingEnabled: boolean;
|
||||
private storageCore = new StorageCore();
|
||||
|
||||
constructor(
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(IGeocodingRepository) private geocodingRepository: IGeocodingRepository,
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>,
|
||||
|
||||
configService: ConfigService,
|
||||
@@ -100,6 +122,131 @@ export class MetadataExtractionProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async addExtractedLivePhoto(sourceAsset: AssetEntity, video: string, created: Date | null): Promise<AssetEntity> {
|
||||
if (sourceAsset.livePhotoVideoId) {
|
||||
const [liveAsset] = await this.assetRepository.getByIds([sourceAsset.livePhotoVideoId]);
|
||||
// already exists so no need to generate ID.
|
||||
if (liveAsset.originalPath == video) {
|
||||
return liveAsset;
|
||||
}
|
||||
liveAsset.originalPath = video;
|
||||
return this.assetRepository.save(liveAsset);
|
||||
}
|
||||
const liveAsset = await this.assetRepository.save({
|
||||
ownerId: sourceAsset.ownerId,
|
||||
owner: sourceAsset.owner,
|
||||
|
||||
checksum: await this.cryptoRepository.hashFile(video),
|
||||
originalPath: video,
|
||||
|
||||
fileCreatedAt: created ?? sourceAsset.fileCreatedAt,
|
||||
fileModifiedAt: sourceAsset.fileModifiedAt,
|
||||
|
||||
deviceAssetId: 'NONE',
|
||||
deviceId: 'NONE',
|
||||
|
||||
type: AssetType.VIDEO,
|
||||
isFavorite: false,
|
||||
isArchived: sourceAsset.isArchived,
|
||||
duration: null,
|
||||
isVisible: false,
|
||||
livePhotoVideo: null,
|
||||
resizePath: null,
|
||||
webpPath: null,
|
||||
thumbhash: null,
|
||||
encodedVideoPath: null,
|
||||
tags: [],
|
||||
sharedLinks: [],
|
||||
originalFileName: path.parse(video).name,
|
||||
faces: [],
|
||||
sidecarPath: null,
|
||||
isReadOnly: sourceAsset.isReadOnly,
|
||||
});
|
||||
|
||||
sourceAsset.livePhotoVideoId = liveAsset.id;
|
||||
await this.assetRepository.save(sourceAsset);
|
||||
return liveAsset;
|
||||
}
|
||||
|
||||
private async extractNewPixelLivePhoto(
|
||||
asset: AssetEntity,
|
||||
directory: DirectoryEntry[],
|
||||
fileCreatedAt: Date | null,
|
||||
): Promise<AssetEntity | null> {
|
||||
if (asset.livePhotoVideoId) {
|
||||
// Already extracted, don't try again.
|
||||
const [ret] = await this.assetRepository.getByIds([asset.livePhotoVideoId]);
|
||||
this.logger.log(`Already extracted asset ${ret.originalPath}.`);
|
||||
return ret;
|
||||
}
|
||||
let foundMotionPhoto = false;
|
||||
let motionPhotoOffsetFromEnd = 0;
|
||||
let motionPhotoLength = 0;
|
||||
|
||||
// Look for the directory entry with semantic label "MotionPhoto", which is the embedded video.
|
||||
// Then, determine the length from the end of the file to the start of the embedded video.
|
||||
for (const entry of directory) {
|
||||
if (entry.Item.Semantic == 'MotionPhoto') {
|
||||
if (foundMotionPhoto) {
|
||||
this.logger.error(`Asset ${asset.originalPath} has more than one motion photo.`);
|
||||
continue;
|
||||
}
|
||||
foundMotionPhoto = true;
|
||||
motionPhotoLength = entry.Item.Length ?? 0;
|
||||
}
|
||||
if (foundMotionPhoto) {
|
||||
motionPhotoOffsetFromEnd += entry.Item.Length ?? 0;
|
||||
motionPhotoOffsetFromEnd += entry.Item.Padding ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundMotionPhoto || motionPhotoLength == 0) {
|
||||
return null;
|
||||
}
|
||||
return this.extractEmbeddedVideo(asset, motionPhotoOffsetFromEnd, motionPhotoLength, fileCreatedAt);
|
||||
}
|
||||
|
||||
private async extractEmbeddedVideo(
|
||||
asset: AssetEntity,
|
||||
offsetFromEnd: number,
|
||||
length: number | null,
|
||||
fileCreatedAt: Date | null,
|
||||
) {
|
||||
let file = null;
|
||||
try {
|
||||
file = await fs.promises.open(asset.originalPath);
|
||||
let extracted = null;
|
||||
// Read in embedded video.
|
||||
const stat = await file.stat();
|
||||
if (length == null) {
|
||||
length = offsetFromEnd;
|
||||
}
|
||||
const offset = stat.size - offsetFromEnd;
|
||||
extracted = await file.read({
|
||||
buffer: Buffer.alloc(length),
|
||||
position: offset,
|
||||
length: length,
|
||||
});
|
||||
|
||||
// Write out extracted video, and add it to the asset repository.
|
||||
const encodedVideoFolder = this.storageCore.getFolderLocation(StorageFolder.ENCODED_VIDEO, asset.ownerId);
|
||||
this.storageRepository.mkdirSync(encodedVideoFolder);
|
||||
const livePhotoPath = path.join(encodedVideoFolder, path.parse(asset.originalPath).name + '.mp4');
|
||||
await fs.promises.writeFile(livePhotoPath, extracted.buffer);
|
||||
|
||||
const result = await this.addExtractedLivePhoto(asset, livePhotoPath, fileCreatedAt);
|
||||
await this.handleMetadataExtraction({ id: result.id });
|
||||
return result;
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to extract live photo ${asset.originalPath}: ${e}`);
|
||||
return null;
|
||||
} finally {
|
||||
if (file) {
|
||||
await file.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async handlePhotoMetadataExtraction(asset: AssetEntity) {
|
||||
const mediaExifData = await exiftool.read<ImmichTags>(asset.originalPath).catch((error: any) => {
|
||||
this.logger.warn(
|
||||
@@ -163,6 +310,37 @@ export class MetadataExtractionProcessor {
|
||||
const longitude = getExifProperty('GPSLongitude');
|
||||
newExif.latitude = latitude !== null ? parseLatitude(latitude) : null;
|
||||
newExif.longitude = longitude !== null ? parseLongitude(longitude) : null;
|
||||
if (getExifProperty('MotionPhoto')) {
|
||||
// Seen on more recent Pixel phones: starting as early as Pixel 4a, possibly earlier.
|
||||
const rawDirectory = getExifProperty('Directory');
|
||||
if (Array.isArray(rawDirectory)) {
|
||||
// exiftool-vendor thinks directory is a string, but actually it's an array of DirectoryEntry.
|
||||
const directory = rawDirectory as DirectoryEntry[];
|
||||
await this.extractNewPixelLivePhoto(asset, directory, fileCreatedAt);
|
||||
} else {
|
||||
this.logger.warn(`Failed to get Pixel motionPhoto information: directory: ${JSON.stringify(rawDirectory)}`);
|
||||
}
|
||||
} else if (getExifProperty('MicroVideo')) {
|
||||
// Seen on earlier Pixel phones - Pixel 2 and earlier, possibly Pixel 3.
|
||||
let offset = getExifProperty('MicroVideoOffset'); // offset from end of file.
|
||||
if (typeof offset == 'string') {
|
||||
offset = parseInt(offset);
|
||||
}
|
||||
if (Number.isNaN(offset) || offset == null) {
|
||||
this.logger.warn(
|
||||
`Failed to get MicroVideo information for ${asset.originalPath}, offset=${getExifProperty(
|
||||
'MicroVideoOffset',
|
||||
)}`,
|
||||
);
|
||||
} else {
|
||||
await this.extractEmbeddedVideo(asset, offset, null, fileCreatedAt);
|
||||
}
|
||||
}
|
||||
|
||||
const projectionType = getExifProperty('ProjectionType');
|
||||
if (projectionType) {
|
||||
newExif.projectionType = String(projectionType).toUpperCase();
|
||||
}
|
||||
|
||||
newExif.livePhotoCID = getExifProperty('MediaGroupUUID');
|
||||
if (newExif.livePhotoCID && !asset.livePhotoVideoId) {
|
||||
|
||||
@@ -625,6 +625,7 @@ const assetInfo: ExifResponseDto = {
|
||||
state: 'state',
|
||||
country: 'country',
|
||||
description: 'description',
|
||||
projectionType: null,
|
||||
};
|
||||
|
||||
const assetResponse: AssetResponseDto = {
|
||||
@@ -882,6 +883,7 @@ export const sharedLinkStub = {
|
||||
livePhotoVideoId: null,
|
||||
originalFileName: 'asset_1.jpeg',
|
||||
exifInfo: {
|
||||
projectionType: null,
|
||||
livePhotoCID: null,
|
||||
assetId: 'id_1',
|
||||
description: 'description',
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM node:18.16.0-alpine3.18@sha256:9036ddb8252ba7089c2c83eb2b0dcaf74ff1069e8ddf
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
EXPOSE 3000
|
||||
RUN apk add --no-cache setpriv
|
||||
RUN apk add --no-cache setpriv tini
|
||||
|
||||
FROM base as builder
|
||||
|
||||
@@ -39,6 +39,6 @@ COPY --from=prod /usr/src/app/build ./build
|
||||
COPY package.json package-lock.json ./
|
||||
COPY entrypoint.sh ./
|
||||
|
||||
ENTRYPOINT ["/bin/sh"]
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
|
||||
CMD ["entrypoint.sh"]
|
||||
|
||||
0
web/entrypoint.sh
Normal file → Executable file
0
web/entrypoint.sh
Normal file → Executable file
103
web/package-lock.json
generated
103
web/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "immich-web",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
||||
"@zoom-image/svelte": "^0.1.8",
|
||||
"axios": "^0.27.2",
|
||||
"buffer": "^6.0.3",
|
||||
@@ -1840,6 +1841,47 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@cfcs/core": {
|
||||
"version": "0.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@cfcs/core/-/core-0.0.24.tgz",
|
||||
"integrity": "sha512-feB38qu+eDk0Pggh/yR7gjaNmvUYA2uCxHP3Pz2MLE4LZ/9jPdtu8bzCSI47yTEhWyZCF5Pk698hdz8IN2mTjA==",
|
||||
"dependencies": {
|
||||
"@egjs/component": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/component": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/component/-/component-3.0.4.tgz",
|
||||
"integrity": "sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g=="
|
||||
},
|
||||
"node_modules/@egjs/imready": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/imready/-/imready-1.4.1.tgz",
|
||||
"integrity": "sha512-JIOBs4lB7FYdsKi5uvz2j3SObX8eShtZjtqlOH41tm185aJOQZwiKBK8+V4MxzG4X6DqVhpdN8UcuVwBbElfsg==",
|
||||
"dependencies": {
|
||||
"@cfcs/core": "^0.0.24",
|
||||
"@egjs/component": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/svelte-view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/svelte-view360/-/svelte-view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-qFNbLNME8H7QU2lg8SCKUTPoBXVdBcM5m8zmlDRE72esCTguDzUq2szXD7L1JWcb2lYPTFl3HVp/sZlcQ/1HpQ==",
|
||||
"dependencies": {
|
||||
"@egjs/view360": "4.0.0-beta.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@egjs/view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/view360/-/view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-prVTTxuQ1/k59NM7G0tm58k2vPHGoaExoFr5E7MoJaSGF56Otj4okQHAxxosXH87aQLN0feZMtBlsKz0b/7zEw==",
|
||||
"dependencies": {
|
||||
"@egjs/component": "^3.0.2",
|
||||
"@egjs/imready": "^1.3.0",
|
||||
"@types/webxr": "^0.5.1",
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||
@@ -3821,6 +3863,11 @@
|
||||
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/webxr": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
|
||||
"integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw=="
|
||||
},
|
||||
"node_modules/@types/yargs": {
|
||||
"version": "17.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz",
|
||||
@@ -6290,6 +6337,11 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
@@ -13334,6 +13386,47 @@
|
||||
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
|
||||
"dev": true
|
||||
},
|
||||
"@cfcs/core": {
|
||||
"version": "0.0.24",
|
||||
"resolved": "https://registry.npmjs.org/@cfcs/core/-/core-0.0.24.tgz",
|
||||
"integrity": "sha512-feB38qu+eDk0Pggh/yR7gjaNmvUYA2uCxHP3Pz2MLE4LZ/9jPdtu8bzCSI47yTEhWyZCF5Pk698hdz8IN2mTjA==",
|
||||
"requires": {
|
||||
"@egjs/component": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"@egjs/component": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/component/-/component-3.0.4.tgz",
|
||||
"integrity": "sha512-sXA7bGbIeLF2OAw/vpka66c6QBBUPcA4UUhR4WGJfnp2XWdiI8QrnJGJMr/UxpE/xnevX9tN3jvNPlW8WkHl3g=="
|
||||
},
|
||||
"@egjs/imready": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/imready/-/imready-1.4.1.tgz",
|
||||
"integrity": "sha512-JIOBs4lB7FYdsKi5uvz2j3SObX8eShtZjtqlOH41tm185aJOQZwiKBK8+V4MxzG4X6DqVhpdN8UcuVwBbElfsg==",
|
||||
"requires": {
|
||||
"@cfcs/core": "^0.0.24",
|
||||
"@egjs/component": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"@egjs/svelte-view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/svelte-view360/-/svelte-view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-qFNbLNME8H7QU2lg8SCKUTPoBXVdBcM5m8zmlDRE72esCTguDzUq2szXD7L1JWcb2lYPTFl3HVp/sZlcQ/1HpQ==",
|
||||
"requires": {
|
||||
"@egjs/view360": "4.0.0-beta.7"
|
||||
}
|
||||
},
|
||||
"@egjs/view360": {
|
||||
"version": "4.0.0-beta.7",
|
||||
"resolved": "https://registry.npmjs.org/@egjs/view360/-/view360-4.0.0-beta.7.tgz",
|
||||
"integrity": "sha512-prVTTxuQ1/k59NM7G0tm58k2vPHGoaExoFr5E7MoJaSGF56Otj4okQHAxxosXH87aQLN0feZMtBlsKz0b/7zEw==",
|
||||
"requires": {
|
||||
"@egjs/component": "^3.0.2",
|
||||
"@egjs/imready": "^1.3.0",
|
||||
"@types/webxr": "^0.5.1",
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"@esbuild/android-arm": {
|
||||
"version": "0.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
|
||||
@@ -14748,6 +14841,11 @@
|
||||
"integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/webxr": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.2.tgz",
|
||||
"integrity": "sha512-szL74BnIcok9m7QwYtVmQ+EdIKwbjPANudfuvDrAF8Cljg9MKUlIoc1w5tjj9PMpeSH3U1Xnx//czQybJ0EfSw=="
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "17.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.22.tgz",
|
||||
@@ -16510,6 +16608,11 @@
|
||||
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
|
||||
"dev": true
|
||||
},
|
||||
"gl-matrix": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
|
||||
"integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA=="
|
||||
},
|
||||
"glob": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz",
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@egjs/svelte-view360": "^4.0.0-beta.7",
|
||||
"@zoom-image/svelte": "^0.1.8",
|
||||
"axios": "^0.27.2",
|
||||
"buffer": "^6.0.3",
|
||||
|
||||
140
web/src/api/open-api/api.ts
generated
140
web/src/api/open-api/api.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
@@ -1304,6 +1304,12 @@ export interface ExifResponseDto {
|
||||
* @memberof ExifResponseDto
|
||||
*/
|
||||
'description'?: string | null;
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
* @memberof ExifResponseDto
|
||||
*/
|
||||
'projectionType'?: string | null;
|
||||
}
|
||||
/**
|
||||
*
|
||||
@@ -1802,6 +1808,50 @@ export interface PeopleResponseDto {
|
||||
*/
|
||||
'people': Array<PersonResponseDto>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PeopleUpdateDto
|
||||
*/
|
||||
export interface PeopleUpdateDto {
|
||||
/**
|
||||
*
|
||||
* @type {Array<PeopleUpdateItem>}
|
||||
* @memberof PeopleUpdateDto
|
||||
*/
|
||||
'people': Array<PeopleUpdateItem>;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
* @interface PeopleUpdateItem
|
||||
*/
|
||||
export interface PeopleUpdateItem {
|
||||
/**
|
||||
* Person id.
|
||||
* @type {string}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'id': string;
|
||||
/**
|
||||
* Person name.
|
||||
* @type {string}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'name'?: string;
|
||||
/**
|
||||
* Asset is used to get the feature face thumbnail.
|
||||
* @type {string}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'featureFaceAssetId'?: string;
|
||||
/**
|
||||
* Person visibility
|
||||
* @type {boolean}
|
||||
* @memberof PeopleUpdateItem
|
||||
*/
|
||||
'isHidden'?: boolean;
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @export
|
||||
@@ -8940,6 +8990,50 @@ export const PersonApiAxiosParamCreator = function (configuration?: Configuratio
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updatePeople: async (peopleUpdateDto: PeopleUpdateDto, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
|
||||
// verify required parameter 'peopleUpdateDto' is not null or undefined
|
||||
assertParamExists('updatePeople', 'peopleUpdateDto', peopleUpdateDto)
|
||||
const localVarPath = `/person`;
|
||||
// use dummy base URL string because the URL constructor only accepts absolute URLs.
|
||||
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
|
||||
let baseOptions;
|
||||
if (configuration) {
|
||||
baseOptions = configuration.baseOptions;
|
||||
}
|
||||
|
||||
const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options};
|
||||
const localVarHeaderParameter = {} as any;
|
||||
const localVarQueryParameter = {} as any;
|
||||
|
||||
// authentication cookie required
|
||||
|
||||
// authentication api_key required
|
||||
await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration)
|
||||
|
||||
// authentication bearer required
|
||||
// http bearer authentication required
|
||||
await setBearerAuthToObject(localVarHeaderParameter, configuration)
|
||||
|
||||
|
||||
|
||||
localVarHeaderParameter['Content-Type'] = 'application/json';
|
||||
|
||||
setSearchParams(localVarUrlObj, localVarQueryParameter);
|
||||
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
|
||||
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
|
||||
localVarRequestOptions.data = serializeDataIfNeeded(peopleUpdateDto, localVarRequestOptions, configuration)
|
||||
|
||||
return {
|
||||
url: toPathString(localVarUrlObj),
|
||||
options: localVarRequestOptions,
|
||||
};
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -9049,6 +9143,16 @@ export const PersonApiFp = function(configuration?: Configuration) {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.mergePerson(id, mergePersonDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
async updatePeople(peopleUpdateDto: PeopleUpdateDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<Array<BulkIdResponseDto>>> {
|
||||
const localVarAxiosArgs = await localVarAxiosParamCreator.updatePeople(peopleUpdateDto, options);
|
||||
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -9116,6 +9220,15 @@ export const PersonApiFactory = function (configuration?: Configuration, basePat
|
||||
mergePerson(id: string, mergePersonDto: MergePersonDto, options?: any): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.mergePerson(id, mergePersonDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {PeopleUpdateDto} peopleUpdateDto
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
*/
|
||||
updatePeople(peopleUpdateDto: PeopleUpdateDto, options?: any): AxiosPromise<Array<BulkIdResponseDto>> {
|
||||
return localVarFp.updatePeople(peopleUpdateDto, options).then((request) => request(axios, basePath));
|
||||
},
|
||||
/**
|
||||
*
|
||||
* @param {string} id
|
||||
@@ -9206,6 +9319,20 @@ export interface PersonApiMergePersonRequest {
|
||||
readonly mergePersonDto: MergePersonDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePeople operation in PersonApi.
|
||||
* @export
|
||||
* @interface PersonApiUpdatePeopleRequest
|
||||
*/
|
||||
export interface PersonApiUpdatePeopleRequest {
|
||||
/**
|
||||
*
|
||||
* @type {PeopleUpdateDto}
|
||||
* @memberof PersonApiUpdatePeople
|
||||
*/
|
||||
readonly peopleUpdateDto: PeopleUpdateDto
|
||||
}
|
||||
|
||||
/**
|
||||
* Request parameters for updatePerson operation in PersonApi.
|
||||
* @export
|
||||
@@ -9289,6 +9416,17 @@ export class PersonApi extends BaseAPI {
|
||||
return PersonApiFp(this.configuration).mergePerson(requestParameters.id, requestParameters.mergePersonDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePeopleRequest} requestParameters Request parameters.
|
||||
* @param {*} [options] Override http request option.
|
||||
* @throws {RequiredError}
|
||||
* @memberof PersonApi
|
||||
*/
|
||||
public updatePeople(requestParameters: PersonApiUpdatePeopleRequest, options?: AxiosRequestConfig) {
|
||||
return PersonApiFp(this.configuration).updatePeople(requestParameters.peopleUpdateDto, options).then((request) => request(this.axios, this.basePath));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {PersonApiUpdatePersonRequest} requestParameters Request parameters.
|
||||
|
||||
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@@ -4,7 +4,7 @@
|
||||
* Immich
|
||||
* Immich API
|
||||
*
|
||||
* The version of the OpenAPI document: 1.68.0
|
||||
* The version of the OpenAPI document: 1.71.0
|
||||
*
|
||||
*
|
||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
import DetailPanel from './detail-panel.svelte';
|
||||
import PhotoViewer from './photo-viewer.svelte';
|
||||
import VideoViewer from './video-viewer.svelte';
|
||||
import PanoramaViewer from './panorama-viewer.svelte';
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import ProfileImageCropper from '../shared-components/profile-image-cropper.svelte';
|
||||
|
||||
@@ -293,6 +295,8 @@
|
||||
on:close={closeViewer}
|
||||
on:onVideoEnded={() => (shouldPlayMotionPhoto = false)}
|
||||
/>
|
||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<PanoramaViewer {publicSharedKey} {asset} />
|
||||
{:else}
|
||||
<PhotoViewer {publicSharedKey} {asset} on:close={closeViewer} />
|
||||
{/if}
|
||||
|
||||
20
web/src/lib/components/asset-viewer/panorama-viewer.css
Normal file
20
web/src/lib/components/asset-viewer/panorama-viewer.css
Normal file
@@ -0,0 +1,20 @@
|
||||
.view360-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
touch-action: pan-y;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.view360-canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
40
web/src/lib/components/asset-viewer/panorama-viewer.svelte
Normal file
40
web/src/lib/components/asset-viewer/panorama-viewer.svelte
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
import { api, AssetResponseDto } from '@api';
|
||||
import View360, { EquirectProjection } from '@egjs/svelte-view360';
|
||||
import './panorama-viewer.css';
|
||||
export let asset: AssetResponseDto;
|
||||
export let publicSharedKey = '';
|
||||
let dataUrl = '';
|
||||
let errorMessage = '';
|
||||
const loadAssetData = async () => {
|
||||
try {
|
||||
const { data } = await api.assetApi.serveFile(
|
||||
{ id: asset.id, isThumb: false, isWeb: false, key: publicSharedKey },
|
||||
{ responseType: 'blob' },
|
||||
);
|
||||
if (data instanceof Blob) {
|
||||
dataUrl = URL.createObjectURL(data);
|
||||
return dataUrl;
|
||||
} else {
|
||||
throw new Error('Invalid data format');
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = 'Failed to load asset';
|
||||
return '';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div transition:fade={{ duration: 150 }} class="flex h-full select-none place-content-center place-items-center">
|
||||
{#await loadAssetData()}
|
||||
<LoadingSpinner />
|
||||
{:then assetData}
|
||||
{#if assetData}
|
||||
<View360 autoResize={true} initialZoom={0.5} projection={new EquirectProjection({ src: assetData })} />
|
||||
{:else}
|
||||
<p>{errorMessage}</p>
|
||||
{/if}
|
||||
{/await}
|
||||
</div>
|
||||
@@ -14,16 +14,22 @@
|
||||
export let shadow = false;
|
||||
export let circle = false;
|
||||
export let hidden = false;
|
||||
export let border = false;
|
||||
let complete = false;
|
||||
|
||||
export let eyeColor = 'white';
|
||||
</script>
|
||||
|
||||
<img
|
||||
style:width={widthStyle}
|
||||
style:height={heightStyle}
|
||||
style:filter={hidden ? 'grayscale(75%)' : 'none'}
|
||||
style:filter={hidden ? 'grayscale(50%)' : 'none'}
|
||||
style:opacity={hidden ? '0.5' : '1'}
|
||||
src={url}
|
||||
alt={altText}
|
||||
class="object-cover transition duration-300"
|
||||
class="object-cover transition duration-300 {border
|
||||
? 'border-[3px] border-immich-dark-primary/80 hover:border-immich-primary'
|
||||
: ''}"
|
||||
class:rounded-lg={curve}
|
||||
class:shadow-lg={shadow}
|
||||
class:rounded-full={circle}
|
||||
@@ -32,9 +38,10 @@
|
||||
use:imageLoad
|
||||
on:image-load|once={() => (complete = true)}
|
||||
/>
|
||||
|
||||
{#if hidden}
|
||||
<div class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform">
|
||||
<EyeOffOutline size="2em" />
|
||||
<EyeOffOutline size="2em" color={eyeColor} />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import IntersectionObserver from '$lib/components/asset-viewer/intersection-observer.svelte';
|
||||
import { timeToSeconds } from '$lib/utils/time-to-seconds';
|
||||
import { api, AssetResponseDto, AssetTypeEnum, ThumbnailFormat } from '@api';
|
||||
@@ -12,6 +13,7 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import ImageThumbnail from './image-thumbnail.svelte';
|
||||
import VideoThumbnail from './video-thumbnail.svelte';
|
||||
import Rotate360Icon from 'svelte-material-icons/Rotate360.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -124,6 +126,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.type === AssetTypeEnum.Image && asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<div class="absolute right-0 top-0 z-20 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||
<span class="pr-2 pt-2">
|
||||
<Rotate360Icon size="24" />
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if asset.resized}
|
||||
<ImageThumbnail
|
||||
url={api.getAssetThumbnailUrl(asset.id, format, publicSharedKey)}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
$: unselectedPeople = people.filter((source) => !selectedPeople.includes(source) && source.id !== person.id);
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.personApi.getAllPeople({ withHidden: true });
|
||||
const { data } = await api.personApi.getAllPeople({ withHidden: false });
|
||||
people = data.people;
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user