Compare commits

..

7 Commits

Author SHA1 Message Date
Zack Pollard
eabf535246 wip: pmtiles server 2024-09-24 13:05:19 +01:00
Zack Pollard
b0947bf9b9 feat: serve map tile styles from tiles.immich.cloud (#12858)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2024-09-24 13:05:17 +01:00
Daniel Dietzler
5ea3b5a5c1 fix: open api (#12878) 2024-09-24 13:05:15 +01:00
Jason Rasmussen
ded18f3b6b refactor(mobile): open api dto upgrade (#12793) 2024-09-24 13:05:15 +01:00
Jason Rasmussen
3724dc465b fix: remove no longer needed LD_LIBRARY_PATH (#12872) 2024-09-24 13:05:15 +01:00
Daniel Dietzler
91b07fbac8 fix: show asset count for unassigned faces (#12871) 2024-09-24 13:05:15 +01:00
Zack Pollard
fe84f1cc90 wip: local tileserver 2024-09-23 23:10:45 +01:00
91 changed files with 7851 additions and 1927 deletions

View File

@@ -25,6 +25,7 @@ server/upload/
server/src/queries
server/dist/
server/www/
server/resources/v1.pmtiles
web/node_modules/
web/coverage/

200
cli/package-lock.json generated
View File

@@ -1353,17 +1353,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/type-utils": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1387,16 +1387,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1416,14 +1416,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1434,14 +1434,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
"integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1459,9 +1459,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1473,14 +1473,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1502,16 +1502,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1525,13 +1525,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1543,9 +1543,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz",
"integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1566,8 +1566,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "2.1.1",
"vitest": "2.1.1"
"@vitest/browser": "2.1.0",
"vitest": "2.1.0"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1576,14 +1576,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
"integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1592,9 +1592,9 @@
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
"integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1606,7 +1606,7 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/spy": "2.1.1",
"@vitest/spy": "2.1.0",
"msw": "^2.3.5",
"vite": "^5.0.0"
},
@@ -1633,13 +1633,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
"integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.1",
"@vitest/utils": "2.1.0",
"pathe": "^1.1.2"
},
"funding": {
@@ -1647,13 +1647,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
"integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"magic-string": "^0.30.11",
"pathe": "^1.1.2"
},
@@ -1661,10 +1661,23 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
"integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1675,13 +1688,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
"integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1689,6 +1702,19 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/acorn": {
"version": "8.12.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz",
@@ -4215,9 +4241,9 @@
}
},
"node_modules/vite-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz",
"integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4257,19 +4283,19 @@
}
},
"node_modules/vitest": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz",
"integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "2.1.1",
"@vitest/mocker": "2.1.1",
"@vitest/pretty-format": "^2.1.1",
"@vitest/runner": "2.1.1",
"@vitest/snapshot": "2.1.1",
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/expect": "2.1.0",
"@vitest/mocker": "2.1.0",
"@vitest/pretty-format": "^2.1.0",
"@vitest/runner": "2.1.0",
"@vitest/snapshot": "2.1.0",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"debug": "^4.3.6",
"magic-string": "^0.30.11",
@@ -4280,7 +4306,7 @@
"tinypool": "^1.0.0",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.1",
"vite-node": "2.1.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -4295,8 +4321,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.1",
"@vitest/ui": "2.1.1",
"@vitest/browser": "2.1.0",
"@vitest/ui": "2.1.0",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -24,6 +24,7 @@ services:
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
- ../server/resources/v1.pmtiles:/usr/src/app/resources/v1.pmtiles
env_file:
- .env
environment:

View File

@@ -27,7 +27,7 @@ You may use a VPN service to open an encrypted connection to your Immich instanc
If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
:::tip Video tutorial
:::tip Video toturial
You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created.
:::

View File

@@ -58,7 +58,6 @@ Optionally, you can enable hardware acceleration for machine learning and transc
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication.
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`.
- Set your timezone by uncommenting the `TZ=` line.
### Step 3 - Start the containers

View File

@@ -27,14 +27,23 @@ If this should not work, try running `docker compose up -d --force-recreate`.
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
:::
### Supported filesystems
The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
If this is an issue, you can change the bind mount to a Docker volume instead.
Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues.
## General
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `./upload`<sup>\*3</sup> | server | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*1</sup>⚠️ | `./upload`<sup>\*2</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
@@ -43,14 +52,17 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
\*2: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.
:::tip
`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
:::
## Workers
| Variable | Description | Default | Containers |

View File

@@ -23,33 +23,7 @@ Immich requires the command `docker compose` - the similarly named `docker-compo
- **RAM**: Minimum 4GB, recommended 6GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
- This can present an issue for Windows users. See below for details and an alternative setup.
- This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems)
for more details and alternatives.
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
- Network shares are supported for the storage of image and video assets only. It is not recommended to use a network share for your database location due to performance and possible data loss issues.
### Special requirements for Windows users
<details>
<summary>Database storage on Windows systems</summary>
The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
If this is an issue, you can change the bind mount to a Docker volume instead as follows:
Make the following change to `.env`:
```diff
- DB_DATA_LOCATION=./postgres
+ DB_DATA_LOCATION=pgdata
```
Add the following line to the bottom of `docker-compose.yml`:
```diff
volumes:
model-cache:
+ pgdata:
```
</details>
- Network shares are supported for the storage of image and video assets only.

View File

@@ -16091,9 +16091,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",

424
e2e/package-lock.json generated
View File

@@ -1149,13 +1149,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz",
"integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==",
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.0.tgz",
"integrity": "sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.47.1"
"playwright": "1.47.0"
},
"bin": {
"playwright": "cli.js"
@@ -1165,9 +1165,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
"cpu": [
"arm"
],
@@ -1179,9 +1179,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
"cpu": [
"arm64"
],
@@ -1193,9 +1193,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
"cpu": [
"arm64"
],
@@ -1207,9 +1207,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
"cpu": [
"x64"
],
@@ -1221,9 +1221,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
"cpu": [
"arm"
],
@@ -1235,9 +1235,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
"cpu": [
"arm"
],
@@ -1249,9 +1249,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
"cpu": [
"arm64"
],
@@ -1263,9 +1263,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
"cpu": [
"arm64"
],
@@ -1277,9 +1277,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
"cpu": [
"ppc64"
],
@@ -1291,9 +1291,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
"cpu": [
"riscv64"
],
@@ -1305,9 +1305,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
"cpu": [
"s390x"
],
@@ -1319,9 +1319,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
"cpu": [
"x64"
],
@@ -1333,9 +1333,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
"cpu": [
"x64"
],
@@ -1347,9 +1347,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
"cpu": [
"arm64"
],
@@ -1361,9 +1361,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
"cpu": [
"ia32"
],
@@ -1375,9 +1375,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
"cpu": [
"x64"
],
@@ -1471,9 +1471,9 @@
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true,
"license": "MIT"
},
@@ -1596,9 +1596,9 @@
}
},
"node_modules/@types/pg": {
"version": "8.11.10",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
"integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==",
"version": "8.11.9",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.9.tgz",
"integrity": "sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1733,17 +1733,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/type-utils": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1767,16 +1767,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1796,14 +1796,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1814,14 +1814,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
"integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1839,9 +1839,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1853,14 +1853,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1908,16 +1908,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1931,13 +1931,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1949,9 +1949,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz",
"integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1972,8 +1972,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "2.1.1",
"vitest": "2.1.1"
"@vitest/browser": "2.1.0",
"vitest": "2.1.0"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1982,14 +1982,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
"integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1998,9 +1998,9 @@
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
"integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2012,7 +2012,7 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/spy": "2.1.1",
"@vitest/spy": "2.1.0",
"msw": "^2.3.5",
"vite": "^5.0.0"
},
@@ -2039,13 +2039,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
"integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.1",
"@vitest/utils": "2.1.0",
"pathe": "^1.1.2"
},
"funding": {
@@ -2053,13 +2053,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
"integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"magic-string": "^0.30.11",
"pathe": "^1.1.2"
},
@@ -2067,10 +2067,23 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
"integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2081,13 +2094,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
"integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -2095,6 +2108,19 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -4142,9 +4168,9 @@
}
},
"node_modules/jose": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz",
"integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==",
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
"integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -5013,15 +5039,15 @@
}
},
"node_modules/pg": {
"version": "8.13.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz",
"integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
"integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.0",
"pg-protocol": "^1.7.0",
"pg-connection-string": "^2.6.4",
"pg-pool": "^3.6.2",
"pg-protocol": "^1.6.1",
"pg-types": "^2.1.0",
"pgpass": "1.x"
},
@@ -5048,11 +5074,10 @@
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
"dev": true,
"license": "MIT"
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.4.tgz",
"integrity": "sha512-v+Z7W/0EO707aNMaAEfiGnGL9sxxumwLl2fJvCQtMn9Fxsg+lPpPkdcyBSv/KFgpGdYkMfn+EI1Or2EHjpgLCA==",
"dev": true
},
"node_modules/pg-int8": {
"version": "1.0.1",
@@ -5073,21 +5098,19 @@
}
},
"node_modules/pg-pool": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.2.tgz",
"integrity": "sha512-Htjbg8BlwXqSBQ9V8Vjtc+vzf/6fVUuak/3/XXKA9oxZprwW3IMDQTGHP+KDmVL7rtd+R1QjbnCFPuTHm3G4hg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.7.0.tgz",
"integrity": "sha512-hTK/mE36i8fDDhgDFjy6xNOG+LCorxLG3WO17tku+ij6sVHXh1jQUJ8hYAnRhNla4QVD2H8er/FOjc/+EgC6yQ==",
"dev": true,
"license": "MIT"
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
"integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==",
"dev": true
},
"node_modules/pg-types": {
"version": "2.2.0",
@@ -5135,13 +5158,13 @@
}
},
"node_modules/playwright": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz",
"integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==",
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz",
"integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.47.1"
"playwright-core": "1.47.0"
},
"bin": {
"playwright": "cli.js"
@@ -5154,9 +5177,9 @@
}
},
"node_modules/playwright-core": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz",
"integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==",
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.0.tgz",
"integrity": "sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5606,9 +5629,9 @@
}
},
"node_modules/rollup": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5622,32 +5645,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.22.4",
"@rollup/rollup-android-arm64": "4.22.4",
"@rollup/rollup-darwin-arm64": "4.22.4",
"@rollup/rollup-darwin-x64": "4.22.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.22.4",
"@rollup/rollup-linux-arm-musleabihf": "4.22.4",
"@rollup/rollup-linux-arm64-gnu": "4.22.4",
"@rollup/rollup-linux-arm64-musl": "4.22.4",
"@rollup/rollup-linux-powerpc64le-gnu": "4.22.4",
"@rollup/rollup-linux-riscv64-gnu": "4.22.4",
"@rollup/rollup-linux-s390x-gnu": "4.22.4",
"@rollup/rollup-linux-x64-gnu": "4.22.4",
"@rollup/rollup-linux-x64-musl": "4.22.4",
"@rollup/rollup-win32-arm64-msvc": "4.22.4",
"@rollup/rollup-win32-ia32-msvc": "4.22.4",
"@rollup/rollup-win32-x64-msvc": "4.22.4",
"@rollup/rollup-android-arm-eabi": "4.21.3",
"@rollup/rollup-android-arm64": "4.21.3",
"@rollup/rollup-darwin-arm64": "4.21.3",
"@rollup/rollup-darwin-x64": "4.21.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.21.3",
"@rollup/rollup-linux-arm-musleabihf": "4.21.3",
"@rollup/rollup-linux-arm64-gnu": "4.21.3",
"@rollup/rollup-linux-arm64-musl": "4.21.3",
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.3",
"@rollup/rollup-linux-riscv64-gnu": "4.21.3",
"@rollup/rollup-linux-s390x-gnu": "4.21.3",
"@rollup/rollup-linux-x64-gnu": "4.21.3",
"@rollup/rollup-linux-x64-musl": "4.21.3",
"@rollup/rollup-win32-arm64-msvc": "4.21.3",
"@rollup/rollup-win32-ia32-msvc": "4.21.3",
"@rollup/rollup-win32-x64-msvc": "4.21.3",
"fsevents": "~2.3.2"
}
},
"node_modules/rollup/node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true,
"license": "MIT"
},
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -6387,9 +6403,9 @@
}
},
"node_modules/vite": {
"version": "5.4.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
"version": "5.4.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6447,9 +6463,9 @@
}
},
"node_modules/vite-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz",
"integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6484,19 +6500,19 @@
}
},
"node_modules/vitest": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz",
"integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "2.1.1",
"@vitest/mocker": "2.1.1",
"@vitest/pretty-format": "^2.1.1",
"@vitest/runner": "2.1.1",
"@vitest/snapshot": "2.1.1",
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/expect": "2.1.0",
"@vitest/mocker": "2.1.0",
"@vitest/pretty-format": "^2.1.0",
"@vitest/runner": "2.1.0",
"@vitest/snapshot": "2.1.0",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"debug": "^4.3.6",
"magic-string": "^0.30.11",
@@ -6507,7 +6523,7 @@
"tinypool": "^1.0.0",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.1",
"vite-node": "2.1.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -6522,8 +6538,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.1",
"@vitest/ui": "2.1.1",
"@vitest/browser": "2.1.0",
"@vitest/ui": "2.1.0",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -11,7 +11,7 @@ from typing import Any, AsyncGenerator, Callable, Iterator
from zipfile import BadZipFile
import orjson
from fastapi import Depends, FastAPI, File, Form, HTTPException, Response
from fastapi import Depends, FastAPI, File, Form, HTTPException
from fastapi.responses import ORJSONResponse
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
from PIL.Image import Image
@@ -124,23 +124,6 @@ def get_entries(entries: str = Form()) -> InferenceEntries:
raise HTTPException(422, "Invalid request format.")
def get_entry(entries: str = Form()) -> InferenceEntry:
try:
request: PipelineRequest = orjson.loads(entries)
for task, types in request.items():
for type, entry in types.items():
parsed: InferenceEntry = {
"name": entry["modelName"],
"task": task,
"type": type,
"options": entry.get("options", {}),
}
return parsed
except (orjson.JSONDecodeError, ValidationError, KeyError, AttributeError) as e:
log.error(f"Invalid request format: {e}")
raise HTTPException(422, "Invalid request format.")
app = FastAPI(lifespan=lifespan)
@@ -154,20 +137,6 @@ def ping() -> str:
return "pong"
@app.post("/load", response_model=TextResponse)
async def load_model(entry: InferenceEntry = Depends(get_entry)) -> None:
model = await model_cache.get(entry["name"], entry["type"], entry["task"], ttl=settings.model_ttl)
model = await load(model)
return Response(status_code=200)
@app.post("/unload", response_model=TextResponse)
async def unload_model(entry: InferenceEntry = Depends(get_entry)) -> None:
await model_cache.unload(entry["name"], entry["type"], entry["task"])
print("unload")
return Response(status_code=200)
@app.post("/predict", dependencies=[Depends(update_state)])
async def predict(
entries: InferenceEntries = Depends(get_entries),

View File

@@ -58,10 +58,3 @@ class ModelCache:
async def revalidate(self, key: str, ttl: int | None) -> None:
if ttl is not None and key in self.cache._handlers:
await self.cache.expire(key, ttl)
async def unload(self, model_name: str, model_type: ModelType, model_task: ModelTask) -> None:
key = f"{model_name}{model_type}{model_task}"
async with OptimisticLock(self.cache, key):
value = await self.cache.get(key)
if value is not None:
await self.cache.delete(key)

View File

@@ -64,42 +64,45 @@ custom_lint:
allowed:
# required / wanted
- lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,exif_info,user}.repository.dart
- lib/repositories/{album,asset,backup,user}.repository.dart
# acceptable exceptions for the time being
- integration_test/test_utils/general_helper.dart
- lib/main.dart
- lib/routing/router.dart
- lib/utils/{db,migration,renderlist_generator}.dart
- lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart
- test/**.dart
# refactor to make the providers and services testable
- lib/pages/common/album_asset_selection.page.dart
- lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
- lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
- lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart
- lib/services/{asset,background,backup,immich_logger,sync}.service.dart
- lib/widgets/asset_grid/asset_grid_data_structure.dart
- lib/services/{asset,asset_description,background,backup,backup_verification,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart
- lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart
- import_rule_openapi:
message: openapi must only be used through ApiRepositories
restrict: package:openapi
allowed:
# requried / wanted
- lib/repositories/*_api.repository.dart
- lib/repositories/album_api.repository.dart
# acceptable exceptions for the time being
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
# refactor
- lib/models/activities/activity.model.dart
- lib/models/map/map_marker.model.dart
- lib/models/search/search_filter.model.dart
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
- lib/models/shared_link/shared_link.model.dart
- lib/pages/search/search_input.page.dart
- lib/providers/asset_viewer/asset_people.provider.dart
- lib/providers/authentication.provider.dart
- lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
- lib/providers/map/map_state.provider.dart
- lib/providers/search/{search,search_filter}.provider.dart
- lib/providers/search/{people,search,search_filter}.provider.dart
- lib/providers/websocket.provider.dart
- lib/routing/auth_guard.dart
- lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart
- lib/services/{activity,api,asset,asset_description,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
- lib/widgets/album/album_thumbnail_listtile.dart
- lib/widgets/forms/login/login_form.dart
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart

View File

@@ -1 +0,0 @@
const int noDbId = -9223372036854775808; // from Isar

View File

@@ -1,16 +0,0 @@
import 'package:immich_mobile/models/activities/activity.model.dart';
abstract interface class IActivityApiRepository {
Future<List<Activity>> getAll(
String albumId, {
String? assetId,
});
Future<Activity> create(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
});
Future<void> delete(String id);
Future<ActivityStats> getStats(String albumId, {String? assetId});
}

View File

@@ -1,6 +1,5 @@
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IAssetRepository {
@@ -8,20 +7,4 @@ abstract interface class IAssetRepository {
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids);
Future<List<Asset>> getByAlbum(Album album, {User? notOwnedBy});
Future<void> deleteById(List<int> ids);
Future<List<Asset>> getAll({
required int ownerId,
bool? remote,
int limit = 100,
});
Future<List<Asset>> updateAll(List<Asset> assets);
Future<List<Asset>> getMatches({
required List<Asset> assets,
required int ownerId,
bool? remote,
int limit = 100,
});
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids);
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets);
}

View File

@@ -1,18 +0,0 @@
import 'package:immich_mobile/entities/asset.entity.dart';
abstract interface class IAssetApiRepository {
// Future<Asset> get(String id);
// Future<List<Asset>> getAll();
// Future<Asset> create(Asset asset);
Future<Asset> update(
String id, {
String? description,
});
// Future<void> delete(String id);
Future<List<Asset>> search({List<String> personIds = const []});
}

View File

@@ -1,9 +0,0 @@
import 'package:immich_mobile/entities/exif_info.entity.dart';
abstract interface class IExifInfoRepository {
Future<ExifInfo?> get(int id);
Future<ExifInfo> update(ExifInfo exifInfo);
Future<void> delete(int id);
}

View File

@@ -1,13 +0,0 @@
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IPartnerApiRepository {
Future<List<User>> getAll(Direction direction);
Future<User> create(String id);
Future<User> update(String id, {required bool inTimeline});
Future<void> delete(String id);
}
enum Direction {
sharedWithMe,
sharedByMe,
}

View File

@@ -1,22 +0,0 @@
abstract interface class IPersonApiRepository {
Future<List<Person>> getAll();
Future<Person> update(String id, {String? name});
}
class Person {
Person({
required this.id,
required this.isHidden,
required this.name,
required this.thumbnailPath,
this.birthDate,
this.updatedAt,
});
final String id;
final DateTime? birthDate;
final bool isHidden;
final String name;
final String thumbnailPath;
final DateTime? updatedAt;
}

View File

@@ -3,6 +3,4 @@ import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IUserRepository {
Future<List<User>> getByIds(List<String> ids);
Future<User?> get(String id);
Future<List<User>> getAll({bool self = true});
Future<User> update(User user);
}

View File

@@ -1,11 +0,0 @@
import 'dart:typed_data';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IUserApiRepository {
Future<List<User>> getAll();
Future<({String profileImagePath})> createProfileImage({
required String name,
required Uint8List data,
});
}

View File

@@ -1,4 +1,5 @@
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:openapi/api.dart';
enum ActivityType { comment, like }
@@ -37,6 +38,16 @@ class Activity {
);
}
Activity.fromDto(ActivityResponseDto dto)
: id = dto.id,
assetId = dto.assetId,
comment = dto.comment,
createdAt = dto.createdAt,
type = dto.type == ReactionType.comment
? ActivityType.comment
: ActivityType.like,
user = User.fromSimpleUserDto(dto.user);
@override
String toString() {
return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
@@ -64,9 +75,3 @@ class Activity {
user.hashCode;
}
}
class ActivityStats {
final int comments;
const ActivityStats({required this.comments});
}

View File

@@ -2,7 +2,7 @@
import 'dart:convert';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:openapi/api.dart';
class SearchLocationFilter {
String? country;
@@ -235,7 +235,7 @@ class SearchDisplayFilters {
class SearchFilter {
String? context;
String? filename;
Set<Person> people;
Set<PersonResponseDto> people;
SearchLocationFilter location;
SearchCameraFilter camera;
SearchDateFilter date;
@@ -258,7 +258,7 @@ class SearchFilter {
SearchFilter copyWith({
String? context,
String? filename,
Set<Person>? people,
Set<PersonResponseDto>? people,
SearchLocationFilter? location,
SearchCameraFilter? camera,
SearchDateFilter? date,

View File

@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
@@ -31,6 +30,7 @@ import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart';
import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart';
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
import 'package:isar/isar.dart';
@RoutePage()
// ignore: must_be_immutable
@@ -73,7 +73,7 @@ class GalleryViewerPage extends HookConsumerWidget {
: <Asset>[];
final stackElements = showStack ? [currentAsset, ...stack] : <Asset>[];
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = currentAsset.id == noDbId;
final isFromDto = currentAsset.id == Isar.autoIncrement;
Asset asset = stackIndex.value == -1
? currentAsset

View File

@@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
@@ -20,6 +19,7 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
import 'package:openapi/api.dart';
@RoutePage()
class SearchInputPage extends HookConsumerWidget {
@@ -110,7 +110,7 @@ class SearchInputPage extends HookConsumerWidget {
}
showPeoplePicker() {
handleOnSelect(Set<Person> value) {
handleOnSelect(Set<PersonResponseDto> value) {
filter.value = filter.value.copyWith(
people: value,
);

View File

@@ -1,9 +1,9 @@
import 'package:immich_mobile/repositories/activity_api.repository.dart';
import 'package:immich_mobile/services/activity.service.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_service.provider.g.dart';
@riverpod
ActivityService activityService(ActivityServiceRef ref) =>
ActivityService(ref.watch(activityApiRepositoryProvider));
ActivityService(ref.watch(apiServiceProvider));

View File

@@ -6,7 +6,7 @@ part of 'activity_service.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$activityServiceHash() => r'23a3ee7db71676d2719daa64217a683cc5c7eab0';
String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0';
/// See also [activityService].
@ProviderFor(activityService)

View File

@@ -11,7 +11,7 @@ class ActivityStatistics extends _$ActivityStatistics {
ref
.watch(activityServiceProvider)
.getStatistics(albumId, assetId: assetId)
.then((stats) => state = stats.comments);
.then((comments) => state = comments);
return 0;
}

View File

@@ -7,7 +7,7 @@ part of 'activity_statistics.provider.dart';
// **************************************************************************
String _$activityStatisticsHash() =>
r'1f43f0bcb11c754ca3cb586a13570db25023b9a8';
r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -5,5 +5,5 @@ import 'package:immich_mobile/services/user.service.dart';
final otherUsersProvider = FutureProvider.autoDispose<List<User>>((ref) {
UserService userService = ref.watch(userServiceProvider);
return userService.getUsers();
return userService.getUsersInDb();
});

View File

@@ -6,7 +6,7 @@ part of 'map_state.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$mapStateNotifierHash() => r'22e4e571bd0730dbc34b109255a62b920e9c7d66';
String _$mapStateNotifierHash() => r'31fafe17aa85c48379a22ed3db3cc94af59ce5b8';
/// See also [MapStateNotifier].
@ProviderFor(MapStateNotifier)

View File

@@ -1,14 +1,14 @@
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/services/person.service.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:openapi/api.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'people.provider.g.dart';
@riverpod
Future<List<Person>> getAllPeople(
Future<List<PersonResponseDto>> getAllPeople(
GetAllPeopleRef ref,
) async {
final PersonService personService = ref.read(personServiceProvider);

View File

@@ -6,11 +6,12 @@ part of 'people.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$getAllPeopleHash() => r'3417b7e0c211382d4480a415e352139995d57b6d';
String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd';
/// See also [getAllPeople].
@ProviderFor(getAllPeople)
final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
final getAllPeopleProvider =
AutoDisposeFutureProvider<List<PersonResponseDto>>.internal(
getAllPeople,
name: r'getAllPeopleProvider',
debugGetCreateSourceHash:
@@ -19,7 +20,7 @@ final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
allTransitiveDependencies: null,
);
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonResponseDto>>;
String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832';
/// Copied from Dart SDK

View File

@@ -1,67 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final activityApiRepositoryProvider = Provider(
(ref) => ActivityApiRepository(ref.watch(apiServiceProvider).activitiesApi),
);
class ActivityApiRepository extends BaseApiRepository
implements IActivityApiRepository {
final ActivitiesApi _api;
ActivityApiRepository(this._api);
@override
Future<List<Activity>> getAll(String albumId, {String? assetId}) async {
final response =
await checkNull(_api.getActivities(albumId, assetId: assetId));
return response.map(_toActivity).toList();
}
@override
Future<Activity> create(
String albumId,
ActivityType type, {
String? assetId,
String? comment,
}) async {
final dto = ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment
? ReactionType.comment
: ReactionType.like,
assetId: assetId,
comment: comment,
);
final response = await checkNull(_api.createActivity(dto));
return _toActivity(response);
}
@override
Future<void> delete(String id) {
return checkNull(_api.deleteActivity(id));
}
@override
Future<ActivityStats> getStats(String albumId, {String? assetId}) async {
final response =
await checkNull(_api.getActivityStatistics(albumId, assetId: assetId));
return ActivityStats(comments: response.comments);
}
static Activity _toActivity(ActivityResponseDto dto) => Activity(
id: dto.id,
createdAt: dto.createdAt,
type: dto.type == ReactionType.comment
? ActivityType.comment
: ActivityType.like,
user: User.fromSimpleUserDto(dto.user),
assetId: dto.assetId,
comment: dto.comment,
);
}

View File

@@ -1,31 +1,30 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/errors.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final albumApiRepositoryProvider = Provider(
(ref) => AlbumApiRepository(ref.watch(apiServiceProvider).albumsApi),
);
class AlbumApiRepository extends BaseApiRepository
implements IAlbumApiRepository {
class AlbumApiRepository implements IAlbumApiRepository {
final AlbumsApi _api;
AlbumApiRepository(this._api);
@override
Future<Album> get(String id) async {
final dto = await checkNull(_api.getAlbumInfo(id));
final dto = await _checkNull(_api.getAlbumInfo(id));
return _toAlbum(dto);
}
@override
Future<List<Album>> getAll({bool? shared}) async {
final dtos = await checkNull(_api.getAllAlbums(shared: shared));
final dtos = await _checkNull(_api.getAllAlbums(shared: shared));
return dtos.map(_toAlbum).toList().cast();
}
@@ -38,7 +37,7 @@ class AlbumApiRepository extends BaseApiRepository
final users = sharedUserIds.map(
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
);
final responseDto = await checkNull(
final responseDto = await _checkNull(
_api.createAlbum(
CreateAlbumDto(
albumName: name,
@@ -58,7 +57,7 @@ class AlbumApiRepository extends BaseApiRepository
String? description,
bool? activityEnabled,
}) async {
final response = await checkNull(
final response = await _checkNull(
_api.updateAlbumInfo(
albumId,
UpdateAlbumDto(
@@ -82,7 +81,7 @@ class AlbumApiRepository extends BaseApiRepository
String albumId,
Iterable<String> assetIds,
) async {
final response = await checkNull(
final response = await _checkNull(
_api.addAssetsToAlbum(
albumId,
BulkIdsDto(ids: assetIds.toList()),
@@ -107,7 +106,7 @@ class AlbumApiRepository extends BaseApiRepository
String albumId,
Iterable<String> assetIds,
) async {
final response = await checkNull(
final response = await _checkNull(
_api.removeAssetFromAlbum(
albumId,
BulkIdsDto(ids: assetIds.toList()),
@@ -128,7 +127,7 @@ class AlbumApiRepository extends BaseApiRepository
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
final albumUsers =
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
final response = await checkNull(
final response = await _checkNull(
_api.addUsersToAlbum(
albumId,
AddUsersDto(albumUsers: albumUsers),
@@ -142,6 +141,12 @@ class AlbumApiRepository extends BaseApiRepository
return _api.removeUserFromAlbum(albumId, userId);
}
static Future<T> _checkNull<T>(Future<T?> future) async {
final response = await future;
if (response == null) throw NoResponseDtoError();
return response;
}
static Album _toAlbum(AlbumResponseDto dto) {
final Album album = Album(
remoteId: dto.id,

View File

@@ -1,11 +1,6 @@
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
@@ -40,104 +35,4 @@ class AssetRepository implements IAssetRepository {
@override
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) =>
_db.assets.getAllByRemoteId(ids);
@override
Future<List<Asset>> getAll({
required int ownerId,
bool? remote,
int limit = 100,
}) {
if (remote == null) {
return _db.assets
.where()
.ownerIdEqualToAnyChecksum(ownerId)
.limit(limit)
.findAll();
}
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
if (remote) {
query = _db.assets
.where()
.localIdIsNull()
.filter()
.remoteIdIsNotNull()
.ownerIdEqualTo(ownerId);
} else {
query = _db.assets
.where()
.remoteIdIsNull()
.filter()
.localIdIsNotNull()
.ownerIdEqualTo(ownerId);
}
return query.limit(limit).findAll();
}
@override
Future<List<Asset>> updateAll(List<Asset> assets) async {
await _db.writeTxn(() => _db.assets.putAll(assets));
return assets;
}
@override
Future<List<Asset>> getMatches({
required List<Asset> assets,
required int ownerId,
bool? remote,
int limit = 100,
}) {
final QueryBuilder<Asset, Asset, QAfterFilterCondition> query;
if (remote == null) {
query = _db.assets.filter().remoteIdIsNotNull().or().localIdIsNotNull();
} else if (remote) {
query = _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull();
} else {
query = _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull();
}
return _getMatchesImpl(query, ownerId, assets, limit);
}
@override
Future<List<DeviceAsset?>> getDeviceAssetsById(List<Object> ids) =>
Platform.isAndroid
? _db.androidDeviceAssets.getAll(ids.cast())
: _db.iOSDeviceAssets.getAllById(ids.cast());
@override
Future<void> upsertDeviceAssets(List<DeviceAsset> deviceAssets) =>
_db.writeTxn(
() => Platform.isAndroid
? _db.androidDeviceAssets.putAll(deviceAssets.cast())
: _db.iOSDeviceAssets.putAll(deviceAssets.cast()),
);
}
Future<List<Asset>> _getMatchesImpl(
QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
int ownerId,
List<Asset> assets,
int limit,
) =>
query
.ownerIdEqualTo(ownerId)
.anyOf(
assets,
(q, Asset a) => q
.fileNameEqualTo(a.fileName)
.and()
.durationInSecondsEqualTo(a.durationInSeconds)
.and()
.fileCreatedAtBetween(
a.fileCreatedAt.subtract(const Duration(hours: 12)),
a.fileCreatedAt.add(const Duration(hours: 12)),
)
.and()
.not()
.checksumEqualTo(a.checksum),
)
.sortByFileName()
.thenByFileCreatedAt()
.thenByFileModifiedAt()
.limit(limit)
.findAll();

View File

@@ -1,52 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final assetApiRepositoryProvider = Provider(
(ref) => AssetApiRepository(
ref.watch(apiServiceProvider).assetsApi,
ref.watch(apiServiceProvider).searchApi,
),
);
class AssetApiRepository extends BaseApiRepository
implements IAssetApiRepository {
final AssetsApi _api;
final SearchApi _searchApi;
AssetApiRepository(this._api, this._searchApi);
@override
Future<Asset> update(String id, {String? description}) async {
final response = await checkNull(
_api.updateAsset(id, UpdateAssetDto(description: description)),
);
return Asset.remote(response);
}
@override
Future<List<Asset>> search({List<String> personIds = const []}) async {
// TODO this always fetches all assets, change API and usage to actually do pagination
final List<Asset> result = [];
bool hasNext = true;
int currentPage = 1;
while (hasNext) {
final response = await checkNull(
_searchApi.searchMetadata(
MetadataSearchDto(
personIds: personIds,
page: currentPage,
size: 1000,
),
),
);
result.addAll(response.assets.items.map(Asset.remote));
hasNext = response.assets.nextPage != null;
currentPage++;
}
return result;
}
}

View File

@@ -1,11 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/errors.dart';
abstract class BaseApiRepository {
@protected
Future<T> checkNull<T>(Future<T?> future) async {
final response = await future;
if (response == null) throw NoResponseDtoError();
return response;
}
}

View File

@@ -1,28 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:isar/isar.dart';
final exifInfoRepositoryProvider =
Provider((ref) => ExifInfoRepository(ref.watch(dbProvider)));
class ExifInfoRepository implements IExifInfoRepository {
final Isar _db;
ExifInfoRepository(
this._db,
);
@override
Future<void> delete(int id) => _db.exifInfos.delete(id);
@override
Future<ExifInfo?> get(int id) => _db.exifInfos.get(id);
@override
Future<ExifInfo> update(ExifInfo exifInfo) async {
await _db.writeTxn(() => _db.exifInfos.put(exifInfo));
return exifInfo;
}
}

View File

@@ -1,51 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final partnerApiRepositoryProvider = Provider(
(ref) => PartnerApiRepository(
ref.watch(apiServiceProvider).partnersApi,
),
);
class PartnerApiRepository extends BaseApiRepository
implements IPartnerApiRepository {
final PartnersApi _api;
PartnerApiRepository(this._api);
@override
Future<List<User>> getAll(Direction direction) async {
final response = await checkNull(
_api.getPartners(
direction == Direction.sharedByMe
? PartnerDirection.by
: PartnerDirection.with_,
),
);
return response.map(User.fromPartnerDto).toList();
}
@override
Future<User> create(String id) async {
final dto = await checkNull(_api.createPartner(id));
return User.fromPartnerDto(dto);
}
@override
Future<void> delete(String id) => checkNull(_api.removePartner(id));
@override
Future<User> update(String id, {required bool inTimeline}) async {
final dto = await checkNull(
_api.updatePartner(
id,
UpdatePartnerDto(inTimeline: inTimeline),
),
);
return User.fromPartnerDto(dto);
}
}

View File

@@ -1,38 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final personApiRepositoryProvider = Provider(
(ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi),
);
class PersonApiRepository extends BaseApiRepository
implements IPersonApiRepository {
final PeopleApi _api;
PersonApiRepository(this._api);
@override
Future<List<Person>> getAll() async {
final dto = await checkNull(_api.getAllPeople());
return dto.people.map(_toPerson).toList();
}
@override
Future<Person> update(String id, {String? name}) async {
final dto = await checkNull(
_api.updatePerson(id, PersonUpdateDto(name: name)),
);
return _toPerson(dto);
}
static Person _toPerson(PersonResponseDto dto) => Person(
birthDate: dto.birthDate,
id: dto.id,
isHidden: dto.isHidden,
name: dto.name,
thumbnailPath: dto.thumbnailPath,
);
}

View File

@@ -1,5 +1,4 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
@@ -21,19 +20,4 @@ class UserRepository implements IUserRepository {
@override
Future<User?> get(String id) => _db.users.getById(id);
@override
Future<List<User>> getAll({bool self = true}) {
if (self) {
return _db.users.where().findAll();
}
final int userId = Store.get(StoreKey.currentUser).isarId;
return _db.users.where().isarIdNotEqualTo(userId).findAll();
}
@override
Future<User> update(User user) async {
await _db.writeTxn(() => _db.users.put(user));
return user;
}
}

View File

@@ -1,41 +0,0 @@
import 'dart:typed_data';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/base_api.repository.dart';
import 'package:openapi/api.dart';
final userApiRepositoryProvider = Provider(
(ref) => UserApiRepository(
ref.watch(apiServiceProvider).usersApi,
),
);
class UserApiRepository extends BaseApiRepository
implements IUserApiRepository {
final UsersApi _api;
UserApiRepository(this._api);
@override
Future<List<User>> getAll() async {
final dto = await checkNull(_api.searchUsers());
return dto.map(User.fromSimpleUserDto).toList();
}
@override
Future<({String profileImagePath})> createProfileImage({
required String name,
required Uint8List data,
}) async {
final response = await checkNull(
_api.createProfileImage(
MultipartFile.fromBytes('file', data, filename: name),
),
);
return (profileImagePath: response.profileImagePath);
}
}

View File

@@ -1,31 +1,41 @@
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/constants/errors.dart';
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class ActivityService with ErrorLoggerMixin {
final IActivityApiRepository _activityApiRepository;
final ApiService _apiService;
@override
final Logger logger = Logger("ActivityService");
ActivityService(this._activityApiRepository);
ActivityService(this._apiService);
Future<List<Activity>> getAllActivities(
String albumId, {
String? assetId,
}) async {
return logError(
() => _activityApiRepository.getAll(albumId, assetId: assetId),
() async {
final list = await _apiService.activitiesApi
.getActivities(albumId, assetId: assetId);
return list != null ? list.map(Activity.fromDto).toList() : [];
},
defaultValue: [],
errorMessage: "Failed to get all activities for album $albumId",
);
}
Future<ActivityStats> getStatistics(String albumId, {String? assetId}) async {
Future<int> getStatistics(String albumId, {String? assetId}) async {
return logError(
() => _activityApiRepository.getStats(albumId, assetId: assetId),
defaultValue: const ActivityStats(comments: 0),
() async {
final dto = await _apiService.activitiesApi
.getActivityStatistics(albumId, assetId: assetId);
return dto?.comments ?? 0;
},
defaultValue: 0,
errorMessage: "Failed to statistics for album $albumId",
);
}
@@ -33,7 +43,7 @@ class ActivityService with ErrorLoggerMixin {
Future<bool> removeActivity(String id) async {
return logError(
() async {
await _activityApiRepository.delete(id);
await _apiService.activitiesApi.deleteActivity(id);
return true;
},
defaultValue: false,
@@ -48,12 +58,22 @@ class ActivityService with ErrorLoggerMixin {
String? comment,
}) async {
return guardError(
() => _activityApiRepository.create(
albumId,
type,
assetId: assetId,
comment: comment,
),
() async {
final dto = await _apiService.activitiesApi.createActivity(
ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment
? ReactionType.comment
: ReactionType.like,
assetId: assetId,
comment: comment,
),
);
if (dto != null) {
return Activity.fromDto(dto);
}
throw NoResponseDtoError();
},
errorMessage: "Failed to create $type for album $albumId",
);
}

View File

@@ -9,13 +9,9 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/backup.service.dart';
@@ -28,8 +24,6 @@ import 'package:openapi/api.dart';
final assetServiceProvider = Provider(
(ref) => AssetService(
ref.watch(assetApiRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
ref.watch(apiServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(userServiceProvider),
@@ -40,8 +34,6 @@ final assetServiceProvider = Provider(
);
class AssetService {
final IAssetApiRepository _assetApiRepository;
final IExifInfoRepository _exifInfoRepository;
final ApiService _apiService;
final SyncService _syncService;
final UserService _userService;
@@ -51,8 +43,6 @@ class AssetService {
final Isar _db;
AssetService(
this._assetApiRepository,
this._exifInfoRepository,
this._apiService,
this._syncService,
this._userService,
@@ -352,46 +342,4 @@ class AssetService {
log.severe("Error while syncing uploaded asset to albums", error, stack);
}
}
Future<void> setDescription(
Asset asset,
String newDescription,
) async {
final remoteAssetId = asset.remoteId;
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
final result = await _assetApiRepository.update(
remoteAssetId,
description: newDescription,
);
final description = result.exifInfo?.description;
if (description != null) {
var exifInfo = await _exifInfoRepository.get(localExifId);
if (exifInfo != null) {
exifInfo.description = description;
await _exifInfoRepository.update(exifInfo);
}
}
}
Future<String> getDescription(Asset asset) async {
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (localExifId == null) {
return "";
}
final exifInfo = await _exifInfoRepository.get(localExifId);
return exifInfo?.description ?? "";
}
}

View File

@@ -0,0 +1,66 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class AssetDescriptionService {
AssetDescriptionService(this._db, this._api);
final Isar _db;
final ApiService _api;
Future<void> setDescription(
Asset asset,
String newDescription,
) async {
final remoteAssetId = asset.remoteId;
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
final result = await _api.assetsApi.updateAsset(
remoteAssetId,
UpdateAssetDto(description: newDescription),
);
final description = result?.exifInfo?.description;
if (description != null) {
var exifInfo = await _db.exifInfos.get(localExifId);
if (exifInfo != null) {
exifInfo.description = description;
await _db.writeTxn(
() => _db.exifInfos.put(exifInfo),
);
}
}
}
String getAssetDescription(Asset asset) {
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (localExifId == null) {
return "";
}
final exifInfo = _db.exifInfos.getSync(localExifId);
return exifInfo?.description ?? "";
}
}
final assetDescriptionServiceProvider = Provider(
(ref) => AssetDescriptionService(
ref.watch(dbProvider),
ref.watch(apiServiceProvider),
),
);

View File

@@ -18,9 +18,7 @@ import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/backup.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
@@ -32,6 +30,7 @@ import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/partner.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
@@ -363,20 +362,16 @@ class BackgroundService {
apiService.setAccessToken(Store.get(StoreKey.accessToken));
AppSettingsService settingService = AppSettingsService();
AppSettingsService settingsService = AppSettingsService();
PartnerService partnerService = PartnerService(apiService, db);
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
BackupRepository backupAlbumRepository = BackupRepository(db);
AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
FileMediaRepository fileMediaRepository = FileMediaRepository();
UserRepository userRepository = UserRepository(db);
UserApiRepository userApiRepository =
UserApiRepository(apiService.usersApi);
AlbumApiRepository albumApiRepository =
AlbumApiRepository(apiService.albumsApi);
PartnerApiRepository partnerApiRepository =
PartnerApiRepository(apiService.partnersApi);
HashService hashService =
HashService(assetRepository, this, albumMediaRepository);
HashService hashService = HashService(db, this, albumMediaRepository);
EntityService entityService =
EntityService(assetRepository, userRepository);
SyncService syncSerive = SyncService(
@@ -386,12 +381,8 @@ class BackgroundService {
albumMediaRepository,
albumApiRepository,
);
UserService userService = UserService(
partnerApiRepository,
userApiRepository,
userRepository,
syncSerive,
);
UserService userService =
UserService(apiService, db, syncSerive, partnerService);
AlbumService albumService = AlbumService(
userService,
syncSerive,

View File

@@ -8,46 +8,41 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
/// Finds duplicates originating from missing EXIF information
class BackupVerificationService {
final Isar _db;
final IFileMediaRepository _fileMediaRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
BackupVerificationService(
this._fileMediaRepository,
this._assetRepository,
this._exifInfoRepository,
);
BackupVerificationService(this._db, this._fileMediaRepository);
/// Returns at most [limit] assets that were backed up without exif
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
final owner = Store.get(StoreKey.currentUser).isarId;
final List<Asset> onlyLocal = await _assetRepository.getAll(
ownerId: owner,
remote: false,
limit: limit,
final List<Asset> onlyLocal = await _db.assets
.where()
.remoteIdIsNull()
.filter()
.ownerIdEqualTo(owner)
.localIdIsNotNull()
.findAll();
final List<Asset> remoteMatches = await _getMatches(
_db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(),
owner,
onlyLocal,
limit,
);
final List<Asset> remoteMatches = await _assetRepository.getMatches(
assets: onlyLocal,
ownerId: owner,
remote: true,
limit: limit,
);
final List<Asset> localMatches = await _assetRepository.getMatches(
assets: remoteMatches,
ownerId: owner,
remote: false,
limit: limit,
final List<Asset> localMatches = await _getMatches(
_db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(),
owner,
remoteMatches,
limit,
);
final List<Asset> deleteCandidates = [], originals = [];
@@ -57,7 +52,7 @@ class BackupVerificationService {
localMatches,
compare: (a, b) => a.fileName.compareTo(b.fileName),
both: (a, b) async {
a.exifInfo = await _exifInfoRepository.get(a.id);
a.exifInfo = await _db.exifInfos.get(a.id);
deleteCandidates.add(a);
originals.add(b);
return false;
@@ -197,6 +192,35 @@ class BackupVerificationService {
return bytes.buffer.asUint64List(start);
}
static Future<List<Asset>> _getMatches(
QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
int ownerId,
List<Asset> assets,
int limit,
) =>
query
.ownerIdEqualTo(ownerId)
.anyOf(
assets,
(q, Asset a) => q
.fileNameEqualTo(a.fileName)
.and()
.durationInSecondsEqualTo(a.durationInSeconds)
.and()
.fileCreatedAtBetween(
a.fileCreatedAt.subtract(const Duration(hours: 12)),
a.fileCreatedAt.add(const Duration(hours: 12)),
)
.and()
.not()
.checksumEqualTo(a.checksum),
)
.sortByFileName()
.thenByFileCreatedAt()
.thenByFileModifiedAt()
.limit(limit)
.findAll();
static bool _sameExceptTimeZone(DateTime a, DateTime b) {
final ms = a.isAfter(b)
? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch
@@ -209,8 +233,7 @@ class BackupVerificationService {
final backupVerificationServiceProvider = Provider(
(ref) => BackupVerificationService(
ref.watch(dbProvider),
ref.watch(fileMediaRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
),
);

View File

@@ -4,24 +4,20 @@ import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/entities/ios_device_asset.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
class HashService {
HashService(
this._assetRepository,
this._backgroundService,
this._albumMediaRepository,
);
final IAssetRepository _assetRepository;
HashService(this._db, this._backgroundService, this._albumMediaRepository);
final Isar _db;
final BackgroundService _backgroundService;
final IAlbumMediaRepository _albumMediaRepository;
final _log = Logger('HashService');
@@ -59,8 +55,7 @@ class HashService {
final ids = assets
.map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
.toList();
final List<DeviceAsset?> hashes =
await _assetRepository.getDeviceAssetsById(ids);
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
final List<DeviceAsset> toAdd = [];
final List<String> toHash = [];
@@ -111,6 +106,12 @@ class HashService {
return _getHashedAssets(assets, hashes);
}
/// Lookup hashes of assets by their local ID
Future<List<DeviceAsset?>> _lookupHashes(List<Object> ids) =>
Platform.isAndroid
? _db.androidDeviceAssets.getAll(ids.cast())
: _db.iOSDeviceAssets.getAllById(ids.cast());
/// Processes a batch of files and saves any successfully hashed
/// values to the DB table.
Future<void> _processBatch(
@@ -130,7 +131,11 @@ class HashService {
final validHashes = anyNull
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
: toAdd;
await _assetRepository.upsertDeviceAssets(validHashes);
await _db.writeTxn(
() => Platform.isAndroid
? _db.androidDeviceAssets.putAll(validHashes.cast())
: _db.iOSDeviceAssets.putAll(validHashes.cast()),
);
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
}
@@ -163,7 +168,7 @@ class HashService {
final hashServiceProvider = Provider(
(ref) => HashService(
ref.watch(assetRepositoryProvider),
ref.watch(dbProvider),
ref.watch(backgroundServiceProvider),
ref.watch(albumMediaRepositoryProvider),
),

View File

@@ -1,17 +1,18 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/models/memories/memory.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
return MemoryService(
ref.watch(apiServiceProvider),
ref.watch(assetRepositoryProvider),
ref.watch(dbProvider),
);
});
@@ -19,9 +20,9 @@ class MemoryService {
final log = Logger("MemoryService");
final ApiService _apiService;
final IAssetRepository _assetRepository;
final Isar _db;
MemoryService(this._apiService, this._assetRepository);
MemoryService(this._apiService, this._db);
Future<List<Memory>?> getMemoryLane() async {
try {
@@ -38,7 +39,7 @@ class MemoryService {
List<Memory> memories = [];
for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) {
final dbAssets =
await _assetRepository.getAllByRemoteId(assets.map((e) => e.id));
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
if (dbAssets.isNotEmpty) {
final String title = yearsAgo <= 1
? 'memories_year_ago'.tr()

View File

@@ -1,33 +1,43 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final partnerServiceProvider = Provider(
(ref) => PartnerService(
ref.watch(partnerApiRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
),
);
class PartnerService {
final IPartnerApiRepository _partnerApiRepository;
final IUserRepository _userRepository;
final ApiService _apiService;
final Isar _db;
final Logger _log = Logger("PartnerService");
PartnerService(
this._partnerApiRepository,
this._userRepository,
);
PartnerService(this._apiService, this._db);
Future<List<User>?> getPartners(PartnerDirection direction) async {
try {
final userDtos = await _apiService.partnersApi.getPartners(direction);
if (userDtos != null) {
return userDtos.map((u) => User.fromPartnerDto(u)).toList();
}
} catch (e) {
_log.warning("Failed to get partners for direction $direction", e);
}
return null;
}
Future<bool> removePartner(User partner) async {
try {
await _partnerApiRepository.delete(partner.id);
await _apiService.partnersApi.removePartner(partner.id);
partner.isPartnerSharedBy = false;
await _userRepository.update(partner);
await _db.writeTxn(() => _db.users.put(partner));
} catch (e) {
_log.warning("Failed to remove partner ${partner.id}", e);
return false;
@@ -37,10 +47,12 @@ class PartnerService {
Future<bool> addPartner(User partner) async {
try {
await _partnerApiRepository.create(partner.id);
partner.isPartnerSharedBy = true;
await _userRepository.update(partner);
return true;
final dto = await _apiService.partnersApi.createPartner(partner.id);
if (dto != null) {
partner.isPartnerSharedBy = true;
await _db.writeTxn(() => _db.users.put(partner));
return true;
}
} catch (e) {
_log.warning("Failed to add partner ${partner.id}", e);
}
@@ -49,13 +61,13 @@ class PartnerService {
Future<bool> updatePartner(User partner, {required bool inTimeline}) async {
try {
final dto = await _partnerApiRepository.update(
partner.id,
inTimeline: inTimeline,
);
partner.inTimeline = dto.inTimeline;
await _userRepository.update(partner);
return true;
final dto = await _apiService.partnersApi
.updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline));
if (dto != null) {
partner.inTimeline = dto.inTimeline ?? partner.inTimeline;
await _db.writeTxn(() => _db.users.put(partner));
return true;
}
} catch (e) {
_log.warning("Failed to update partner ${partner.id}", e);
}

View File

@@ -1,37 +1,29 @@
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/person_api.repository.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'person.service.g.dart';
@riverpod
PersonService personService(PersonServiceRef ref) => PersonService(
ref.watch(personApiRepositoryProvider),
ref.watch(assetApiRepositoryProvider),
ref.read(assetRepositoryProvider),
);
PersonService personService(PersonServiceRef ref) =>
PersonService(ref.read(apiServiceProvider), ref.read(dbProvider));
class PersonService {
final Logger _log = Logger("PersonService");
final IPersonApiRepository _personApiRepository;
final IAssetApiRepository _assetApiRepository;
final IAssetRepository _assetRepository;
final ApiService _apiService;
final Isar _db;
PersonService(
this._personApiRepository,
this._assetApiRepository,
this._assetRepository,
);
PersonService(this._apiService, this._db);
Future<List<Person>> getAllPeople() async {
Future<List<PersonResponseDto>> getAllPeople() async {
try {
return await _personApiRepository.getAll();
final peopleResponseDto = await _apiService.peopleApi.getAllPeople();
return peopleResponseDto?.people ?? [];
} catch (error, stack) {
_log.severe("Error while fetching curated people", error, stack);
return [];
@@ -39,19 +31,50 @@ class PersonService {
}
Future<List<Asset>> getPersonAssets(String id) async {
List<Asset> result = [];
var hasNext = true;
var currentPage = 1;
try {
final assets = await _assetApiRepository.search(personIds: [id]);
return await _assetRepository
.getAllByRemoteId(assets.map((a) => a.remoteId!));
while (hasNext) {
final response = await _apiService.searchApi.searchMetadata(
MetadataSearchDto(
personIds: [id],
page: currentPage,
size: 1000,
),
);
if (response == null) {
break;
}
if (response.assets.nextPage == null) {
hasNext = false;
}
final assets = response.assets.items;
final mapAssets =
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
result.addAll(mapAssets);
currentPage++;
}
} catch (error, stack) {
_log.severe("Error while fetching person assets", error, stack);
}
return [];
return result;
}
Future<Person?> updateName(String id, String name) async {
Future<PersonResponseDto?> updateName(String id, String name) async {
try {
return await _personApiRepository.update(id, name: name);
return await _apiService.peopleApi.updatePerson(
id,
PersonUpdateDto(
name: name,
),
);
} catch (error, stack) {
_log.severe("Error while updating person name", error, stack);
}

View File

@@ -6,7 +6,7 @@ part of 'person.service.dart';
// RiverpodGenerator
// **************************************************************************
String _$personServiceHash() => r'32f28cb5a3de0553c17447e33a0efde7409a43ed';
String _$personServiceHash() => r'54e6df4b8eea744f6de009f8315c9fe6230f6798';
/// See also [personService].
@ProviderFor(personService)

View File

@@ -1,27 +1,27 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final searchServiceProvider = Provider(
(ref) => SearchService(
ref.watch(apiServiceProvider),
ref.watch(assetRepositoryProvider),
ref.watch(dbProvider),
),
);
class SearchService {
final ApiService _apiService;
final IAssetRepository _assetRepository;
final Isar _db;
final _log = Logger("SearchService");
SearchService(this._apiService, this._assetRepository);
SearchService(this._apiService, this._db);
Future<List<String>?> getSearchSuggestions(
SearchSuggestionType type, {
@@ -103,7 +103,7 @@ class SearchService {
return null;
}
return _assetRepository
return _db.assets
.getAllByRemoteId(response.assets.items.map((e) => e.id));
} catch (error, stackTrace) {
_log.severe("Failed to search for assets", error, stackTrace);

View File

@@ -1,17 +1,17 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class StackService {
StackService(this._api, this._assetRepository);
StackService(this._api, this._db);
final ApiService _api;
final IAssetRepository _assetRepository;
final Isar _db;
Future<StackResponseDto?> getStack(String stackId) async {
try {
@@ -61,7 +61,10 @@ class StackService {
removeAssets.add(asset);
}
await _assetRepository.updateAll(removeAssets);
_db.writeTxn(() async {
await _db.assets.putAll(removeAssets);
});
} catch (error) {
debugPrint("Error while deleting stack: $error");
}
@@ -71,6 +74,6 @@ class StackService {
final stackServiceProvider = Provider(
(ref) => StackService(
ref.watch(apiServiceProvider),
ref.watch(assetRepositoryProvider),
ref.watch(dbProvider),
),
);

View File

@@ -1,48 +1,68 @@
import 'package:collection/collection.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:http/http.dart';
import 'package:image_picker/image_picker.dart';
import 'package:immich_mobile/interfaces/partner_api.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/interfaces/user_api.interface.dart';
import 'package:immich_mobile/repositories/partner_api.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/repositories/user_api.repository.dart';
import 'package:immich_mobile/services/partner.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final userServiceProvider = Provider(
(ref) => UserService(
ref.watch(partnerApiRepositoryProvider),
ref.watch(userApiRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(syncServiceProvider),
ref.watch(partnerServiceProvider),
),
);
class UserService {
final IPartnerApiRepository _partnerApiRepository;
final IUserApiRepository _userApiRepository;
final IUserRepository _userRepository;
final ApiService _apiService;
final Isar _db;
final SyncService _syncService;
final PartnerService _partnerService;
final Logger _log = Logger("UserService");
UserService(
this._partnerApiRepository,
this._userApiRepository,
this._userRepository,
this._apiService,
this._db,
this._syncService,
this._partnerService,
);
Future<List<User>> getUsers({bool self = false}) =>
_userRepository.getAll(self: self);
Future<({String profileImagePath})?> uploadProfileImage(XFile image) async {
Future<List<User>?> _getAllUsers() async {
try {
return await _userApiRepository.createProfileImage(
name: image.name,
data: await image.readAsBytes(),
final dto = await _apiService.usersApi.searchUsers();
return dto?.map(User.fromSimpleUserDto).toList();
} catch (e) {
_log.warning("Failed get all users", e);
return null;
}
}
Future<List<User>> getUsersInDb({bool self = false}) async {
if (self) {
return _db.users.where().findAll();
}
final int userId = Store.get(StoreKey.currentUser).isarId;
return _db.users.where().isarIdNotEqualTo(userId).findAll();
}
Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async {
try {
return await _apiService.usersApi.createProfileImage(
MultipartFile.fromBytes(
'file',
await image.readAsBytes(),
filename: image.name,
),
);
} catch (e) {
_log.warning("Failed to upload profile image", e);
@@ -51,19 +71,13 @@ class UserService {
}
Future<List<User>?> getUsersFromServer() async {
List<User>? users;
try {
users = await _userApiRepository.getAll();
} catch (e) {
_log.warning("Failed to fetch users", e);
users = null;
}
final List<User> sharedBy =
await _partnerApiRepository.getAll(Direction.sharedByMe);
final List<User> sharedWith =
await _partnerApiRepository.getAll(Direction.sharedWithMe);
final List<User>? users = await _getAllUsers();
final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.by);
final List<User>? sharedWith =
await _partnerService.getPartners(PartnerDirection.with_);
if (users == null) {
if (users == null || sharedBy == null || sharedWith == null) {
_log.warning("Failed to refresh users");
return null;
}

View File

@@ -1,7 +1,7 @@
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
String getThumbnailUrl(
@@ -61,7 +61,7 @@ String getOriginalUrlForRemoteId(final String id) {
String getImageCacheKey(final Asset asset) {
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = asset.id == noDbId;
final isFromDto = asset.id == Isar.autoIncrement;
return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
}

View File

@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
import 'package:immich_mobile/utils/storage_indicator.dart';
import 'package:isar/isar.dart';
class ThumbnailImage extends ConsumerWidget {
/// The asset to show the thumbnail image for
@@ -46,7 +46,7 @@ class ThumbnailImage extends ConsumerWidget {
? context.primaryColor.darken(amount: 0.6)
: context.primaryColor.lighten(amount: 0.8);
// Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
final isFromDto = asset.id == noDbId;
final isFromDto = asset.id == Isar.autoIncrement;
Widget buildSelectionIcon(Asset asset) {
if (isSelected) {

View File

@@ -8,7 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/services/asset_description.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:logging/logging.dart';
@@ -29,16 +29,14 @@ class DescriptionInput extends HookConsumerWidget {
final focusNode = useFocusNode();
final isFocus = useState(false);
final isTextEmpty = useState(controller.text.isEmpty);
final assetService = ref.watch(assetServiceProvider);
final descriptionProvider = ref.watch(assetDescriptionServiceProvider);
final owner = ref.watch(currentUserProvider);
final hasError = useState(false);
final assetWithExif = ref.watch(assetDetailProvider(asset));
useEffect(
() {
assetService
.getDescription(asset)
.then((value) => controller.text = value);
controller.text = descriptionProvider.getAssetDescription(asset);
return null;
},
[assetWithExif.value],
@@ -47,7 +45,7 @@ class DescriptionInput extends HookConsumerWidget {
submitDescription(String description) async {
hasError.value = false;
try {
await assetService.setDescription(
await descriptionProvider.setDescription(
asset,
description,
);

View File

@@ -3,23 +3,23 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/interfaces/person_api.interface.dart';
import 'package:immich_mobile/providers/search/people.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:openapi/api.dart';
class PeoplePicker extends HookConsumerWidget {
const PeoplePicker({super.key, required this.onSelect, this.filter});
final Function(Set<Person>) onSelect;
final Set<Person>? filter;
final Function(Set<PersonResponseDto>) onSelect;
final Set<PersonResponseDto>? filter;
@override
Widget build(BuildContext context, WidgetRef ref) {
var imageSize = 45.0;
final people = ref.watch(getAllPeopleProvider);
final headers = ApiService.getRequestHeaders();
final selectedPeople = useState<Set<Person>>(filter ?? {});
final selectedPeople = useState<Set<PersonResponseDto>>(filter ?? {});
return people.widgetWhen(
onData: (people) {

View File

@@ -116,6 +116,7 @@ Class | Method | HTTP request | Description
*AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up |
*AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken |
*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets |
*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random |
*DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive |
*DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info |
*DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates |
@@ -124,6 +125,7 @@ Class | Method | HTTP request | Description
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports |
*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum |
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries |
@@ -136,7 +138,6 @@ Class | Method | HTTP request | Description
*LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} |
*LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate |
*MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers |
*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json |
*MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode |
*MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets |
*MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories |
@@ -171,6 +172,7 @@ Class | Method | HTTP request | Description
*SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random |
*SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart |
*ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license |
*ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about |
@@ -330,6 +332,7 @@ Class | Method | HTTP request | Description
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
- [JobCountsDto](doc//JobCountsDto.md)
- [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md)
- [JobStatusDto](doc//JobStatusDto.md)
@@ -337,14 +340,13 @@ Class | Method | HTTP request | Description
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
- [LicenseKeyDto](doc//LicenseKeyDto.md)
- [LicenseResponseDto](doc//LicenseResponseDto.md)
- [LoadTextualModelOnConnection](doc//LoadTextualModelOnConnection.md)
- [LogLevel](doc//LogLevel.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
- [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
- [MapTheme](doc//MapTheme.md)
- [MemoriesResponse](doc//MemoriesResponse.md)
- [MemoriesUpdate](doc//MemoriesUpdate.md)
- [MemoryCreateDto](doc//MemoryCreateDto.md)
@@ -377,6 +379,7 @@ Class | Method | HTTP request | Description
- [PurchaseResponse](doc//PurchaseResponse.md)
- [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- [RandomSearchDto](doc//RandomSearchDto.md)
- [RatingsResponse](doc//RatingsResponse.md)
- [RatingsUpdate](doc//RatingsUpdate.md)
- [ReactionLevel](doc//ReactionLevel.md)
@@ -450,6 +453,7 @@ Class | Method | HTTP request | Description
- [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md)
- [TrashResponseDto](doc//TrashResponseDto.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md)

View File

@@ -144,6 +144,7 @@ part 'model/image_format.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
part 'model/job_counts_dto.dart';
part 'model/job_create_dto.dart';
part 'model/job_name.dart';
part 'model/job_settings_dto.dart';
part 'model/job_status_dto.dart';
@@ -151,14 +152,13 @@ part 'model/library_response_dto.dart';
part 'model/library_stats_response_dto.dart';
part 'model/license_key_dto.dart';
part 'model/license_response_dto.dart';
part 'model/load_textual_model_on_connection.dart';
part 'model/log_level.dart';
part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
part 'model/manual_job_name.dart';
part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_response_dto.dart';
part 'model/map_theme.dart';
part 'model/memories_response.dart';
part 'model/memories_update.dart';
part 'model/memory_create_dto.dart';
@@ -191,6 +191,7 @@ part 'model/places_response_dto.dart';
part 'model/purchase_response.dart';
part 'model/purchase_update.dart';
part 'model/queue_status_dto.dart';
part 'model/random_search_dto.dart';
part 'model/ratings_response.dart';
part 'model/ratings_update.dart';
part 'model/reaction_level.dart';
@@ -264,6 +265,7 @@ part 'model/time_bucket_size.dart';
part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart';
part 'model/trash_response_dto.dart';
part 'model/update_album_dto.dart';
part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart';

View File

@@ -166,7 +166,6 @@ class ApiClient {
/// Returns a native instance of an OpenAPI class matching the [specified type][targetType].
static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) {
upgradeDto(value, targetType);
try {
switch (targetType) {
case 'String':
@@ -343,6 +342,8 @@ class ApiClient {
return JobCommandDto.fromJson(value);
case 'JobCountsDto':
return JobCountsDto.fromJson(value);
case 'JobCreateDto':
return JobCreateDto.fromJson(value);
case 'JobName':
return JobNameTypeTransformer().decode(value);
case 'JobSettingsDto':
@@ -357,8 +358,6 @@ class ApiClient {
return LicenseKeyDto.fromJson(value);
case 'LicenseResponseDto':
return LicenseResponseDto.fromJson(value);
case 'LoadTextualModelOnConnection':
return LoadTextualModelOnConnection.fromJson(value);
case 'LogLevel':
return LogLevelTypeTransformer().decode(value);
case 'LoginCredentialDto':
@@ -367,12 +366,12 @@ class ApiClient {
return LoginResponseDto.fromJson(value);
case 'LogoutResponseDto':
return LogoutResponseDto.fromJson(value);
case 'ManualJobName':
return ManualJobNameTypeTransformer().decode(value);
case 'MapMarkerResponseDto':
return MapMarkerResponseDto.fromJson(value);
case 'MapReverseGeocodeResponseDto':
return MapReverseGeocodeResponseDto.fromJson(value);
case 'MapTheme':
return MapThemeTypeTransformer().decode(value);
case 'MemoriesResponse':
return MemoriesResponse.fromJson(value);
case 'MemoriesUpdate':
@@ -437,6 +436,8 @@ class ApiClient {
return PurchaseUpdate.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
case 'RandomSearchDto':
return RandomSearchDto.fromJson(value);
case 'RatingsResponse':
return RatingsResponse.fromJson(value);
case 'RatingsUpdate':
@@ -583,6 +584,8 @@ class ApiClient {
return TranscodeHWAccelTypeTransformer().decode(value);
case 'TranscodePolicy':
return TranscodePolicyTypeTransformer().decode(value);
case 'TrashResponseDto':
return TrashResponseDto.fromJson(value);
case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value);
case 'UpdateAlbumUserDto':

View File

@@ -14,36 +14,30 @@ class CLIPConfig {
/// Returns a new [CLIPConfig] instance.
CLIPConfig({
required this.enabled,
required this.loadTextualModelOnConnection,
required this.modelName,
});
bool enabled;
LoadTextualModelOnConnection loadTextualModelOnConnection;
String modelName;
@override
bool operator ==(Object other) => identical(this, other) || other is CLIPConfig &&
other.enabled == enabled &&
other.loadTextualModelOnConnection == loadTextualModelOnConnection &&
other.modelName == modelName;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(loadTextualModelOnConnection.hashCode) +
(modelName.hashCode);
@override
String toString() => 'CLIPConfig[enabled=$enabled, loadTextualModelOnConnection=$loadTextualModelOnConnection, modelName=$modelName]';
String toString() => 'CLIPConfig[enabled=$enabled, modelName=$modelName]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'loadTextualModelOnConnection'] = this.loadTextualModelOnConnection;
json[r'modelName'] = this.modelName;
return json;
}
@@ -52,12 +46,12 @@ class CLIPConfig {
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static CLIPConfig? fromJson(dynamic value) {
upgradeDto(value, "CLIPConfig");
if (value is Map) {
final json = value.cast<String, dynamic>();
return CLIPConfig(
enabled: mapValueOfType<bool>(json, r'enabled')!,
loadTextualModelOnConnection: LoadTextualModelOnConnection.fromJson(json[r'loadTextualModelOnConnection'])!,
modelName: mapValueOfType<String>(json, r'modelName')!,
);
}
@@ -107,7 +101,6 @@ class CLIPConfig {
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'loadTextualModelOnConnection',
'modelName',
};
}

View File

@@ -22,14 +22,14 @@ class FacialRecognitionConfig {
bool enabled;
/// Minimum value: 0.1
/// Minimum value: 0
/// Maximum value: 2
double maxDistance;
/// Minimum value: 1
int minFaces;
/// Minimum value: 0.1
/// Minimum value: 0
/// Maximum value: 1
double minScore;

View File

@@ -1,107 +0,0 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// 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 LoadTextualModelOnConnection {
/// Returns a new [LoadTextualModelOnConnection] instance.
LoadTextualModelOnConnection({
required this.enabled,
required this.ttl,
});
bool enabled;
/// Minimum value: 0
num ttl;
@override
bool operator ==(Object other) => identical(this, other) || other is LoadTextualModelOnConnection &&
other.enabled == enabled &&
other.ttl == ttl;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(enabled.hashCode) +
(ttl.hashCode);
@override
String toString() => 'LoadTextualModelOnConnection[enabled=$enabled, ttl=$ttl]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'enabled'] = this.enabled;
json[r'ttl'] = this.ttl;
return json;
}
/// Returns a new [LoadTextualModelOnConnection] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static LoadTextualModelOnConnection? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return LoadTextualModelOnConnection(
enabled: mapValueOfType<bool>(json, r'enabled')!,
ttl: num.parse('${json[r'ttl']}'),
);
}
return null;
}
static List<LoadTextualModelOnConnection> listFromJson(dynamic json, {bool growable = false,}) {
final result = <LoadTextualModelOnConnection>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = LoadTextualModelOnConnection.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, LoadTextualModelOnConnection> mapFromJson(dynamic json) {
final map = <String, LoadTextualModelOnConnection>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = LoadTextualModelOnConnection.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of LoadTextualModelOnConnection-objects as value to a dart map
static Map<String, List<LoadTextualModelOnConnection>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<LoadTextualModelOnConnection>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = LoadTextualModelOnConnection.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'enabled',
'ttl',
};
}

View File

@@ -1,6 +1,5 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/providers/activity_service.provider.dart';
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
import 'package:mocktail/mocktail.dart';
@@ -26,7 +25,7 @@ void main() {
test('Returns the proper count family', () async {
when(
() => activityMock.getStatistics('test-album', assetId: 'test-asset'),
).thenAnswer((_) async => const ActivityStats(comments: 5));
).thenAnswer((_) async => 5);
// Read here to make the getStatistics call
container.read(activityStatisticsProvider('test-album', 'test-asset'));
@@ -51,7 +50,7 @@ void main() {
test('Adds activity', () async {
when(
() => activityMock.getStatistics('test-album'),
).thenAnswer((_) async => const ActivityStats(comments: 10));
).thenAnswer((_) async => 10);
final provider = activityStatisticsProvider('test-album');
container.listen(
@@ -72,7 +71,7 @@ void main() {
test('Removes activity', () async {
when(
() => activityMock.getStatistics('new-album', assetId: 'test-asset'),
).thenAnswer((_) async => const ActivityStats(comments: 10));
).thenAnswer((_) async => 10);
final provider = activityStatisticsProvider('new-album', 'test-asset');
container.listen(

View File

@@ -3167,6 +3167,123 @@
]
}
},
"/map/style.json": {
"get": {
"operationId": "getMapStyle",
"parameters": [
{
"name": "key",
"required": false,
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "theme",
"required": true,
"in": "query",
"schema": {
"$ref": "#/components/schemas/MapTheme"
}
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Map"
]
}
},
"/map/tiles.json": {
"get": {
"operationId": "getTilesJson",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": ""
}
},
"tags": [
"Map"
]
}
},
"/map/tiles/{z}/{x}/{y}.{format}": {
"get": {
"operationId": "getTiles",
"parameters": [
{
"name": "format",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "x",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "y",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "z",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Map"
]
}
},
"/memories": {
"get": {
"operationId": "searchMemories",
@@ -8656,16 +8773,12 @@
"enabled": {
"type": "boolean"
},
"loadTextualModelOnConnection": {
"$ref": "#/components/schemas/LoadTextualModelOnConnection"
},
"modelName": {
"type": "string"
}
},
"required": [
"enabled",
"loadTextualModelOnConnection",
"modelName"
],
"type": "object"
@@ -9123,7 +9236,7 @@
"maxDistance": {
"format": "double",
"maximum": 2,
"minimum": 0.1,
"minimum": 0,
"type": "number"
},
"minFaces": {
@@ -9133,7 +9246,7 @@
"minScore": {
"format": "double",
"maximum": 1,
"minimum": 0.1,
"minimum": 0,
"type": "number"
},
"modelName": {
@@ -9506,17 +9619,6 @@
],
"type": "object"
},
"LoadTextualModelOnConnection": {
"properties": {
"enabled": {
"type": "boolean"
}
},
"required": [
"enabled"
],
"type": "object"
},
"LogLevel": {
"enum": [
"verbose",
@@ -9661,6 +9763,13 @@
],
"type": "object"
},
"MapTheme": {
"enum": [
"light",
"dark"
],
"type": "string"
},
"MemoriesResponse": {
"properties": {
"enabled": {
@@ -10876,12 +10985,6 @@
"loginPageMessage": {
"type": "string"
},
"mapDarkStyleUrl": {
"type": "string"
},
"mapLightStyleUrl": {
"type": "string"
},
"oauthButtonText": {
"type": "string"
},
@@ -10897,8 +11000,6 @@
"isInitialized",
"isOnboarded",
"loginPageMessage",
"mapDarkStyleUrl",
"mapLightStyleUrl",
"oauthButtonText",
"trashDays",
"userDeleteDelay"

View File

@@ -1142,13 +1142,8 @@ export type SystemConfigLoggingDto = {
enabled: boolean;
level: LogLevel;
};
export type LoadTextualModelOnConnection = {
enabled: boolean;
ttl: number;
};
export type ClipConfig = {
enabled: boolean;
loadTextualModelOnConnection: LoadTextualModelOnConnection;
modelName: string;
};
export type DuplicateDetectionConfig = {

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240924@sha256:fff4358d435065a626c64a4c015cbfce6ee714b05fabe39aa0d83d8cff3951f2 AS dev
FROM ghcr.io/immich-app/base-server-dev:20240917@sha256:3d92952d37cd68f5bf641aa80e5cc034e0d11f3774147f5db8db93138cfa5b3b AS dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -41,7 +41,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240924@sha256:af3089fe48d7ff162594bd7edfffa56ba4e7014ad10ad69c4ebfd428e39b06ff
FROM ghcr.io/immich-app/base-server-prod:20240917@sha256:67a40250f03812fe1e6f6b6345a3c7b71b3a9f24c65ed4862e82be8b3e53d23a
WORKDIR /usr/src/app
ENV NODE_ENV=production \

655
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,7 @@
"openid-client": "^5.4.3",
"pg": "^8.11.3",
"picomatch": "^4.0.2",
"pmtiles": "^3.0.7",
"react": "^18.3.1",
"react-email": "^3.0.0",
"reflect-metadata": "^0.2.0",

View File

@@ -5,7 +5,7 @@
"sources": {
"protomaps": {
"type": "vector",
"url": "https://tiles.immich.cloud/v1.json"
"url": "http://localhost:2283/api/map/tiles.json"
}
},
"layers": [

View File

@@ -5,7 +5,7 @@
"sources": {
"protomaps": {
"type": "vector",
"url": "https://tiles.immich.cloud/v1.json"
"url": "http://localhost:2283/api/map/tiles.json"
}
},
"layers": [

6367
server/resources/tiles.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -120,9 +120,6 @@ export interface SystemConfig {
clip: {
enabled: boolean;
modelName: string;
loadTextualModelOnConnection: {
enabled: boolean;
};
};
duplicateDetection: {
enabled: boolean;
@@ -273,9 +270,6 @@ export const defaults = Object.freeze<SystemConfig>({
clip: {
enabled: true,
modelName: 'ViT-B-32__openai',
loadTextualModelOnConnection: {
enabled: false,
},
},
duplicateDetection: {
enabled: true,

View File

@@ -1,5 +1,6 @@
import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
import { Controller, Get, HttpCode, HttpStatus, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { AuthDto } from 'src/dtos/auth.dto';
import {
MapMarkerDto,
@@ -27,4 +28,12 @@ export class MapController {
reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise<MapReverseGeocodeResponseDto[]> {
return this.service.reverseGeocode(dto);
}
@Authenticated()
@Get('pmtiles')
@HttpCode(HttpStatus.OK)
async getPMTiles(@Res() res: Response) {
const tileStream = await this.service.getLocalTilesStream();
return tileStream.pipe(res);
}
}

View File

@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsNumber, IsObject, IsString, Max, Min, ValidateNested } from 'class-validator';
import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class TaskConfig {
@@ -14,17 +14,7 @@ export class ModelConfig extends TaskConfig {
modelName!: string;
}
export class LoadTextualModelOnConnection {
@ValidateBoolean()
enabled!: boolean;
}
export class CLIPConfig extends ModelConfig {
@Type(() => LoadTextualModelOnConnection)
@ValidateNested()
@IsObject()
loadTextualModelOnConnection!: LoadTextualModelOnConnection;
}
export class CLIPConfig extends ModelConfig {}
export class DuplicateDetectionConfig extends TaskConfig {
@IsNumber()
@@ -37,14 +27,14 @@ export class DuplicateDetectionConfig extends TaskConfig {
export class FacialRecognitionConfig extends ModelConfig {
@IsNumber()
@Min(0.1)
@Min(0)
@Max(1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })
minScore!: number;
@IsNumber()
@Min(0.1)
@Min(0)
@Max(2)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })

View File

@@ -46,11 +46,6 @@ export interface Face {
score: number;
}
export enum LoadTextModelActions {
LOAD,
UNLOAD,
}
export type FacialRecognitionResponse = { [ModelTask.FACIAL_RECOGNITION]: Face[] } & VisualResponse;
export type DetectedFaces = { faces: Face[] } & VisualResponse;
export type MachineLearningRequest = ClipVisualRequest | ClipTextualRequest | FacialRecognitionRequest;
@@ -59,5 +54,4 @@ export interface IMachineLearningRepository {
encodeImage(url: string, imagePath: string, config: ModelOptions): Promise<number[]>;
encodeText(url: string, text: string, config: ModelOptions): Promise<number[]>;
detectFaces(url: string, imagePath: string, config: FaceDetectionOptions): Promise<DetectedFaces>;
prepareTextModel(url: string, config: ModelOptions, action: LoadTextModelActions): Promise<void>;
}

View File

@@ -483,13 +483,16 @@ UPDATE "albums"
SET
"albumThumbnailAssetId" = (
SELECT
"album_assets"."assetsId"
"albums_assets2"."assetsId"
FROM
"albums_assets_assets" "album_assets"
INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id"
AND "assets"."deletedAt" IS NULL
"assets" "assets",
"albums_assets_assets" "albums_assets2"
WHERE
"album_assets"."albumsId" = "albums"."id"
(
"albums_assets2"."assetsId" = "assets"."id"
AND "albums_assets2"."albumsId" = "albums"."id"
)
AND ("assets"."deletedAt" IS NULL)
ORDER BY
"assets"."fileCreatedAt" DESC
LIMIT
@@ -502,21 +505,17 @@ WHERE
SELECT
1
FROM
"albums_assets_assets" "album_assets"
INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id"
AND "assets"."deletedAt" IS NULL
"albums_assets_assets" "albums_assets"
WHERE
"album_assets"."albumsId" = "albums"."id"
"albums"."id" = "albums_assets"."albumsId"
)
OR "albums"."albumThumbnailAssetId" IS NOT NULL
AND NOT EXISTS (
SELECT
1
FROM
"albums_assets_assets" "album_assets"
INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id"
AND "assets"."deletedAt" IS NULL
"albums_assets_assets" "albums_assets"
WHERE
"album_assets"."albumsId" = "albums"."id"
AND "albums"."albumThumbnailAssetId" = "album_assets"."assetsId"
"albums"."id" = "albums_assets"."albumsId"
AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"
)

View File

@@ -277,26 +277,32 @@ export class AlbumRepository implements IAlbumRepository {
@GenerateSql()
async updateThumbnails(): Promise<number | undefined> {
// Subquery for getting a new thumbnail.
const builder = this.dataSource
.createQueryBuilder('albums_assets_assets', 'album_assets')
.innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"')
.where('"album_assets"."albumsId" = "albums"."id"');
const newThumbnail = builder
.clone()
.select('"album_assets"."assetsId"')
.orderBy('"assets"."fileCreatedAt"', 'DESC')
const newThumbnail = this.assetRepository
.createQueryBuilder('assets')
.select('albums_assets2.assetsId')
.addFrom('albums_assets_assets', 'albums_assets2')
.where('albums_assets2.assetsId = assets.id')
.andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query
.orderBy('assets.fileCreatedAt', 'DESC')
.limit(1);
const hasAssets = builder.clone().select('1');
const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"');
// Using dataSource, because there is no direct access to albums_assets_assets.
const albumHasAssets = this.dataSource
.createQueryBuilder()
.select('1')
.from('albums_assets_assets', 'albums_assets')
.where('"albums"."id" = "albums_assets"."albumsId"');
const albumContainsThumbnail = albumHasAssets
.clone()
.andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"');
const updateAlbums = this.repository
.createQueryBuilder('albums')
.update(AlbumEntity)
.set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` })
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`)
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`);
.where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`)
.orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`);
const result = await updateAlbums.execute();

View File

@@ -9,7 +9,6 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
import { SystemConfigCore } from 'src/cores/system-config.core';
import {
ArgsOf,
ClientEventMap,
@@ -20,8 +19,6 @@ import {
ServerEventMap,
} from 'src/interfaces/event.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMachineLearningRepository, LoadTextModelActions } from 'src/interfaces/machine-learning.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { AuthService } from 'src/services/auth.service';
import { Instrumentation } from 'src/utils/instrumentation';
@@ -36,7 +33,6 @@ type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler<T>[] }>;
@Injectable()
export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository {
private emitHandlers: EmitHandlers = {};
private configCore: SystemConfigCore;
@WebSocketServer()
private server?: Server;
@@ -45,11 +41,8 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
private moduleRef: ModuleRef,
private eventEmitter: EventEmitter2,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
) {
this.logger.setContext(EventRepository.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
}
afterInit(server: Server) {
@@ -75,21 +68,6 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' },
});
if ('background' in client.handshake.query && client.handshake.query.background === 'false') {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
if (machineLearning.clip.loadTextualModelOnConnection.enabled) {
try {
console.log(this.server);
this.machineLearningRepository.prepareTextModel(
machineLearning.url,
machineLearning.clip,
LoadTextModelActions.LOAD,
);
} catch (error) {
this.logger.warn(error);
}
}
}
await client.join(auth.user.id);
if (auth.session) {
await client.join(auth.session.id);
@@ -105,21 +83,6 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
async handleDisconnect(client: Socket) {
this.logger.log(`Websocket Disconnect: ${client.id}`);
await client.leave(client.nsp.name);
if ('background' in client.handshake.query && client.handshake.query.background === 'false') {
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
if (machineLearning.clip.loadTextualModelOnConnection.enabled && this.server?.engine.clientsCount == 0) {
try {
this.machineLearningRepository.prepareTextModel(
machineLearning.url,
machineLearning.clip,
LoadTextModelActions.UNLOAD,
);
this.logger.debug('sent request to unload text model');
} catch (error) {
this.logger.warn(error);
}
}
}
}
on<T extends EmitEvent>(event: T, handler: EmitHandler<T>): void {

View File

@@ -7,7 +7,6 @@ import {
FaceDetectionOptions,
FacialRecognitionResponse,
IMachineLearningRepository,
LoadTextModelActions,
MachineLearningRequest,
ModelPayload,
ModelTask,
@@ -21,9 +20,13 @@ const errorPrefix = 'Machine learning request';
@Injectable()
export class MachineLearningRepository implements IMachineLearningRepository {
private async predict<T>(url: string, payload: ModelPayload, config: MachineLearningRequest): Promise<T> {
const formData = await this.getFormData(config, payload);
const formData = await this.getFormData(payload, config);
const res = await this.fetchData(url, '/predict', formData);
const res = await fetch(new URL('/predict', url), { method: 'POST', body: formData }).catch(
(error: Error | any) => {
throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`);
},
);
if (res.status >= 400) {
throw new Error(`${errorPrefix} '${JSON.stringify(config)}' failed with status ${res.status}: ${res.statusText}`);
@@ -31,30 +34,6 @@ export class MachineLearningRepository implements IMachineLearningRepository {
return res.json();
}
private async fetchData(url: string, path: string, formData?: FormData): Promise<Response> {
const res = await fetch(new URL(path, url), { method: 'POST', body: formData }).catch((error: Error | any) => {
throw new Error(`${errorPrefix} to "${url}" failed with ${error?.cause || error}`);
});
return res;
}
private prepareTextModelUrl: Record<LoadTextModelActions, string> = {
[LoadTextModelActions.LOAD]: '/load',
[LoadTextModelActions.UNLOAD]: '/unload',
};
async prepareTextModel(url: string, { modelName }: CLIPConfig, actions: LoadTextModelActions) {
try {
const request = { [ModelTask.SEARCH]: { [ModelType.TEXTUAL]: { modelName } } };
const formData = await this.getFormData(request);
const res = await this.fetchData(url, this.prepareTextModelUrl[actions], formData);
if (res.status >= 400) {
throw new Error(`${errorPrefix} Loadings textual model failed with status ${res.status}: ${res.statusText}`);
}
} catch (error) {}
}
async detectFaces(url: string, imagePath: string, { modelName, minScore }: FaceDetectionOptions) {
const request = {
[ModelTask.FACIAL_RECOGNITION]: {
@@ -82,17 +61,16 @@ export class MachineLearningRepository implements IMachineLearningRepository {
return response[ModelTask.SEARCH];
}
private async getFormData(config: MachineLearningRequest, payload?: ModelPayload): Promise<FormData> {
private async getFormData(payload: ModelPayload, config: MachineLearningRequest): Promise<FormData> {
const formData = new FormData();
formData.append('entries', JSON.stringify(config));
if (payload) {
if ('imagePath' in payload) {
formData.append('image', new Blob([await readFile(payload.imagePath)]));
} else if ('text' in payload) {
formData.append('text', payload.text);
} else {
throw new Error('Invalid input');
}
if ('imagePath' in payload) {
formData.append('image', new Blob([await readFile(payload.imagePath)]));
} else if ('text' in payload) {
formData.append('text', payload.text);
} else {
throw new Error('Invalid input');
}
return formData;

View File

@@ -6,6 +6,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { getMyPartnerIds } from 'src/utils/asset.util';
@@ -18,6 +19,7 @@ export class MapService {
@Inject(IPartnerRepository) private partnerRepository: IPartnerRepository,
@Inject(IMapRepository) private mapRepository: IMapRepository,
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
) {
this.logger.setContext(MapService.name);
this.configCore = SystemConfigCore.create(systemMetadataRepository, logger);
@@ -43,6 +45,11 @@ export class MapService {
return this.mapRepository.getMapMarkers(userIds, albumIds, options);
}
async getLocalTilesStream() {
const readStream = await this.storageRepository.createReadStream('/usr/src/app/uploads/tiles/tiles.pmtiles');
return readStream.stream;
}
async reverseGeocode(dto: MapReverseGeocodeDto) {
const { lat: latitude, lon: longitude } = dto;
// eventually this should probably return an array of results

View File

@@ -2,4 +2,4 @@
This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing).
When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [the server project](../server).
When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project).

258
web/package-lock.json generated
View File

@@ -759,9 +759,9 @@
}
},
"node_modules/@faker-js/faker": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.1.tgz",
"integrity": "sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==",
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz",
"integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==",
"dev": true,
"funding": [
{
@@ -1875,9 +1875,9 @@
"integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA=="
},
"node_modules/@sveltejs/adapter-static": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.5.tgz",
"integrity": "sha512-kFJR7RxeB6FBvrKZWAEzIALatgy11ISaaZbcPup8JdWUdrmmfUHHTJ738YHJTEfnCiiXi6aX8Q6ePY7tnSMD6Q==",
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.4.tgz",
"integrity": "sha512-Qm4GAHCnRXwfWG9/AtnQ7mqjyjTs7i0Opyb8H2KH9rMR7fLxqiPx/tXeoE6HHo66+72CjyOb4nFH3lrejY4vzA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -1885,9 +1885,9 @@
}
},
"node_modules/@sveltejs/enhanced-img": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.8.tgz",
"integrity": "sha512-n66u46ZeqHltiTm0BEjWptYmCrCY0EltEEvakmC7d5o5ZejDbOvOWm914mebbRKaP2Bezv65TNCod/wqvw/0KA==",
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.4.tgz",
"integrity": "sha512-eX+ob5uWr0bTLMKeG9nhhM84aR88hqiLiyEfWZPX7ijhk/wlmYSUX9nOiaVHh2ct1U+Ju9Hhb90Copw+ZNOB8w==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1901,9 +1901,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.5.28",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.28.tgz",
"integrity": "sha512-/O7pvFGBsQPcFa9UrW8eUC5uHTOXLsUp3SN0dY6YmRAL9nfPSrJsSJk//j5vMpinSshzUjteAFcfQTU+04Ka1w==",
"version": "2.5.26",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.26.tgz",
"integrity": "sha512-8l1JTIM2L+bS8ebq1E+nGjv/YSKSnD9Q19bYIUkc41vaEG2JjVUx6ikvPIJv2hkQAuqJLzoPrXlKk4KcyWOv3Q==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -2318,17 +2318,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/type-utils": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -2352,16 +2352,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2381,14 +2381,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2399,14 +2399,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
"integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -2424,9 +2424,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2438,14 +2438,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -2493,16 +2493,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2516,13 +2516,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -2534,9 +2534,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz",
"integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2557,8 +2557,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "2.1.1",
"vitest": "2.1.1"
"@vitest/browser": "2.1.0",
"vitest": "2.1.0"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -2567,14 +2567,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
"integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -2583,9 +2583,9 @@
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
"integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2597,7 +2597,7 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/spy": "2.1.1",
"@vitest/spy": "2.1.0",
"msw": "^2.3.5",
"vite": "^5.0.0"
},
@@ -2624,13 +2624,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
"integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.1",
"@vitest/utils": "2.1.0",
"pathe": "^1.1.2"
},
"funding": {
@@ -2638,13 +2638,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
"integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"magic-string": "^0.30.11",
"pathe": "^1.1.2"
},
@@ -2652,10 +2652,23 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
"integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2666,13 +2679,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
"integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -2680,10 +2693,23 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@zoom-image/core": {
"version": "0.38.0",
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.38.0.tgz",
"integrity": "sha512-rA6/qTGfsRtWRs+WfMF0dIs+Ft9GBFusxXzEqqFsQa/0iYtN0MmOiuKzXGYPcIFKTbmQW/qqk0afIBtWd9163g==",
"version": "0.37.1",
"resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.37.1.tgz",
"integrity": "sha512-mIJaZJBi3jvOD2gtzoSe4yhnxfvx7GcYlVTLoJE6VPawb3Ei5dvHuRRXa8/dNHtCf1Xf2RNSEm1Za2+TqkAiBQ==",
"license": "MIT",
"dependencies": {
"@namnode/store": "^0.1.0"
@@ -2694,12 +2720,12 @@
}
},
"node_modules/@zoom-image/svelte": {
"version": "0.2.22",
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.22.tgz",
"integrity": "sha512-lExo4M511/HtkmCsBzV5f8ABs8bEMZGtIrwl1pJro77iJ+5j9Yt7KUlPs6o+Yp028T6fqGJUsOCxCNWNZn9BIg==",
"version": "0.2.21",
"resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.21.tgz",
"integrity": "sha512-242xKpIaVZC/cymvNF4+JlcKwAaM9l3W2QS4DHSsnqT8xvPBgBgns+1lqOuYYKSAa85DB1UL0NMBhTg8Gk4RpA==",
"license": "MIT",
"dependencies": {
"@zoom-image/core": "0.38.0"
"@zoom-image/core": "0.37.1"
},
"funding": {
"type": "github",
@@ -3838,9 +3864,9 @@
}
},
"node_modules/eslint-plugin-svelte": {
"version": "2.44.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.44.0.tgz",
"integrity": "sha512-wav4MOs02vBb1WjvTCYItwJCxMkuk2Z4p+K/eyjL0N/z7ahXLP+0LtQQjiKc2ezuif7GnZLbD1F3o1VHzSvdVg==",
"version": "2.43.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz",
"integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3854,7 +3880,7 @@
"postcss-safe-parser": "^6.0.0",
"postcss-selector-parser": "^6.1.0",
"semver": "^7.6.2",
"svelte-eslint-parser": "^0.41.1"
"svelte-eslint-parser": "^0.41.0"
},
"engines": {
"node": "^14.17.0 || >=16.0.0"
@@ -7134,9 +7160,9 @@
}
},
"node_modules/svelte-eslint-parser": {
"version": "0.41.1",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz",
"integrity": "sha512-08ndI6zTghzI8SuJAFpvMbA/haPSGn3xz19pjre19yYMw8Nw/wQJ2PrZBI/L8ijGTgtkWCQQiLLy+Z1tfaCwNA==",
"version": "0.41.0",
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz",
"integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -7652,9 +7678,9 @@
"peer": true
},
"node_modules/tailwindcss": {
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"version": "3.4.11",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8220,9 +8246,9 @@
}
},
"node_modules/vite-node": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz",
"integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -8256,19 +8282,19 @@
}
},
"node_modules/vitest": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz",
"integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/expect": "2.1.1",
"@vitest/mocker": "2.1.1",
"@vitest/pretty-format": "^2.1.1",
"@vitest/runner": "2.1.1",
"@vitest/snapshot": "2.1.1",
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/expect": "2.1.0",
"@vitest/mocker": "2.1.0",
"@vitest/pretty-format": "^2.1.0",
"@vitest/runner": "2.1.0",
"@vitest/snapshot": "2.1.0",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"debug": "^4.3.6",
"magic-string": "^0.30.11",
@@ -8279,7 +8305,7 @@
"tinypool": "^1.0.0",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
"vite-node": "2.1.1",
"vite-node": "2.1.0",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -8294,8 +8320,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "2.1.1",
"@vitest/ui": "2.1.1",
"@vitest/browser": "2.1.0",
"@vitest/ui": "2.1.0",
"happy-dom": "*",
"jsdom": "*"
},

View File

@@ -75,21 +75,6 @@
</FormatMessage>
</p>
</SettingInputField>
<SettingAccordion
key="Preload clip model"
title={$t('admin.machine_learning_preload_model')}
subtitle={$t('admin.machine_learning_preload_model_setting_description')}
>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title={$t('admin.machine_learning_preload_model_enabled')}
subtitle={$t('admin.machine_learning_preload_model_enabled_description')}
bind:checked={config.machineLearning.clip.loadTextualModelOnConnection.enabled}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
/>
</div>
</SettingAccordion>
</div>
</SettingAccordion>
@@ -160,7 +145,7 @@
desc={$t('admin.machine_learning_min_detection_score_description')}
bind:value={config.machineLearning.facialRecognition.minScore}
step="0.1"
min={0.1}
min={0}
max={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.minScore !==
@@ -173,7 +158,7 @@
desc={$t('admin.machine_learning_max_recognition_distance_description')}
bind:value={config.machineLearning.facialRecognition.maxDistance}
step="0.1"
min={0.1}
min={0}
max={2}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
isEdited={config.machineLearning.facialRecognition.maxDistance !==

View File

@@ -7,7 +7,6 @@
export let id: string;
export let albumName: string;
export let isOwned: boolean;
export let onUpdate: (albumName: string) => void;
$: newAlbumName = albumName;
@@ -17,17 +16,17 @@
}
try {
({ albumName } = await updateAlbumInfo({
await updateAlbumInfo({
id,
updateAlbumDto: {
albumName: newAlbumName,
},
}));
onUpdate(albumName);
});
} catch (error) {
handleError(error, $t('errors.unable_to_save_album'));
return;
}
albumName = newAlbumName;
};
</script>

View File

@@ -116,12 +116,6 @@
"machine_learning_min_detection_score_description": "Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.",
"machine_learning_min_recognized_faces": "Minimum recognized faces",
"machine_learning_min_recognized_faces_description": "The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.",
"machine_learning_preload_model": "Preload model",
"machine_learning_preload_model_enabled": "Enable preload model",
"machine_learning_preload_model_enabled_description": "Preload the textual model during the connexion instead of during the first search",
"machine_learning_preload_model_setting_description": "Preload the textual model during the connexion",
"machine_learning_preload_model_ttl": "Inactivity time before a model in unloaded",
"machine_learning_preload_model_ttl_description": "Preload the textual model during the connexion",
"machine_learning_settings": "Machine Learning Settings",
"machine_learning_settings_description": "Manage machine learning features and settings",
"machine_learning_smart_search": "Smart Search",

View File

@@ -35,9 +35,6 @@ const websocket: Socket<Events> = io({
reconnection: true,
forceNew: true,
autoConnect: false,
query: {
background: false,
},
});
export const websocketStore = {

View File

@@ -589,12 +589,7 @@
{#if viewMode !== ViewMode.SELECT_THUMBNAIL}
<!-- ALBUM TITLE -->
<section class="pt-8 md:pt-24">
<AlbumTitle
id={album.id}
albumName={album.albumName}
{isOwned}
onUpdate={(albumName) => (album.albumName = albumName)}
/>
<AlbumTitle id={album.id} bind:albumName={album.albumName} {isOwned} />
{#if album.assetCount > 0}
<AlbumSummary {album} />