Compare commits

..

1 Commits

Author SHA1 Message Date
Jason Rasmussen
8af7a9c1b9 refactor: orientation 2024-09-19 12:06:20 -04:00
562 changed files with 6960 additions and 8362 deletions

View File

@@ -33,7 +33,6 @@
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a>
<a href="readme_i18n/README_vi_VN.md">Tiếng Việt</a>
</p>

206
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.20",
"version": "2.2.19",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.20",
"version": "2.2.19",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -52,7 +52,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.116.0",
"version": "1.115.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -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

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.20",
"version": "2.2.19",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -51,4 +51,5 @@ services:
volumes:
- /usr/lib/wsl:/usr/lib/wsl
environment:
- LD_LIBRARY_PATH=/usr/lib/wsl/lib
- LIBVA_DRIVER_NAME=d3d12

View File

@@ -333,11 +333,7 @@ You may need to add mount points or docker volumes for the following internal co
- `immich-machine-learning:/.cache`
- `redis:/data`
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.
:::note Docker Compose Volumes
The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
:::
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`.
For a further hardened system, you can add the following block to every container except for `immich_postgres`.

View File

@@ -64,43 +64,3 @@ Below is an example config for Apache2 site configuration.
ProxyPreserveHost On
</VirtualHost>
```
### Traefik Proxy example config
The example below is for Traefik version 3.
The most important is to increase the `respondingTimeouts` of the entrypoint used by immich. In this example of entrypoint `websecure` for port `443`. Per default it's set to 60s which leeds to videos stop uploading after 1 minute (Error Code 499). With this config it will fail after 10 minutes which is in most cases enough. Increase it if needed.
`traefik.yaml`
```yaml
[...]
entryPoints:
websecure:
address: :443
# this section needs to be added
transport:
respondingTimeouts:
readTimeout: 600s
idleTimeout: 600s
writeTimeout: 600s
```
The second part is in the `docker-compose.yml` file where immich is in. Add the Traefik specific labels like in the example.
`docker-compose.yml`
```yaml
services:
immich-server:
[...]
labels:
traefik.enable: true
# increase readingTimeouts for the entrypoint used here
traefik.http.routers.immich.entrypoints: websecure
traefik.http.routers.immich.rule: Host(`immich.your-domain.com`)
traefik.http.services.immich.loadbalancer.server.port: 3001
```
Keep in mind, that Traefik needs to communicate with the network where immich is in, usually done
by adding the Traefik network to the `immich-server`.

View File

@@ -3,7 +3,6 @@ sidebar_position: 1
---
import AppArchitecture from './img/app-architecture.png';
import MobileArchitecture from './img/immich_mobile_architecture.svg';
# Architecture
@@ -29,14 +28,7 @@ All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for
### Mobile App
The mobile app is written in [Dart](https://dart.dev/) using [Flutter](https://flutter.dev/). Below is an architecture overview:
<MobileArchitecture className="p-4 dark:bg-immich-dark-primary my-4" />
The diagrams shows the target architecture, the current state of the code-base is not always following the architecture yet. New code and contributions should follow this architecture.
Currently, it uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management (providers).
Entities and Models are the two types of data classes used. While entities are stored in the on-device database, models are ephemeral and only kept in memory.
The Repositories should be the only place where other data classes are used internally (such as OpenAPI DTOs). However, their interfaces must not use foreign data classes!
The mobile app is written in [Flutter](https://flutter.dev/). It uses [Isar Database](https://isar.dev/) for a local database and [Riverpod](https://riverpod.dev/) for state management.
### Web Client

View File

@@ -1,104 +0,0 @@
<mxfile host="app.diagrams.net" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36" version="24.7.16">
<diagram name="Page-1" id="Bp2gX--FtC4sSMWxsLrs">
<mxGraphModel dx="1728" dy="954" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" pageWidth="850" pageHeight="1100" background="none" math="0" shadow="0">
<root>
<mxCell id="0" />
<mxCell id="1" parent="0" />
<mxCell id="zHhczcy2-Jv_nqmJUiNH-1" value="" style="verticalLabelPosition=bottom;verticalAlign=top;html=1;shape=mxgraph.basic.polygon;polyCoords=[[0.25,0],[0.75,0],[1,0.25],[1,0.75],[0.75,1],[0.25,1],[0,0.75],[0,0.25]];polyline=0;strokeWidth=4;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
<mxGeometry x="280" y="217.5" width="465" height="465" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-2" value="&lt;b&gt;&lt;font style=&quot;font-size: 22px;&quot;&gt;Mobile App&lt;/font&gt;&lt;/b&gt;" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;rounded=1;fontColor=#ffffff;" vertex="1" parent="1">
<mxGeometry x="442.5" y="225" width="140" height="40" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-25" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-4" target="zHhczcy2-Jv_nqmJUiNH-5">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-4" value="Services" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FFB400;" vertex="1" parent="1">
<mxGeometry x="530" y="420" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-26" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-12">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-5" value="Repositories" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#1E83F7;" vertex="1" parent="1">
<mxGeometry x="650" y="420" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-24" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-4">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-6" value="Providers" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#ED79B5;" vertex="1" parent="1">
<mxGeometry x="410" y="420" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-29" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=0;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-8">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-30" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.75;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-7" target="zHhczcy2-Jv_nqmJUiNH-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-7" value="Pages" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
<mxGeometry x="290" y="480" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-31" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.25;entryDx=0;entryDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-8" target="zHhczcy2-Jv_nqmJUiNH-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-8" value="Widgets" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#FA2921;" vertex="1" parent="1">
<mxGeometry x="290" y="360" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-11" value="User" style="shape=umlActor;verticalLabelPosition=bottom;verticalAlign=top;html=1;outlineConnect=0;rounded=1;fillColor=#4251B0;" vertex="1" parent="1">
<mxGeometry x="180" y="368.5" width="81.5" height="163" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-12" value="platform&lt;div&gt;system&lt;/div&gt;" style="rhombus;whiteSpace=wrap;html=1;rounded=1;fillColor=#ED79B5;" vertex="1" parent="1">
<mxGeometry x="800" y="410" width="80" height="80" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-13" value="on-device&lt;div&gt;database&lt;/div&gt;" style="shape=cylinder3;whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;size=15;rounded=1;fillColor=#FA2921;" vertex="1" parent="1">
<mxGeometry x="810" y="310" width="60" height="80" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-14" value="server" style="ellipse;shape=cloud;whiteSpace=wrap;html=1;rounded=1;fillColor=#FFB400;" vertex="1" parent="1">
<mxGeometry x="780" y="500" width="120" height="80" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-16" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.75;exitDx=0;exitDy=0;entryX=0.07;entryY=0.4;entryDx=0;entryDy=0;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-14">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-39" value="OpenAPI" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];rounded=1;labelBackgroundColor=#1E83F7;" vertex="1" connectable="0" parent="zHhczcy2-Jv_nqmJUiNH-16">
<mxGeometry x="0.0697" y="1" relative="1" as="geometry">
<mxPoint x="8" y="10" as="offset" />
</mxGeometry>
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-23" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.75;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-6" target="zHhczcy2-Jv_nqmJUiNH-6">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-27" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.25;exitDx=0;exitDy=0;entryX=0;entryY=1;entryDx=0;entryDy=-15;entryPerimeter=0;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-5" target="zHhczcy2-Jv_nqmJUiNH-13">
<mxGeometry relative="1" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-34" style="edgeStyle=none;rounded=1;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;dashed=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-3">
<mxGeometry relative="1" as="geometry">
<mxPoint x="810" y="360" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-36" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" source="zHhczcy2-Jv_nqmJUiNH-9">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="512.08" y="665" as="sourcePoint" />
<mxPoint x="512.08" y="265" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-37" value="UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
<mxGeometry x="387.5" y="640" width="70" height="30" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-38" value="non-UI part" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontStyle=1;fontSize=14;fontColor=#FFFFFF;" vertex="1" parent="1">
<mxGeometry x="550" y="640" width="90" height="30" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-41" value="" style="endArrow=none;dashed=1;html=1;rounded=1;" edge="1" parent="1" target="zHhczcy2-Jv_nqmJUiNH-9">
<mxGeometry width="50" height="50" relative="1" as="geometry">
<mxPoint x="512.08" y="665" as="sourcePoint" />
<mxPoint x="512.08" y="265" as="targetPoint" />
</mxGeometry>
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-9" value="Models" style="rounded=1;whiteSpace=wrap;html=1;fillColor=#18C249;" vertex="1" parent="1">
<mxGeometry x="470" y="510" width="80" height="60" as="geometry" />
</mxCell>
<mxCell id="zHhczcy2-Jv_nqmJUiNH-3" value="Entities" style="rounded=1;whiteSpace=wrap;html=1;gradientColor=none;fillColor=#18C249;" vertex="1" parent="1">
<mxGeometry x="472.5" y="330" width="80" height="60" as="geometry" />
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -1,14 +1,18 @@
# External Libraries
# Libraries
External libraries track assets stored in the filesystem outside of Immich. When the external library is scanned, Immich will load videos and photos from disk and create the corresponding assets. These assets will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. Later, if a file is modified outside of Immich, you need to scan the library for the changes to show up.
## Overview
If an external asset is deleted from disk, Immich will move it to trash on rescan. To restore the asset, you need to restore the original file. After 30 days the file will be removed from trash, and any changes to metadata within Immich will be lost.
Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
:::caution
## External Libraries
If you add metadata to an external asset in any way (i.e. add it to an album or edit the description), that metadata is only stored inside Immich and will not be persisted to the external asset file. If you move an asset to another location within the library all such metadata will be lost upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release.
External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
:::
If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:
- Scan Library Files: This is the default scan method and also the quickest. It will scan all files in the library and add new files to the library. It will notice if any files are missing (see below) but not check existing assets
- Scan All Library Files: Same as above, but will check each existing asset to see if the modification time has changed. If it has, the asset will be updated. Since it has to check each asset, this is slower than Scan Library Files.
- Force Scan All Library Files: Same as above, but will read each asset from disk no matter the modification time. This is useful in some cases where an asset has been modified externally but the modification time has not changed. This is the slowest way to scan because it reads each asset from disk.
:::caution
@@ -16,6 +20,22 @@ Due to aggressive caching it can take some time for a refreshed asset to appear
:::
In external libraries, the file path is used for duplicate detection. This means that if a file is moved to a different location, it will be added as a new asset. If the file is moved back to its original location, it will be added as a new asset. In contrast to upload libraries, two identical files can be uploaded if they are in different locations. This is a deliberate design choice to make Immich reflect the file system as closely as possible. Remember that duplication detection is only done within the same library, so if you have multiple external libraries, the same file can be added to multiple libraries.
:::caution
If you add assets from an external library to an album and then move the asset to another location within the library, the asset will be removed from the album upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release.
:::
### Deleted External Assets
Note: Either a manual or scheduled library scan must have been performed to identify offline assets before this process will work.
In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
### Import Paths
External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. Each import file must be a readable directory that exists on the filesystem; the import path dialog will alert you of any paths that are not accessible.
@@ -46,13 +66,9 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw`
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
Special characters such as @ should be escaped, for instance:
- `**/\@eadir/**` will exclude all files in any directory named `@eadir`
### Automatic watching (EXPERIMENTAL)
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan.
This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button.
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
@@ -68,7 +84,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
### Nightly job
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion.
There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
## Usage
@@ -104,7 +120,7 @@ This will disallow the images from being deleted in the web UI, or adding metada
_Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._
:::
### Create A New Library
### Create External Libraries
These actions must be performed by the Immich administrator.
@@ -128,7 +144,7 @@ Next, we'll add an exclusion pattern to filter out raw files.
- Enter `**/Raw/**` and click save.
- Click save
- Click the drop-down menu on the newly created library
- Click on Scan
- Click on Scan Library Files
The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library.
@@ -145,7 +161,7 @@ If you get an error here, please rename the other external library to something
- Click on Add Path
- Enter `/mnt/media/videos` then click Add
- Click Save
- Click on Scan
- Click on Scan Library Files
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.

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",

View File

@@ -6,7 +6,6 @@ import {
mdiLeadPencil,
mdiLockOff,
mdiLockOutline,
mdiMicrosoftWindows,
mdiSecurity,
mdiSpeedometerSlow,
mdiTrashCan,
@@ -22,18 +21,6 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [
{
icon: mdiMicrosoftWindows,
iconColor: '#357EC7',
title: 'Hidden files in Windows are cursed',
description:
'Hidden files in Windows cannot be opened with the "w" flag. That, combined with SMB option "hide dot files" leads to a lot of confusion.',
link: {
url: 'https://github.com/immich-app/immich/pull/12812',
text: '#12812',
},
date: new Date(2024, 8, 20),
},
{
icon: mdiWrap,
iconColor: 'gray',

View File

@@ -1,8 +1,4 @@
[
{
"label": "v1.116.0",
"url": "https://v1.116.0.archive.immich.app"
},
{
"label": "v1.115.0",
"url": "https://v1.115.0.archive.immich.app"

View File

@@ -22,6 +22,7 @@ services:
- IMMICH_METRICS=true
- IMMICH_ENV=testing
volumes:
- upload:/usr/src/app/upload
- ./test-assets:/test-assets
extra_hosts:
- 'auth-server:host-gateway'
@@ -43,3 +44,7 @@ services:
POSTGRES_DB: immich
ports:
- 5435:5432
volumes:
model-cache:
upload:

438
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.116.0",
"version": "1.115.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.116.0",
"version": "1.115.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.20",
"version": "2.2.19",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -92,7 +92,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.116.0",
"version": "1.115.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -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": {
@@ -4990,9 +5016,9 @@
}
},
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz",
"integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
"dev": true
},
"node_modules/pathe": {
@@ -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

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.116.0",
"version": "1.115.0",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -53,10 +53,8 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'docker compose up --build --renew-anon-volumes --force-recreate --remove-orphans',
command: 'docker compose up --build -V --remove-orphans',
url: 'http://127.0.0.1:2285',
stdout: 'pipe',
stderr: 'pipe',
reuseExistingServer: true,
},
});

View File

@@ -1052,7 +1052,7 @@ describe('/asset', () => {
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
latitude: null,
longitude: null,
orientation: '1',
orientation: 1,
},
},
},

View File

@@ -1,4 +1,11 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import {
LibraryResponseDto,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
removeOfflineFiles,
scanLibrary,
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -8,7 +15,8 @@ import request from 'supertest';
import { utimes } from 'utimes';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/libraries', () => {
let admin: LoginResponseDto;
@@ -285,19 +293,14 @@ describe('/libraries', () => {
expect(body).toEqual(errorDto.unauthorized);
});
it('should import new asset when scanning external library', async () => {
it('should scan external library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
});
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
const { assets } = await utils.metadataSearch(admin.accessToken, {
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
@@ -312,13 +315,8 @@ describe('/libraries', () => {
exclusionPatterns: ['**/directoryA'],
});
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@@ -332,13 +330,8 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
});
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@@ -347,144 +340,95 @@ describe('/libraries', () => {
expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined();
});
it('should reimport a modified file', async () => {
it('should pick up new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true });
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true });
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(0);
});
it('should set an asset offline if its file is missing', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
expect(assets.count).toBe(2);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(trashedAsset.isOffline).toEqual(true);
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 });
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([]);
expect(newAssets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
});
it('should set an asset offline its file is not in any import path', async () => {
it('should offline a file missing from disk', async () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
expect(assets.count).toBe(3);
utils.createDirectory(`${testAssetDir}/temp/another-path/`);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.count).toBe(3);
expect(newAssets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: true,
originalFileName: 'assetC.png',
}),
]),
);
});
it('should offline a file outside of import paths', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await request(app)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [`${testAssetDirInternal}/temp/another-path/`] });
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
.send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(trashedAsset.isOffline).toBe(true);
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([]);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: false,
originalFileName: 'assetA.png',
}),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
}),
]),
);
});
it('should set an asset offline if its file is covered by an exclusion pattern', async () => {
it('should offline a file covered by an exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
@@ -493,12 +437,6 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
originalFileName: 'assetB.png',
});
expect(assets.count).toBe(1);
await request(app)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
@@ -507,21 +445,282 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.isTrashed).toBe(true);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/directoryB/assetB.png`);
expect(trashedAsset.isOffline).toBe(true);
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(2);
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'assetA.png',
}),
]);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: false,
originalFileName: 'assetA.png',
}),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
}),
]),
);
});
it('should not trash an online asset', async () => {
it('should not try to delete offline files', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline1`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(initialAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
});
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(3);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
originalFileName: 'assetC.png',
}),
]),
);
utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
});
describe('with refreshModifiedFiles=true', () => {
it('should reimport modified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(0);
});
});
describe('with refreshAllFiles=true', () => {
it('should reimport all files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id, { refreshAllFiles: true });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
});
});
});
describe('POST /libraries/:id/removeOffline', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(2);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets.count).toBe(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should remove offline files from trash', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(2);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets.count).toBe(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
expect(assets.items[0].isOffline).toBe(false);
expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`);
utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should not remove online files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
@@ -534,11 +733,10 @@ describe('/libraries', () => {
expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
@@ -630,7 +828,7 @@ describe('/libraries', () => {
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app)
.delete(`/libraries/${library.id}`)

View File

@@ -1,7 +1,8 @@
import { LoginResponseDto } from '@immich/sdk';
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
@@ -10,13 +11,18 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/map', () => {
let websocket: Socket;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let asset: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
websocket = await utils.connectWebsocket(admin.accessToken);
asset = await utils.createAsset(admin.accessToken);
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
utils.resetEvents();
const uploadFile = async (input: string) => {
@@ -97,6 +103,63 @@ describe('/map', () => {
});
});
describe('GET /map/style.json', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/style.json');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should allow shared link access', async () => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should throw an error if a theme is not light or dark', async () => {
for (const theme of ['dark1', true, 123, '', null, undefined]) {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
}
});
it('should return the light style.json', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'light' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
});
it('should return the dark style.json', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should not require admin authentication', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
});
describe('GET /map/reverse-geocode', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/reverse-geocode');

View File

@@ -181,7 +181,7 @@ describe('/search', () => {
dto: { size: -1.5 },
expected: ['size must not be less than 1', 'size must be an integer number'],
},
...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({
...['isArchived', 'isFavorite', 'isEncoded', 'isMotion', 'isOffline', 'isVisible'].map((value) => ({
should: `should reject ${value} not a boolean`,
dto: { [value]: 'immich' },
expected: [`${value} must be a boolean value`],

View File

@@ -128,8 +128,6 @@ describe('/server-info', () => {
isInitialized: true,
externalDomain: '',
isOnboarded: false,
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
});
});
});

View File

@@ -134,8 +134,6 @@ describe('/server', () => {
isInitialized: true,
externalDomain: '',
isOnboarded: false,
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json',
});
});
});

View File

@@ -1,13 +1,10 @@
import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk';
import { existsSync } from 'node:fs';
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
describe('/trash', () => {
let admin: LoginResponseDto;
let ws: Socket;
@@ -47,8 +44,6 @@ describe('/trash', () => {
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
expect(existsSync(before.originalPath)).toBe(false);
});
it('should empty the trash with archived assets', async () => {
@@ -69,46 +64,6 @@ describe('/trash', () => {
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
expect(existsSync(before.originalPath)).toBe(false);
});
it('should not delete offline-trashed assets from disk', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.items.length).toBe(1);
const asset = assets.items[0];
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const assetAfter = await utils.getAssetInfo(admin.accessToken, asset.id);
expect(assetAfter).toMatchObject({ isTrashed: true, isOffline: true });
expect(existsSync(`${testAssetDir}/temp/offline/offline.png`)).toBe(true);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
});
});
@@ -136,37 +91,6 @@ describe('/trash', () => {
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
});
it('should not restore offline-trashed assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
const assetId = assets.items[0].id;
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
});
});
describe('POST /trash/restore/assets', () => {
@@ -194,38 +118,5 @@ describe('/trash', () => {
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false);
});
it('should not restore an offline-trashed asset', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
const assetId = assets.items[0].id;
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true);
const { status } = await request(app)
.post('/trash/restore/assets')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [assetId] });
expect(status).toBe(200);
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(true);
});
});
});

View File

@@ -12,8 +12,7 @@ const setup = async () => {
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
const command = 'compose up --build --renew-anon-volumes --force-recreate --remove-orphans';
const child = spawn('docker', command.split(' '), { stdio: 'pipe' });
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
child.stdout.on('data', (data) => {
const input = data.toString();

View File

@@ -156,7 +156,8 @@ export const utils = {
for (const table of tables) {
if (table === 'system_metadata') {
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
// prevent reverse geocoder from being re-initialized
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
} else {
sql.push(`DELETE FROM ${table} CASCADE;`);
}
@@ -372,12 +373,6 @@ export const utils = {
writeFileSync(path, makeRandomImage());
},
createDirectory: (path: string) => {
if (!existsSync(dirname(path))) {
mkdirSync(dirname(path), { recursive: true });
}
},
removeImageFile: (path: string) => {
if (!existsSync(path)) {
return;
@@ -386,14 +381,6 @@ export const utils = {
rmSync(path);
},
removeDirectory: (path: string) => {
if (!existsSync(path)) {
return;
}
rmSync(path);
},
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>

View File

@@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "huggingface-hub"
version = "0.25.0"
version = "0.24.6"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"},
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"},
{file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"},
{file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"},
]
[package.dependencies]
@@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.31.6"
version = "2.31.5"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"},
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"},
{file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
{file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
]
[package.dependencies]
@@ -2834,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.6.6"
version = "0.6.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.6.6-py3-none-linux_armv6l.whl", hash = "sha256:f5bc5398457484fc0374425b43b030e4668ed4d2da8ee7fdda0e926c9f11ccfb"},
{file = "ruff-0.6.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:515a698254c9c47bb84335281a170213b3ee5eb47feebe903e1be10087a167ce"},
{file = "ruff-0.6.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6bb1b4995775f1837ab70f26698dd73852bbb82e8f70b175d2713c0354fe9182"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69c546f412dfae8bb9cc4f27f0e45cdd554e42fecbb34f03312b93368e1cd0a6"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:59627e97364329e4eae7d86fa7980c10e2b129e2293d25c478ebcb861b3e3fd6"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94c3f78c3d32190aafbb6bc5410c96cfed0a88aadb49c3f852bbc2aa9783a7d8"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:704da526c1e137f38c8a067a4a975fe6834b9f8ba7dbc5fd7503d58148851b8f"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:efeede5815a24104579a0f6320660536c5ffc1c91ae94f8c65659af915fb9de9"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e368aef0cc02ca3593eae2fb8186b81c9c2b3f39acaaa1108eb6b4d04617e61f"},
{file = "ruff-0.6.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2653fc3b2a9315bd809725c88dd2446550099728d077a04191febb5ea79a4f79"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:bb858cd9ce2d062503337c5b9784d7b583bcf9d1a43c4df6ccb5eab774fbafcb"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:488f8e15c01ea9afb8c0ba35d55bd951f484d0c1b7c5fd746ce3c47ccdedce68"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:aefb0bd15f1cfa4c9c227b6120573bb3d6c4ee3b29fb54a5ad58f03859bc43c6"},
{file = "ruff-0.6.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a4c0698cc780bcb2c61496cbd56b6a3ac0ad858c966652f7dbf4ceb029252fbe"},
{file = "ruff-0.6.6-py3-none-win32.whl", hash = "sha256:aadf81ddc8ab5b62da7aae78a91ec933cbae9f8f1663ec0325dae2c364e4ad84"},
{file = "ruff-0.6.6-py3-none-win_amd64.whl", hash = "sha256:0adb801771bc1f1b8cf4e0a6fdc30776e7c1894810ff3b344e50da82ef50eeb1"},
{file = "ruff-0.6.6-py3-none-win_arm64.whl", hash = "sha256:4b4d32c137bc781c298964dd4e52f07d6f7d57c03eae97a72d97856844aa510a"},
{file = "ruff-0.6.6.tar.gz", hash = "sha256:0fc030b6fd14814d69ac0196396f6761921bd20831725c7361e1b8100b818034"},
{file = "ruff-0.6.4-py3-none-linux_armv6l.whl", hash = "sha256:c4b153fc152af51855458e79e835fb6b933032921756cec9af7d0ba2aa01a258"},
{file = "ruff-0.6.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:bedff9e4f004dad5f7f76a9d39c4ca98af526c9b1695068198b3bda8c085ef60"},
{file = "ruff-0.6.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d02a4127a86de23002e694d7ff19f905c51e338c72d8e09b56bfb60e1681724f"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7862f42fc1a4aca1ea3ffe8a11f67819d183a5693b228f0bb3a531f5e40336fc"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebe4ff1967c838a1a9618a5a59a3b0a00406f8d7eefee97c70411fefc353617"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:932063a03bac394866683e15710c25b8690ccdca1cf192b9a98260332ca93408"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:50e30b437cebef547bd5c3edf9ce81343e5dd7c737cb36ccb4fe83573f3d392e"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c44536df7b93a587de690e124b89bd47306fddd59398a0fb12afd6133c7b3818"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ea086601b22dc5e7693a78f3fcfc460cceabfdf3bdc36dc898792aba48fbad6"},
{file = "ruff-0.6.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b52387d3289ccd227b62102c24714ed75fbba0b16ecc69a923a37e3b5e0aaaa"},
{file = "ruff-0.6.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0308610470fcc82969082fc83c76c0d362f562e2f0cdab0586516f03a4e06ec6"},
{file = "ruff-0.6.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:803b96dea21795a6c9d5bfa9e96127cc9c31a1987802ca68f35e5c95aed3fc0d"},
{file = "ruff-0.6.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:66dbfea86b663baab8fcae56c59f190caba9398df1488164e2df53e216248baa"},
{file = "ruff-0.6.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:34d5efad480193c046c86608dbba2bccdc1c5fd11950fb271f8086e0c763a5d1"},
{file = "ruff-0.6.4-py3-none-win32.whl", hash = "sha256:f0f8968feea5ce3777c0d8365653d5e91c40c31a81d95824ba61d871a11b8523"},
{file = "ruff-0.6.4-py3-none-win_amd64.whl", hash = "sha256:549daccee5227282289390b0222d0fbee0275d1db6d514550d65420053021a58"},
{file = "ruff-0.6.4-py3-none-win_arm64.whl", hash = "sha256:ac4b75e898ed189b3708c9ab3fc70b79a433219e1e87193b4f2b77251d058d14"},
{file = "ruff-0.6.4.tar.gz", hash = "sha256:ac3b5bfbee99973f80aa1b7cbd1c9cbce200883bdd067300c22a6cc1c7fba212"},
]
[[package]]

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.116.0"
version = "1.115.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.24.3"
"flutter": "3.24.0"
}

View File

@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.24.3",
"dart.flutterSdkPath": ".fvm/versions/3.24.0",
"search.exclude": {
"**/.fvm": true
},

View File

@@ -46,63 +46,21 @@ custom_lint:
- avoid_public_notifier_properties: false
- avoid_manual_providers_as_generated_provider_dependency: false
- unsupported_provider_value: false
- import_rule_photo_manager:
message: photo_manager must only be used in MediaRepositories
restrict: package:photo_manager
allowed:
- photo_manager:
exclude:
# required / wanted
- 'lib/repositories/{album,asset,file}_media.repository.dart'
# acceptable exceptions for the time being
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
- lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
# refactor to make the providers and services testable
- lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
- lib/services/{background,backup}.service.dart # uses only PMProgressHandler
- import_rule_isar:
message: isar must only be used in entities and repositories
restrict: package:isar
allowed:
# required / wanted
- lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,exif_info,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
- test/**.dart
# refactor to make the providers and services testable
- lib/pages/common/album_asset_selection.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
- import_rule_openapi:
message: openapi must only be used through ApiRepositories
restrict: package:openapi
allowed:
# requried / wanted
- lib/repositories/*_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/map/map_marker.model.dart
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
- lib/models/shared_link/shared_link.model.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/websocket.provider.dart
- lib/routing/auth_guard.dart
- lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.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
- album_media.repository.dart
- asset_media.repository.dart
- file_media.repository.dart
# acceptable exceptions for the time being
- asset.entity.dart # to provide local AssetEntity for now
- immich_local_image_provider.dart # accesses thumbnails via PhotoManager
- immich_local_thumbnail_provider.dart # accesses thumbnails via PhotoManager
# refactor to make the providers and services testable
- backup.provider.dart # uses only PMProgressHandler
- manual_upload.provider.dart # uses only PMProgressHandler
- background.service.dart # uses only PMProgressHandler
- backup.service.dart # uses only PMProgressHandler
dart_code_metrics:
metrics:

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 160,
"android.injected.version.name" => "1.116.0",
"android.injected.version.code" => 159,
"android.injected.version.name" => "1.115.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -1,61 +1,36 @@
import 'dart:collection';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/error/error.dart' show ErrorSeverity;
import 'package:custom_lint_builder/custom_lint_builder.dart';
// ignore: depend_on_referenced_packages
import 'package:glob/glob.dart';
PluginBase createPlugin() => ImmichLinter();
class ImmichLinter extends PluginBase {
@override
List<LintRule> getLintRules(CustomLintConfigs configs) {
final List<LintRule> rules = [];
for (final entry in configs.rules.entries) {
if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
final code = makeCode(entry.key, entry.value);
final allowedPaths = getStrings(entry.value, "allowed");
final forbiddenPaths = getStrings(entry.value, "forbidden");
final restrict = getStrings(entry.value, "restrict");
rules.add(ImportRule(code, buildGlob(allowedPaths),
buildGlob(forbiddenPaths), restrict));
}
}
return rules;
}
static makeCode(String name, LintOptions options) => LintCode(
name: name,
problemMessage: options.json["message"] as String,
errorSeverity: ErrorSeverity.WARNING,
);
static List<String> getStrings(LintOptions options, String field) {
final List<String> result = [];
final excludeOption = options.json[field];
if (excludeOption is String) {
result.add(excludeOption);
} else if (excludeOption is List) {
result.addAll(excludeOption.map((option) => option));
}
return result;
}
Glob? buildGlob(List<String> globs) {
if (globs.isEmpty) return null;
if (globs.length == 1) return Glob(globs[0], caseSensitive: true);
return Glob("{${globs.join(",")}}", caseSensitive: true);
}
List<LintRule> getLintRules(CustomLintConfigs configs) => [
PhotoManagerRule(configs.rules[PhotoManagerRule._code.name]),
];
}
// ignore: must_be_immutable
class ImportRule extends DartLintRule {
ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
: super(code: code);
class PhotoManagerRule extends DartLintRule {
PhotoManagerRule(LintOptions? options) : super(code: _code) {
final excludeOption = options?.json["exclude"];
if (excludeOption is String) {
_excludePaths.add(excludeOption);
} else if (excludeOption is List) {
_excludePaths.addAll(excludeOption.map((option) => option));
}
}
final Glob? _allowed;
final Glob? _forbidden;
final List<String> _restrict;
int _rootOffset = -1;
final Set<String> _excludePaths = HashSet();
static const _code = LintCode(
name: 'photo_manager',
problemMessage:
'photo_manager library must only be used in MediaRepository',
errorSeverity: ErrorSeverity.WARNING,
);
@override
void run(
@@ -63,23 +38,11 @@ class ImportRule extends DartLintRule {
ErrorReporter reporter,
CustomLintContext context,
) {
if (_rootOffset == -1) {
const project = "/immich/mobile/";
_rootOffset = resolver.path.indexOf(project) + project.length;
}
final path = resolver.path.substring(_rootOffset);
if ((_allowed != null && _allowed!.matches(path)) &&
(_forbidden == null || !_forbidden!.matches(path))) return;
if (_excludePaths.contains(resolver.source.shortName)) return;
context.registry.addImportDirective((node) {
final uri = node.uri.stringValue;
if (uri == null) return;
for (final restricted in _restrict) {
if (uri.startsWith(restricted) == true) {
reporter.atNode(node, code);
return;
}
if (node.uri.stringValue?.startsWith("package:photo_manager") == true) {
reporter.atNode(node, code);
}
});
}

View File

@@ -159,7 +159,7 @@ packages:
source: hosted
version: "2.4.4"
glob:
dependency: "direct main"
dependency: transitive
description:
name: glob
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"

View File

@@ -8,7 +8,6 @@ dependencies:
analyzer: ^6.8.0
analyzer_plugin: ^0.11.3
custom_lint_builder: ^0.6.4
glob: ^2.1.2
dev_dependencies:
lints: ^4.0.0

View File

@@ -401,7 +401,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 176;
CURRENT_PROJECT_VERSION = 175;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -543,7 +543,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 176;
CURRENT_PROJECT_VERSION = 175;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -571,7 +571,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 176;
CURRENT_PROJECT_VERSION = 175;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.116.0</string>
<string>1.115.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>176</string>
<string>175</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.116.0"
version_number: "1.115.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

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

View File

@@ -3,8 +3,6 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:isar/isar.dart';
// ignore: implementation_imports
import 'package:isar/src/common/isar_links_common.dart';
import 'package:openapi/api.dart';
part 'album.entity.g.dart';
@@ -25,7 +23,6 @@ class Album {
required this.activityEnabled,
});
// fields stored in DB
Id id = Isar.autoIncrement;
@Index(unique: false, replace: false, type: IndexType.hash)
String? remoteId;
@@ -44,17 +41,9 @@ class Album {
final IsarLinks<User> sharedUsers = IsarLinks<User>();
final IsarLinks<Asset> assets = IsarLinks<Asset>();
// transient fields
@ignore
bool isAll = false;
@ignore
String? remoteThumbnailAssetId;
@ignore
int remoteAssetCount = 0;
// getters
@ignore
bool get isRemote => remoteId != null;
@@ -85,18 +74,6 @@ class Album {
@ignore
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
// the following getter are needed because Isar links do not make data
// accessible in an object freshly created (not loaded from DB)
@ignore
Iterable<User> get remoteUsers => sharedUsers.isEmpty
? (sharedUsers as IsarLinksCommon<User>).addedObjects
: sharedUsers;
@ignore
Iterable<Asset> get remoteAssets =>
assets.isEmpty ? (assets as IsarLinksCommon<Asset>).addedObjects : assets;
@override
bool operator ==(other) {
if (other is! Album) return false;
@@ -152,7 +129,6 @@ class Album {
endDate: dto.endDate,
activityEnabled: dto.isActivityEnabled,
);
a.remoteAssetCount = dto.assetCount;
a.owner.value = await db.users.getById(dto.ownerId);
if (dto.albumThumbnailAssetId != null) {
a.thumbnail.value = await db.assets
@@ -188,3 +164,7 @@ extension AssetsHelper on IsarCollection<Album> {
return a;
}
}
extension AlbumResponseDtoHelper on AlbumResponseDto {
List<Asset> getAssets() => assets.map(Asset.remote).toList();
}

View File

@@ -57,64 +57,69 @@ const AssetSchema = CollectionSchema(
name: r'isFavorite',
type: IsarType.bool,
),
r'isTrashed': PropertySchema(
r'isOffline': PropertySchema(
id: 8,
name: r'isOffline',
type: IsarType.bool,
),
r'isTrashed': PropertySchema(
id: 9,
name: r'isTrashed',
type: IsarType.bool,
),
r'livePhotoVideoId': PropertySchema(
id: 9,
id: 10,
name: r'livePhotoVideoId',
type: IsarType.string,
),
r'localId': PropertySchema(
id: 10,
id: 11,
name: r'localId',
type: IsarType.string,
),
r'ownerId': PropertySchema(
id: 11,
id: 12,
name: r'ownerId',
type: IsarType.long,
),
r'remoteId': PropertySchema(
id: 12,
id: 13,
name: r'remoteId',
type: IsarType.string,
),
r'stackCount': PropertySchema(
id: 13,
id: 14,
name: r'stackCount',
type: IsarType.long,
),
r'stackId': PropertySchema(
id: 14,
id: 15,
name: r'stackId',
type: IsarType.string,
),
r'stackPrimaryAssetId': PropertySchema(
id: 15,
id: 16,
name: r'stackPrimaryAssetId',
type: IsarType.string,
),
r'thumbhash': PropertySchema(
id: 16,
id: 17,
name: r'thumbhash',
type: IsarType.string,
),
r'type': PropertySchema(
id: 17,
id: 18,
name: r'type',
type: IsarType.byte,
enumMap: _AssettypeEnumValueMap,
),
r'updatedAt': PropertySchema(
id: 18,
id: 19,
name: r'updatedAt',
type: IsarType.dateTime,
),
r'width': PropertySchema(
id: 19,
id: 20,
name: r'width',
type: IsarType.int,
)
@@ -239,18 +244,19 @@ void _assetSerialize(
writer.writeInt(offsets[5], object.height);
writer.writeBool(offsets[6], object.isArchived);
writer.writeBool(offsets[7], object.isFavorite);
writer.writeBool(offsets[8], object.isTrashed);
writer.writeString(offsets[9], object.livePhotoVideoId);
writer.writeString(offsets[10], object.localId);
writer.writeLong(offsets[11], object.ownerId);
writer.writeString(offsets[12], object.remoteId);
writer.writeLong(offsets[13], object.stackCount);
writer.writeString(offsets[14], object.stackId);
writer.writeString(offsets[15], object.stackPrimaryAssetId);
writer.writeString(offsets[16], object.thumbhash);
writer.writeByte(offsets[17], object.type.index);
writer.writeDateTime(offsets[18], object.updatedAt);
writer.writeInt(offsets[19], object.width);
writer.writeBool(offsets[8], object.isOffline);
writer.writeBool(offsets[9], object.isTrashed);
writer.writeString(offsets[10], object.livePhotoVideoId);
writer.writeString(offsets[11], object.localId);
writer.writeLong(offsets[12], object.ownerId);
writer.writeString(offsets[13], object.remoteId);
writer.writeLong(offsets[14], object.stackCount);
writer.writeString(offsets[15], object.stackId);
writer.writeString(offsets[16], object.stackPrimaryAssetId);
writer.writeString(offsets[17], object.thumbhash);
writer.writeByte(offsets[18], object.type.index);
writer.writeDateTime(offsets[19], object.updatedAt);
writer.writeInt(offsets[20], object.width);
}
Asset _assetDeserialize(
@@ -269,19 +275,20 @@ Asset _assetDeserialize(
id: id,
isArchived: reader.readBoolOrNull(offsets[6]) ?? false,
isFavorite: reader.readBoolOrNull(offsets[7]) ?? false,
isTrashed: reader.readBoolOrNull(offsets[8]) ?? false,
livePhotoVideoId: reader.readStringOrNull(offsets[9]),
localId: reader.readStringOrNull(offsets[10]),
ownerId: reader.readLong(offsets[11]),
remoteId: reader.readStringOrNull(offsets[12]),
stackCount: reader.readLongOrNull(offsets[13]) ?? 0,
stackId: reader.readStringOrNull(offsets[14]),
stackPrimaryAssetId: reader.readStringOrNull(offsets[15]),
thumbhash: reader.readStringOrNull(offsets[16]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ??
isOffline: reader.readBoolOrNull(offsets[8]) ?? false,
isTrashed: reader.readBoolOrNull(offsets[9]) ?? false,
livePhotoVideoId: reader.readStringOrNull(offsets[10]),
localId: reader.readStringOrNull(offsets[11]),
ownerId: reader.readLong(offsets[12]),
remoteId: reader.readStringOrNull(offsets[13]),
stackCount: reader.readLongOrNull(offsets[14]) ?? 0,
stackId: reader.readStringOrNull(offsets[15]),
stackPrimaryAssetId: reader.readStringOrNull(offsets[16]),
thumbhash: reader.readStringOrNull(offsets[17]),
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
AssetType.other,
updatedAt: reader.readDateTime(offsets[18]),
width: reader.readIntOrNull(offsets[19]),
updatedAt: reader.readDateTime(offsets[19]),
width: reader.readIntOrNull(offsets[20]),
);
return object;
}
@@ -312,27 +319,29 @@ P _assetDeserializeProp<P>(
case 8:
return (reader.readBoolOrNull(offset) ?? false) as P;
case 9:
return (reader.readStringOrNull(offset)) as P;
return (reader.readBoolOrNull(offset) ?? false) as P;
case 10:
return (reader.readStringOrNull(offset)) as P;
case 11:
return (reader.readLong(offset)) as P;
return (reader.readStringOrNull(offset)) as P;
case 12:
return (reader.readStringOrNull(offset)) as P;
return (reader.readLong(offset)) as P;
case 13:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 14:
return (reader.readStringOrNull(offset)) as P;
case 14:
return (reader.readLongOrNull(offset) ?? 0) as P;
case 15:
return (reader.readStringOrNull(offset)) as P;
case 16:
return (reader.readStringOrNull(offset)) as P;
case 17:
return (reader.readStringOrNull(offset)) as P;
case 18:
return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ??
AssetType.other) as P;
case 18:
return (reader.readDateTime(offset)) as P;
case 19:
return (reader.readDateTime(offset)) as P;
case 20:
return (reader.readIntOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
@@ -1353,6 +1362,16 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isOfflineEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'isOffline',
value: value,
));
});
}
QueryBuilder<Asset, Asset, QAfterFilterCondition> isTrashedEqualTo(
bool value) {
return QueryBuilder.apply(this, (query) {
@@ -2628,6 +2647,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsOfflineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> sortByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc);
@@ -2882,6 +2913,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.asc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsOfflineDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isOffline', Sort.desc);
});
}
QueryBuilder<Asset, Asset, QAfterSortBy> thenByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isTrashed', Sort.asc);
@@ -3078,6 +3121,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsOffline() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isOffline');
});
}
QueryBuilder<Asset, Asset, QDistinct> distinctByIsTrashed() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'isTrashed');
@@ -3214,6 +3263,12 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
});
}
QueryBuilder<Asset, bool, QQueryOperations> isOfflineProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isOffline');
});
}
QueryBuilder<Asset, bool, QQueryOperations> isTrashedProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isTrashed');

View File

@@ -72,14 +72,13 @@ extension AssetListExtension on Iterable<Asset> {
}
/// Filters out offline assets and returns those that are still accessible by the Immich server
/// TODO: isOffline is removed from Immich, so this method is not useful anymore
Iterable<Asset> nonOfflineOnly({
void Function()? errorCallback,
}) {
final bool onlyLive = every((e) => false);
final bool onlyLive = every((e) => !e.isOffline);
if (!onlyLive) {
if (errorCallback != null) errorCallback();
return where((a) => false);
return where((a) => !a.isOffline);
}
return this;
}

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,40 +0,0 @@
import 'package:immich_mobile/entities/album.entity.dart';
abstract interface class IAlbumApiRepository {
Future<Album> get(String id);
Future<List<Album>> getAll({bool? shared});
Future<Album> create(
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
});
Future<Album> update(
String albumId, {
String? name,
String? thumbnailAssetId,
String? description,
bool? activityEnabled,
});
Future<void> delete(String albumId);
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
);
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
);
Future<Album> addUsers(
String albumId,
Iterable<String> userIds,
);
Future<void> removeUser(String albumId, {required String userId});
}

View File

@@ -1,27 +1,8 @@
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 {
Future<Asset?> getByRemoteId(String id);
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

@@ -2,7 +2,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

@@ -4,15 +4,11 @@ class ServerConfig {
final int trashDays;
final String oauthButtonText;
final String externalDomain;
final String mapDarkStyleUrl;
final String mapLightStyleUrl;
const ServerConfig({
required this.trashDays,
required this.oauthButtonText,
required this.externalDomain,
required this.mapDarkStyleUrl,
required this.mapLightStyleUrl,
});
ServerConfig copyWith({
@@ -24,8 +20,6 @@ class ServerConfig {
trashDays: trashDays ?? this.trashDays,
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
externalDomain: externalDomain ?? this.externalDomain,
mapDarkStyleUrl: mapDarkStyleUrl,
mapLightStyleUrl: mapLightStyleUrl,
);
}
@@ -36,9 +30,7 @@ class ServerConfig {
ServerConfig.fromDto(ServerConfigDto dto)
: trashDays = dto.trashDays,
oauthButtonText = dto.oauthButtonText,
externalDomain = dto.externalDomain,
mapDarkStyleUrl = dto.mapDarkStyleUrl,
mapLightStyleUrl = dto.mapLightStyleUrl;
externalDomain = dto.externalDomain;
@override
bool operator ==(covariant ServerConfig other) {

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

@@ -51,109 +51,107 @@ class CropImagePage extends HookWidget {
],
),
backgroundColor: context.scaffoldBackgroundColor,
body: SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 20),
width: constraints.maxWidth * 0.9,
height: constraints.maxHeight * 0.6,
child: CropImage(
controller: cropController,
image: image,
gridColor: Colors.white,
),
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 20),
width: constraints.maxWidth * 0.9,
height: constraints.maxHeight * 0.6,
child: CropImage(
controller: cropController,
image: image,
gridColor: Colors.white,
),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: context.scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
Expanded(
child: Container(
width: double.infinity,
decoration: BoxDecoration(
color: context.scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
bottom: 10,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Icons.rotate_left,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
cropController.rotateLeft();
},
),
IconButton(
icon: Icon(
Icons.rotate_right,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
cropController.rotateRight();
},
),
],
),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(
left: 20,
right: 20,
bottom: 10,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: null,
label: 'Free',
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(
Icons.rotate_left,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
cropController.rotateLeft();
},
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 1.0,
label: '1:1',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 16.0 / 9.0,
label: '16:9',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 3.0 / 2.0,
label: '3:2',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 7.0 / 5.0,
label: '7:5',
IconButton(
icon: Icon(
Icons.rotate_right,
color: Theme.of(context).iconTheme.color,
),
onPressed: () {
cropController.rotateRight();
},
),
],
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: null,
label: 'Free',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 1.0,
label: '1:1',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 16.0 / 9.0,
label: '16:9',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 3.0 / 2.0,
label: '3:2',
),
_AspectRatioButton(
cropController: cropController,
aspectRatio: aspectRatio,
ratio: 7.0 / 5.0,
label: '7:5',
),
],
),
],
),
),
),
],
);
},
),
),
],
);
},
),
);
}

View File

@@ -2,7 +2,7 @@ import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/favorite.provider.dart';
import 'package:immich_mobile/providers/favorite_provider.dart';
import 'package:immich_mobile/providers/multiselect.provider.dart';
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';

View File

@@ -1,5 +1,4 @@
import 'dart:math';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -8,27 +7,27 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:geolocator/geolocator.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/latlngbounds_extension.dart';
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
import 'package:immich_mobile/models/map/map_event.model.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/map/map_marker.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/widgets/map/map_app_bar.dart';
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
import 'package:immich_mobile/widgets/map/map_bottom_sheet.dart';
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/immich_loading_overlay.dart';
import 'package:immich_mobile/utils/debounce.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
@RoutePage()
@@ -305,7 +304,7 @@ class MapPage extends HookConsumerWidget {
),
Positioned(
right: 0,
bottom: MediaQuery.paddingOf(context).bottom + 16,
bottom: MediaQuery.of(context).padding.bottom + 16,
child: ElevatedButton(
onPressed: onZoomToLocation,
style: ElevatedButton.styleFrom(

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

@@ -86,16 +86,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
void handleAppPause() {
state = AppLifeCycleEnum.paused;
_wasPaused = true;
if (_ref.read(authenticationProvider).isAuthenticated) {
// Do not cancel backup if manual upload is in progress
if (_ref.read(backupProvider.notifier).backupProgress !=
BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup();
}
_ref.read(websocketProvider.notifier).disconnect();
// Do not cancel backup if manual upload is in progress
if (_ref.read(backupProvider.notifier).backupProgress !=
BackUpProgressEnum.manualInProgress) {
_ref.read(backupProvider.notifier).cancelBackup();
}
_ref.read(websocketProvider.notifier).disconnect();
ImmichLogger().flush();
}

View File

@@ -313,9 +313,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
/// Those assets are unique and are used as the total assets
///
Future<void> _updateBackupAssetCount() async {
// Save to persistent storage
await _updatePersistentAlbumsSelection();
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
final Set<BackupCandidate> assetsFromSelectedAlbums = {};
final Set<BackupCandidate> assetsFromExcludedAlbums = {};
@@ -411,6 +408,9 @@ class BackupNotifier extends StateNotifier<BackUpState> {
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
}
// Save to persistent storage
await _updatePersistentAlbumsSelection();
}
/// Get all necessary information for calculating the available albums,

View File

@@ -1,23 +1,28 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/models/map/map_state.model.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'map_state.provider.g.dart';
@Riverpod(keepAlive: true)
class MapStateNotifier extends _$MapStateNotifier {
final _log = Logger("MapStateNotifier");
@override
MapState build() {
final appSettingsProvider = ref.read(appSettingsServiceProvider);
final lightStyleUrl =
ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
final darkStyleUrl =
ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
// Fetch and save the Style JSONs
loadStyles();
return MapState(
themeMode: ThemeMode.values[
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
@@ -29,11 +34,65 @@ class MapStateNotifier extends _$MapStateNotifier {
appSettingsProvider.getSetting<bool>(AppSettingsEnum.mapwithPartners),
relativeTime:
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapRelativeDate),
lightStyleFetched: AsyncData(lightStyleUrl),
darkStyleFetched: AsyncData(darkStyleUrl),
);
}
void loadStyles() async {
final documents = (await getApplicationDocumentsDirectory()).path;
// Set to loading
state = state.copyWith(lightStyleFetched: const AsyncLoading());
// Fetch and save light theme
final lightResponse = await ref
.read(apiServiceProvider)
.mapApi
.getMapStyleWithHttpInfo(MapTheme.light);
if (lightResponse.statusCode >= HttpStatus.badRequest) {
state = state.copyWith(
lightStyleFetched: AsyncError(lightResponse.body, StackTrace.current),
);
_log.severe(
"Cannot fetch map light style",
lightResponse.toLoggerString(),
);
return;
}
final lightJSON = lightResponse.body;
final lightFile = await File("$documents/map-style-light.json")
.writeAsString(lightJSON, flush: true);
// Update state with path
state =
state.copyWith(lightStyleFetched: AsyncData(lightFile.absolute.path));
// Set to loading
state = state.copyWith(darkStyleFetched: const AsyncLoading());
// Fetch and save dark theme
final darkResponse = await ref
.read(apiServiceProvider)
.mapApi
.getMapStyleWithHttpInfo(MapTheme.dark);
if (darkResponse.statusCode >= HttpStatus.badRequest) {
state = state.copyWith(
darkStyleFetched: AsyncError(darkResponse.body, StackTrace.current),
);
_log.severe("Cannot fetch map dark style", darkResponse.toLoggerString());
return;
}
final darkJSON = darkResponse.body;
final darkFile = await File("$documents/map-style-dark.json")
.writeAsString(darkJSON, flush: true);
// Update state with path
state = state.copyWith(darkStyleFetched: AsyncData(darkFile.absolute.path));
}
void switchTheme(ThemeMode mode) {
ref.read(appSettingsServiceProvider).setSetting(
AppSettingsEnum.mapThemeMode,

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

@@ -34,9 +34,6 @@ class ServerInfoNotifier extends StateNotifier<ServerInfo> {
trashDays: 30,
oauthButtonText: '',
externalDomain: '',
mapLightStyleUrl:
'https://tiles.immich.cloud/v1/style/light.json',
mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json',
),
serverDiskInfo: const ServerDiskInfo(
diskAvailable: "0",

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,167 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.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 {
final AlbumsApi _api;
AlbumApiRepository(this._api);
@override
Future<Album> get(String id) async {
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));
return dtos.map(_toAlbum).toList().cast();
}
@override
Future<Album> create(
String name, {
required Iterable<String> assetIds,
Iterable<String> sharedUserIds = const [],
}) async {
final users = sharedUserIds.map(
(id) => AlbumUserCreateDto(userId: id, role: AlbumUserRole.editor),
);
final responseDto = await checkNull(
_api.createAlbum(
CreateAlbumDto(
albumName: name,
assetIds: assetIds.toList(),
albumUsers: users.toList(),
),
),
);
return _toAlbum(responseDto);
}
@override
Future<Album> update(
String albumId, {
String? name,
String? thumbnailAssetId,
String? description,
bool? activityEnabled,
}) async {
final response = await checkNull(
_api.updateAlbumInfo(
albumId,
UpdateAlbumDto(
albumName: name,
albumThumbnailAssetId: thumbnailAssetId,
description: description,
isActivityEnabled: activityEnabled,
),
),
);
return _toAlbum(response);
}
@override
Future<void> delete(String albumId) {
return _api.deleteAlbum(albumId);
}
@override
Future<({List<String> added, List<String> duplicates})> addAssets(
String albumId,
Iterable<String> assetIds,
) async {
final response = await checkNull(
_api.addAssetsToAlbum(
albumId,
BulkIdsDto(ids: assetIds.toList()),
),
);
final List<String> added = [];
final List<String> duplicates = [];
for (final result in response) {
if (result.success) {
added.add(result.id);
} else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) {
duplicates.add(result.id);
}
}
return (added: added, duplicates: duplicates);
}
@override
Future<({List<String> removed, List<String> failed})> removeAssets(
String albumId,
Iterable<String> assetIds,
) async {
final response = await checkNull(
_api.removeAssetFromAlbum(
albumId,
BulkIdsDto(ids: assetIds.toList()),
),
);
final List<String> removed = [], failed = [];
for (final dto in response) {
if (dto.success) {
removed.add(dto.id);
} else {
failed.add(dto.id);
}
}
return (removed: removed, failed: failed);
}
@override
Future<Album> addUsers(String albumId, Iterable<String> userIds) async {
final albumUsers =
userIds.map((userId) => AlbumUserAddDto(userId: userId)).toList();
final response = await checkNull(
_api.addUsersToAlbum(
albumId,
AddUsersDto(albumUsers: albumUsers),
),
);
return _toAlbum(response);
}
@override
Future<void> removeUser(String albumId, {required String userId}) {
return _api.removeUserFromAlbum(albumId, userId);
}
static Album _toAlbum(AlbumResponseDto dto) {
final Album album = Album(
remoteId: dto.id,
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
endDate: dto.endDate,
activityEnabled: dto.isActivityEnabled,
);
album.remoteAssetCount = dto.assetCount;
album.owner.value = User.fromSimpleUserDto(dto.owner);
album.remoteThumbnailAssetId = dto.albumThumbnailAssetId;
final users = dto.albumUsers
.map((albumUser) => User.fromSimpleUserDto(albumUser.user));
album.sharedUsers.addAll(users);
final assets = dto.assets.map(Asset.remote).toList();
album.assets.addAll(assets);
return album;
}
}

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';
@@ -33,111 +28,4 @@ class AssetRepository implements IAssetRepository {
@override
Future<void> deleteById(List<int> ids) =>
_db.writeTxn(() => _db.assets.deleteAll(ids));
@override
Future<Asset?> getByRemoteId(String id) => _db.assets.getByRemoteId(id);
@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';
@@ -18,22 +17,4 @@ class UserRepository implements IUserRepository {
@override
Future<List<User>> getByIds(List<String> ids) async =>
(await _db.users.getAllById(ids)).cast();
@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

@@ -6,61 +6,63 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/album.interface.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/backup.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.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:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
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/services/entity.service.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final albumServiceProvider = Provider(
(ref) => AlbumService(
ref.watch(apiServiceProvider),
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(entityServiceProvider),
ref.watch(albumRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(userRepositoryProvider),
ref.watch(backupRepositoryProvider),
ref.watch(albumMediaRepositoryProvider),
ref.watch(albumApiRepositoryProvider),
),
);
class AlbumService {
final ApiService _apiService;
final UserService _userService;
final SyncService _syncService;
final EntityService _entityService;
final IAlbumRepository _albumRepository;
final IAssetRepository _assetRepository;
final IUserRepository _userRepository;
final IBackupRepository _backupAlbumRepository;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final Logger _log = Logger('AlbumService');
Completer<bool> _localCompleter = Completer()..complete(false);
Completer<bool> _remoteCompleter = Completer()..complete(false);
AlbumService(
this._apiService,
this._userService,
this._syncService,
this._entityService,
this._albumRepository,
this._assetRepository,
this._userRepository,
this._backupAlbumRepository,
this._albumMediaRepository,
this._albumApiRepository,
);
/// Checks all selected device albums for changes of albums and their assets
@@ -162,11 +164,17 @@ class AlbumService {
bool changes = false;
try {
await _userService.refreshUsers();
final List<Album> serverAlbums =
await _albumApiRepository.getAll(shared: isShared ? true : null);
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
.getAllAlbums(shared: isShared ? true : null);
if (serverAlbums == null) {
return false;
}
changes = await _syncService.syncRemoteAlbumsToDb(
serverAlbums,
isShared: isShared,
loadDetails: (dto) async => dto.assetCount == dto.assets.length
? dto
: (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
);
} finally {
_remoteCompleter.complete(changes);
@@ -180,13 +188,30 @@ class AlbumService {
Iterable<Asset> assets, [
Iterable<User> sharedUsers = const [],
]) async {
final Album album = await _albumApiRepository.create(
albumName,
assetIds: assets.map((asset) => asset.remoteId!),
sharedUserIds: sharedUsers.map((user) => user.id),
);
await _entityService.fillAlbumWithDatabaseEntities(album);
return _albumRepository.create(album);
try {
AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
CreateAlbumDto(
albumName: albumName,
assetIds: assets.map((asset) => asset.remoteId!).toList(),
albumUsers: sharedUsers
.map(
(e) => AlbumUserCreateDto(
userId: e.id,
role: AlbumUserRole.editor,
),
)
.toList(),
),
);
if (remote != null) {
final Album album = await Album.remote(remote);
await _albumRepository.create(album);
return album;
}
} catch (e) {
debugPrint("Error createSharedAlbum ${e.toString()}");
}
return null;
}
/*
@@ -218,21 +243,32 @@ class AlbumService {
Album album,
) async {
try {
final result = await _albumApiRepository.addAssets(
var response = await _apiService.albumsApi.addAssetsToAlbum(
album.remoteId!,
assets.map((asset) => asset.remoteId!),
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
);
final List<Asset> addedAssets = result.added
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
.toList();
if (response != null) {
List<Asset> successAssets = [];
List<String> duplicatedAssets = [];
await _updateAssets(album.id, add: addedAssets);
for (final result in response) {
if (result.success) {
successAssets
.add(assets.firstWhere((asset) => asset.remoteId == result.id));
} else if (!result.success &&
result.error == BulkIdResponseDtoErrorEnum.duplicate) {
duplicatedAssets.add(result.id);
}
}
return AlbumAddAssetsResponse(
alreadyInAlbum: result.duplicates,
successfullyAdded: addedAssets.length,
);
await _updateAssets(album.id, add: successAssets);
return AlbumAddAssetsResponse(
alreadyInAlbum: duplicatedAssets,
successfullyAdded: successAssets.length,
);
}
} catch (e) {
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
}
@@ -257,11 +293,20 @@ class AlbumService {
Album album,
) async {
try {
final updatedAlbum =
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
await _albumRepository.update(updatedAlbum);
return true;
final List<AlbumUserAddDto> albumUsers = sharedUserIds
.map((userId) => AlbumUserAddDto(userId: userId))
.toList();
final result = await _apiService.albumsApi.addUsersToAlbum(
album.remoteId!,
AddUsersDto(albumUsers: albumUsers),
);
if (result != null) {
album.sharedUsers.addAll(await _userRepository.getByIds(sharedUserIds));
album.shared = result.shared;
await _albumRepository.update(album);
return true;
}
} catch (e) {
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
}
@@ -270,13 +315,15 @@ class AlbumService {
Future<bool> setActivityEnabled(Album album, bool enabled) async {
try {
final updatedAlbum = await _albumApiRepository.update(
final result = await _apiService.albumsApi.updateAlbumInfo(
album.remoteId!,
activityEnabled: enabled,
UpdateAlbumDto(isActivityEnabled: enabled),
);
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
await _albumRepository.update(updatedAlbum);
return true;
if (result != null) {
album.activityEnabled = enabled;
await _albumRepository.update(album);
return true;
}
} catch (e) {
debugPrint("Error setActivityEnabled ${e.toString()}");
}
@@ -287,7 +334,7 @@ class AlbumService {
try {
final user = Store.get(StoreKey.currentUser);
if (album.owner.value?.isarId == user.isarId) {
await _albumApiRepository.delete(album.remoteId!);
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
}
if (album.shared) {
final foreignAssets =
@@ -318,7 +365,7 @@ class AlbumService {
Future<bool> leaveAlbum(Album album) async {
try {
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
return true;
} catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}");
@@ -331,14 +378,21 @@ class AlbumService {
Iterable<Asset> assets,
) async {
try {
final result = await _albumApiRepository.removeAssets(
final response = await _apiService.albumsApi.removeAssetFromAlbum(
album.remoteId!,
assets.map((asset) => asset.remoteId!),
BulkIdsDto(
ids: assets.map((asset) => asset.remoteId!).toList(),
),
);
final toRemove = result.removed
.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
await _updateAssets(album.id, remove: toRemove.toList());
return true;
if (response != null) {
final toRemove = response.every((e) => e.success)
? assets
: response
.where((e) => e.success)
.map((e) => assets.firstWhere((a) => a.remoteId == e.id));
await _updateAssets(album.id, remove: toRemove.toList());
return true;
}
} catch (e) {
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
}
@@ -350,9 +404,9 @@ class AlbumService {
User user,
) async {
try {
await _albumApiRepository.removeUser(
await _apiService.albumsApi.removeUserFromAlbum(
album.remoteId!,
userId: user.id,
user.id,
);
album.sharedUsers.remove(user);
@@ -373,12 +427,15 @@ class AlbumService {
String newAlbumTitle,
) async {
try {
album = await _albumApiRepository.update(
await _apiService.albumsApi.updateAlbumInfo(
album.remoteId!,
name: newAlbumTitle,
UpdateAlbumDto(
albumName: newAlbumTitle,
),
);
await _entityService.fillAlbumWithDatabaseEntities(album);
album.name = newAlbumTitle;
await _albumRepository.update(album);
return true;
} catch (e) {
debugPrint("Error changeTitleAlbum ${e.toString()}");
@@ -399,8 +456,12 @@ class AlbumService {
for (final albumName in albumNames) {
Album? album = await getAlbumByName(albumName, true);
album ??= await createAlbum(albumName, []);
if (album != null && album.remoteId != null) {
await _albumApiRepository.addAssets(album.remoteId!, assetIds);
await _apiService.albumsApi.addAssetsToAlbum(
album.remoteId!,
BulkIdsDto(ids: assetIds),
);
}
}
}

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

@@ -13,16 +13,12 @@ import 'package:immich_mobile/main.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/repositories/album.repository.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
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';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
@@ -32,6 +28,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,44 +360,26 @@ 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);
UserRepository userRepository = UserRepository(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);
EntityService entityService =
EntityService(assetRepository, userRepository);
SyncService syncSerive = SyncService(
db,
hashService,
entityService,
albumMediaRepository,
albumApiRepository,
);
UserService userService = UserService(
partnerApiRepository,
userApiRepository,
userRepository,
syncSerive,
);
HashService hashService = HashService(db, this, albumMediaRepository);
SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
UserService userService =
UserService(apiService, db, syncSerive, partnerService);
AlbumService albumService = AlbumService(
apiService,
userService,
syncSerive,
entityService,
albumRepository,
assetRepository,
userRepository,
backupAlbumRepository,
albumMediaRepository,
albumApiRepository,
);
BackupService backupService = BackupService(
apiService,

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

@@ -1,52 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/user.interface.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/user.repository.dart';
class EntityService {
final IAssetRepository _assetRepository;
final IUserRepository _userRepository;
EntityService(
this._assetRepository,
this._userRepository,
);
Future<Album> fillAlbumWithDatabaseEntities(Album album) async {
final ownerId = album.ownerId;
if (ownerId != null) {
// replace owner with user from database
album.owner.value = await _userRepository.get(ownerId);
}
final thumbnailAssetId =
album.remoteThumbnailAssetId ?? album.thumbnail.value?.remoteId;
if (thumbnailAssetId != null) {
// set thumbnail with asset from database
album.thumbnail.value =
await _assetRepository.getByRemoteId(thumbnailAssetId);
}
if (album.remoteUsers.isNotEmpty) {
// replace all users with users from database
final users = await _userRepository
.getByIds(album.remoteUsers.map((user) => user.id).toList());
album.sharedUsers.clear();
album.sharedUsers.addAll(users);
}
if (album.remoteAssets.isNotEmpty) {
// replace all assets with assets from database
final assets = await _assetRepository
.getAllByRemoteId(album.remoteAssets.map((asset) => asset.remoteId!));
album.assets.clear();
album.assets.addAll(assets);
}
return album;
}
}
final entityServiceProvider = Provider(
(ref) => EntityService(
ref.watch(assetRepositoryProvider),
ref.watch(userRepositoryProvider),
),
);

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 = [];
@@ -70,19 +65,7 @@ class HashService {
if (hashes[i] != null) {
continue;
}
File? file;
try {
file = await assets[i].local!.originFile;
} catch (error, stackTrace) {
_log.warning(
"Error getting file to hash for asset ${assets[i].localId}, name: ${assets[i].fileName}, created on: ${assets[i].fileCreatedAt}, skipping",
error,
stackTrace,
);
}
final file = await assets[i].local!.originFile;
if (file == null) {
final fileName = assets[i].fileName;
@@ -111,6 +94,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 +119,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 +156,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),
),
);

Some files were not shown because too many files have changed in this diff Show More