Compare commits

..

1 Commits

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

200
cli/package-lock.json generated
View File

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

View File

@@ -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

@@ -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

@@ -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

@@ -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:

430
e2e/package-lock.json generated
View File

@@ -1149,13 +1149,13 @@
}
},
"node_modules/@playwright/test": {
"version": "1.47.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz",
"integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==",
"version": "1.47.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.0.tgz",
"integrity": "sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.47.1"
"playwright": "1.47.0"
},
"bin": {
"playwright": "cli.js"
@@ -1165,9 +1165,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz",
"integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.3.tgz",
"integrity": "sha512-MmKSfaB9GX+zXl6E8z4koOr/xU63AMVleLEa64v7R0QF/ZloMs5vcD1sHgM64GXXS1csaJutG+ddtzcueI/BLg==",
"cpu": [
"arm"
],
@@ -1179,9 +1179,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz",
"integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.3.tgz",
"integrity": "sha512-zrt8ecH07PE3sB4jPOggweBjJMzI1JG5xI2DIsUbkA+7K+Gkjys6eV7i9pOenNSDJH3eOr/jLb/PzqtmdwDq5g==",
"cpu": [
"arm64"
],
@@ -1193,9 +1193,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz",
"integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.3.tgz",
"integrity": "sha512-P0UxIOrKNBFTQaXTxOH4RxuEBVCgEA5UTNV6Yz7z9QHnUJ7eLX9reOd/NYMO3+XZO2cco19mXTxDMXxit4R/eQ==",
"cpu": [
"arm64"
],
@@ -1207,9 +1207,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz",
"integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.3.tgz",
"integrity": "sha512-L1M0vKGO5ASKntqtsFEjTq/fD91vAqnzeaF6sfNAy55aD+Hi2pBI5DKwCO+UNDQHWsDViJLqshxOahXyLSh3EA==",
"cpu": [
"x64"
],
@@ -1221,9 +1221,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz",
"integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.3.tgz",
"integrity": "sha512-btVgIsCjuYFKUjopPoWiDqmoUXQDiW2A4C3Mtmp5vACm7/GnyuprqIDPNczeyR5W8rTXEbkmrJux7cJmD99D2g==",
"cpu": [
"arm"
],
@@ -1235,9 +1235,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz",
"integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.3.tgz",
"integrity": "sha512-zmjbSphplZlau6ZTkxd3+NMtE4UKVy7U4aVFMmHcgO5CUbw17ZP6QCgyxhzGaU/wFFdTfiojjbLG3/0p9HhAqA==",
"cpu": [
"arm"
],
@@ -1249,9 +1249,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz",
"integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.3.tgz",
"integrity": "sha512-nSZfcZtAnQPRZmUkUQwZq2OjQciR6tEoJaZVFvLHsj0MF6QhNMg0fQ6mUOsiCUpTqxTx0/O6gX0V/nYc7LrgPw==",
"cpu": [
"arm64"
],
@@ -1263,9 +1263,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz",
"integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.3.tgz",
"integrity": "sha512-MnvSPGO8KJXIMGlQDYfvYS3IosFN2rKsvxRpPO2l2cum+Z3exiExLwVU+GExL96pn8IP+GdH8Tz70EpBhO0sIQ==",
"cpu": [
"arm64"
],
@@ -1277,9 +1277,9 @@
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz",
"integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.3.tgz",
"integrity": "sha512-+W+p/9QNDr2vE2AXU0qIy0qQE75E8RTwTwgqS2G5CRQ11vzq0tbnfBd6brWhS9bCRjAjepJe2fvvkvS3dno+iw==",
"cpu": [
"ppc64"
],
@@ -1291,9 +1291,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz",
"integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.3.tgz",
"integrity": "sha512-yXH6K6KfqGXaxHrtr+Uoy+JpNlUlI46BKVyonGiaD74ravdnF9BUNC+vV+SIuB96hUMGShhKV693rF9QDfO6nQ==",
"cpu": [
"riscv64"
],
@@ -1305,9 +1305,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz",
"integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.3.tgz",
"integrity": "sha512-R8cwY9wcnApN/KDYWTH4gV/ypvy9yZUHlbJvfaiXSB48JO3KpwSpjOGqO4jnGkLDSk1hgjYkTbTt6Q7uvPf8eg==",
"cpu": [
"s390x"
],
@@ -1319,9 +1319,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz",
"integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.3.tgz",
"integrity": "sha512-kZPbX/NOPh0vhS5sI+dR8L1bU2cSO9FgxwM8r7wHzGydzfSjLRCFAT87GR5U9scj2rhzN3JPYVC7NoBbl4FZ0g==",
"cpu": [
"x64"
],
@@ -1333,9 +1333,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz",
"integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.3.tgz",
"integrity": "sha512-S0Yq+xA1VEH66uiMNhijsWAafffydd2X5b77eLHfRmfLsRSpbiAWiRHV6DEpz6aOToPsgid7TI9rGd6zB1rhbg==",
"cpu": [
"x64"
],
@@ -1347,9 +1347,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz",
"integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.3.tgz",
"integrity": "sha512-9isNzeL34yquCPyerog+IMCNxKR8XYmGd0tHSV+OVx0TmE0aJOo9uw4fZfUuk2qxobP5sug6vNdZR6u7Mw7Q+Q==",
"cpu": [
"arm64"
],
@@ -1361,9 +1361,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz",
"integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.3.tgz",
"integrity": "sha512-nMIdKnfZfzn1Vsk+RuOvl43ONTZXoAPUUxgcU0tXooqg4YrAqzfKzVenqqk2g5efWh46/D28cKFrOzDSW28gTA==",
"cpu": [
"ia32"
],
@@ -1375,9 +1375,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.22.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz",
"integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==",
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.3.tgz",
"integrity": "sha512-fOvu7PCQjAj4eWDEuD8Xz5gpzFqXzGlxHZozHP4b9Jxv9APtdxL6STqztDzMLuRXEc4UpXGGhx029Xgm91QBeA==",
"cpu": [
"x64"
],
@@ -1471,9 +1471,9 @@
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true,
"license": "MIT"
},
@@ -1596,9 +1596,9 @@
}
},
"node_modules/@types/pg": {
"version": "8.11.10",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
"integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==",
"version": "8.11.9",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.9.tgz",
"integrity": "sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1733,17 +1733,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.6.0.tgz",
"integrity": "sha512-UOaz/wFowmoh2G6Mr9gw60B1mm0MzUtm6Ic8G2yM1Le6gyj5Loi/N+O5mocugRGY+8OeeKmkMmbxNqUCq3B4Sg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/type-utils": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/type-utils": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1767,16 +1767,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1796,14 +1796,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.6.0.tgz",
"integrity": "sha512-ZuoutoS5y9UOxKvpc/GkvF4cuEmpokda4wRg64JEia27wX+PysIE9q+lzDtlHHgblwUWwo5/Qn+/WyTUvDwBHw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.5.0.tgz",
"integrity": "sha512-06JOQ9Qgj33yvBEx6tpC8ecP9o860rsR22hWMEd12WcTRrfaFgHr2RB/CA/B+7BMhHkXT4chg2MyboGdFGawYg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0"
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1814,14 +1814,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.6.0.tgz",
"integrity": "sha512-dtePl4gsuenXVwC7dVNlb4mGDcKjDT/Ropsk4za/ouMBPplCLyznIaR+W65mvCvsyS97dymoBRrioEXI7k0XIg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.5.0.tgz",
"integrity": "sha512-N1K8Ix+lUM+cIDhL2uekVn/ZD7TZW+9/rwz8DclQpcQ9rk4sIL5CAlBC0CugWKREmDjBzI/kQqU4wkg46jWLYA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.6.0",
"@typescript-eslint/utils": "8.6.0",
"@typescript-eslint/typescript-estree": "8.5.0",
"@typescript-eslint/utils": "8.5.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1839,9 +1839,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1853,14 +1853,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.6.0.tgz",
"integrity": "sha512-MOVAzsKJIPIlLK239l5s06YXjNqpKTVhBVDnqUumQJja5+Y94V3+4VUFRA0G60y2jNnTVwRCkhyGQpavfsbq/g==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/visitor-keys": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/visitor-keys": "8.5.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@@ -1908,16 +1908,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "8.6.0",
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/typescript-estree": "8.6.0"
"@typescript-eslint/scope-manager": "8.5.0",
"@typescript-eslint/types": "8.5.0",
"@typescript-eslint/typescript-estree": "8.5.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1931,13 +1931,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
"version": "8.5.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.6.0",
"@typescript-eslint/types": "8.5.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1949,9 +1949,9 @@
}
},
"node_modules/@vitest/coverage-v8": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz",
"integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1972,8 +1972,8 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/browser": "2.1.1",
"vitest": "2.1.1"
"@vitest/browser": "2.1.0",
"vitest": "2.1.0"
},
"peerDependenciesMeta": {
"@vitest/browser": {
@@ -1982,14 +1982,14 @@
}
},
"node_modules/@vitest/expect": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
"integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/spy": "2.1.1",
"@vitest/utils": "2.1.1",
"@vitest/spy": "2.1.0",
"@vitest/utils": "2.1.0",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1998,9 +1998,9 @@
}
},
"node_modules/@vitest/mocker": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
"integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2012,7 +2012,7 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
"@vitest/spy": "2.1.1",
"@vitest/spy": "2.1.0",
"msw": "^2.3.5",
"vite": "^5.0.0"
},
@@ -2039,13 +2039,13 @@
}
},
"node_modules/@vitest/runner": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
"integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/utils": "2.1.1",
"@vitest/utils": "2.1.0",
"pathe": "^1.1.2"
},
"funding": {
@@ -2053,13 +2053,13 @@
}
},
"node_modules/@vitest/snapshot": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
"integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"magic-string": "^0.30.11",
"pathe": "^1.1.2"
},
@@ -2067,10 +2067,23 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/spy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
"integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2081,13 +2094,13 @@
}
},
"node_modules/@vitest/utils": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
"integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@vitest/pretty-format": "2.1.1",
"@vitest/pretty-format": "2.1.0",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -2095,6 +2108,19 @@
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/utils/node_modules/@vitest/pretty-format": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.0.tgz",
"integrity": "sha512-7sxf2F3DNYatgmzXXcTh6cq+/fxwB47RIQqZJFoSH883wnVAoccSRT6g+dTKemUBo8Q5N4OYYj1EBXLuRKvp3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -4142,9 +4168,9 @@
}
},
"node_modules/jose": {
"version": "5.9.2",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz",
"integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==",
"version": "5.8.0",
"resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
"integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
"dev": true,
"license": "MIT",
"funding": {
@@ -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

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

@@ -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

@@ -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;`);
}

View File

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

View File

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

View File

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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),
),
);

View File

@@ -8,12 +8,9 @@ import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/album_api.interface.dart';
import 'package:immich_mobile/interfaces/album_media.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/album_api.repository.dart';
import 'package:immich_mobile/repositories/album_media.repository.dart';
import 'package:immich_mobile/services/entity.service.dart';
import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/extensions/collection_extensions.dart';
@@ -21,33 +18,24 @@ import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
final syncServiceProvider = Provider(
(ref) => SyncService(
ref.watch(dbProvider),
ref.watch(hashServiceProvider),
ref.watch(entityServiceProvider),
ref.watch(albumMediaRepositoryProvider),
ref.watch(albumApiRepositoryProvider),
),
);
class SyncService {
final Isar _db;
final HashService _hashService;
final EntityService _entityService;
final IAlbumMediaRepository _albumMediaRepository;
final IAlbumApiRepository _albumApiRepository;
final AsyncMutex _lock = AsyncMutex();
final Logger _log = Logger('SyncService');
SyncService(
this._db,
this._hashService,
this._entityService,
this._albumMediaRepository,
this._albumApiRepository,
);
SyncService(this._db, this._hashService, this._albumMediaRepository);
// public methods:
@@ -77,10 +65,11 @@ class SyncService {
/// Syncs remote albums to the database
/// returns `true` if there were any changes
Future<bool> syncRemoteAlbumsToDb(
List<Album> remote, {
List<AlbumResponseDto> remote, {
required bool isShared,
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
}) =>
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
/// Syncs all device albums and their assets to the database
/// Returns `true` if there were any changes
@@ -300,10 +289,11 @@ class SyncService {
/// Syncs remote albums to the database
/// returns `true` if there were any changes
Future<bool> _syncRemoteAlbumsToDb(
List<Album> remoteAlbums,
List<AlbumResponseDto> remote,
bool isShared,
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
) async {
remoteAlbums.sortBy((e) => e.remoteId!);
remote.sortBy((e) => e.id);
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
@@ -320,14 +310,14 @@ class SyncService {
final List<Asset> existing = [];
final bool changes = await diffSortedLists(
remoteAlbums,
remote,
dbAlbums,
compare: (remoteAlbum, dbAlbum) =>
remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!),
both: (remoteAlbum, dbAlbum) =>
_syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing),
onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing),
onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete),
compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!),
both: (AlbumResponseDto a, Album b) =>
_syncRemoteAlbum(a, b, toDelete, existing, loadDetails),
onlyFirst: (AlbumResponseDto a) =>
_addAlbumFromServer(a, existing, loadDetails),
onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete),
);
if (isShared && toDelete.isNotEmpty) {
@@ -348,22 +338,26 @@ class SyncService {
/// syncing changes from local back to server)
/// accumulates
Future<bool> _syncRemoteAlbum(
Album dto,
AlbumResponseDto dto,
Album album,
List<Asset> deleteCandidates,
List<Asset> existing,
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
) async {
if (!_hasRemoteAlbumChanged(dto, album)) {
if (!_hasAlbumResponseDtoChanged(dto, album)) {
return false;
}
// loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp,
// i.e. it will always be null. Save it here.
final originalDto = dto;
dto = await _albumApiRepository.get(dto.remoteId!);
dto = await loadDetails(dto);
if (dto.assetCount != dto.assets.length) {
return false;
}
final assetsInDb =
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
final List<Asset> assetsOnRemote = dto.getAssets();
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
final (toAdd, toUpdate, toUnlink) = _diffAssets(
assetsOnRemote,
@@ -374,16 +368,15 @@ class SyncService {
// update shared users
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
final List<User> users = dto.remoteUsers.toList()
..sort((a, b) => a.id.compareTo(b.id));
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
final List<String> userIdsToAdd = [];
final List<User> usersToUnlink = [];
diffSortedListsSync(
users,
dto.albumUsers,
sharedUsers,
compare: (User a, User b) => a.id.compareTo(b.id),
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
both: (a, b) => false,
onlyFirst: (User a) => userIdsToAdd.add(a.id),
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
onlySecond: (User a) => usersToUnlink.add(a),
);
@@ -393,19 +386,19 @@ class SyncService {
final assetsToLink = existingInDb + updated;
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
album.name = dto.name;
album.name = dto.albumName;
album.shared = dto.shared;
album.createdAt = dto.createdAt;
album.modifiedAt = dto.modifiedAt;
album.modifiedAt = dto.updatedAt;
album.startDate = dto.startDate;
album.endDate = dto.endDate;
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
album.shared = dto.shared;
album.activityEnabled = dto.activityEnabled;
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
album.activityEnabled = dto.isActivityEnabled;
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
album.thumbnail.value = await _db.assets
.where()
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
.remoteIdEqualTo(dto.albumThumbnailAssetId)
.findFirst();
}
@@ -441,26 +434,27 @@ class SyncService {
/// (shared) assets to the database beforehand
/// accumulates assets already existing in the database
Future<void> _addAlbumFromServer(
Album album,
AlbumResponseDto dto,
List<Asset> existing,
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
) async {
if (album.remoteAssetCount != album.remoteAssets.length) {
album = await _albumApiRepository.get(album.remoteId!);
if (dto.assetCount != dto.assets.length) {
dto = await loadDetails(dto);
}
if (album.remoteAssetCount == album.remoteAssets.length) {
if (dto.assetCount == dto.assets.length) {
// in case an album contains assets not yet present in local DB:
// put missing album assets into local DB
final (existingInDb, updated) =
await _linkWithExistingFromDb(album.remoteAssets.toList());
await _linkWithExistingFromDb(dto.getAssets());
existing.addAll(existingInDb);
await upsertAssetsWithExif(updated);
await _entityService.fillAlbumWithDatabaseEntities(album);
await _db.writeTxn(() => _db.albums.store(album));
final Album a = await Album.remote(dto);
await _db.writeTxn(() => _db.albums.store(a));
} else {
_log.warning(
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
"asset array length ${album.remoteAssets.length} for album ${album.name}");
"Failed to add album from server: assetCount ${dto.assetCount} != "
"asset array length ${dto.assets.length} for album ${dto.albumName}");
}
}
@@ -925,17 +919,17 @@ class SyncService {
}
/// returns `true` if the albums differ on the surface
bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) {
return remoteAlbum.remoteAssetCount != dbAlbum.assetCount ||
remoteAlbum.name != dbAlbum.name ||
remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId ||
remoteAlbum.shared != dbAlbum.shared ||
remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length ||
!remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) ||
!isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) ||
!isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) ||
bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
return dto.assetCount != a.assetCount ||
dto.albumName != a.name ||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
dto.shared != a.shared ||
dto.albumUsers.length != a.sharedUsers.length ||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
!isAtSameMomentAs(
remoteAlbum.lastModifiedAssetTimestamp,
dbAlbum.lastModifiedAssetTimestamp,
dto.lastModifiedAssetTimestamp,
a.lastModifiedAssetTimestamp,
);
}

View File

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

View File

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

View File

@@ -12,19 +12,6 @@ dynamic upgradeDto(dynamic value, String targetType) {
addDefault(value, 'tags', TagsResponse().toJson());
}
break;
case 'ServerConfigDto':
if (value is Map) {
addDefault(
value,
'mapLightStyleUrl',
'https://tiles.immich.cloud/v1/style/light.json',
);
addDefault(
value,
'mapDarkStyleUrl',
'https://tiles.immich.cloud/v1/style/dark.json',
);
}
case 'UserResponseDto':
if (value is Map) {
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());

View File

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

View File

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

View File

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

View File

@@ -124,6 +124,7 @@ Class | Method | HTTP request | Description
*FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix |
*FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports |
*FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum |
*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs |
*JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs |
*JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} |
*LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries |
@@ -330,6 +331,7 @@ Class | Method | HTTP request | Description
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
- [JobCountsDto](doc//JobCountsDto.md)
- [JobCreateDto](doc//JobCreateDto.md)
- [JobName](doc//JobName.md)
- [JobSettingsDto](doc//JobSettingsDto.md)
- [JobStatusDto](doc//JobStatusDto.md)
@@ -337,11 +339,11 @@ Class | Method | HTTP request | Description
- [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md)
- [LicenseKeyDto](doc//LicenseKeyDto.md)
- [LicenseResponseDto](doc//LicenseResponseDto.md)
- [LoadTextualModelOnConnection](doc//LoadTextualModelOnConnection.md)
- [LogLevel](doc//LogLevel.md)
- [LoginCredentialDto](doc//LoginCredentialDto.md)
- [LoginResponseDto](doc//LoginResponseDto.md)
- [LogoutResponseDto](doc//LogoutResponseDto.md)
- [ManualJobName](doc//ManualJobName.md)
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
- [MapTheme](doc//MapTheme.md)
@@ -450,6 +452,7 @@ Class | Method | HTTP request | Description
- [ToneMapping](doc//ToneMapping.md)
- [TranscodeHWAccel](doc//TranscodeHWAccel.md)
- [TranscodePolicy](doc//TranscodePolicy.md)
- [TrashResponseDto](doc//TrashResponseDto.md)
- [UpdateAlbumDto](doc//UpdateAlbumDto.md)
- [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md)
- [UpdateAssetDto](doc//UpdateAssetDto.md)

View File

@@ -144,6 +144,7 @@ part 'model/image_format.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
part 'model/job_counts_dto.dart';
part 'model/job_create_dto.dart';
part 'model/job_name.dart';
part 'model/job_settings_dto.dart';
part 'model/job_status_dto.dart';
@@ -151,11 +152,11 @@ part 'model/library_response_dto.dart';
part 'model/library_stats_response_dto.dart';
part 'model/license_key_dto.dart';
part 'model/license_response_dto.dart';
part 'model/load_textual_model_on_connection.dart';
part 'model/log_level.dart';
part 'model/login_credential_dto.dart';
part 'model/login_response_dto.dart';
part 'model/logout_response_dto.dart';
part 'model/manual_job_name.dart';
part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_response_dto.dart';
part 'model/map_theme.dart';
@@ -264,6 +265,7 @@ part 'model/time_bucket_size.dart';
part 'model/tone_mapping.dart';
part 'model/transcode_hw_accel.dart';
part 'model/transcode_policy.dart';
part 'model/trash_response_dto.dart';
part 'model/update_album_dto.dart';
part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart';

View File

@@ -449,10 +449,7 @@ class AssetsApi {
return null;
}
/// This property was deprecated in v1.116.0
///
/// Note: This method returns the HTTP [Response].
///
/// Performs an HTTP 'GET /assets/random' operation and returns the [Response].
/// Parameters:
///
/// * [num] count:
@@ -485,8 +482,6 @@ class AssetsApi {
);
}
/// This property was deprecated in v1.116.0
///
/// Parameters:
///
/// * [num] count:

View File

@@ -71,63 +71,4 @@ class DeprecatedApi {
}
return null;
}
/// This property was deprecated in v1.116.0
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [num] count:
Future<Response> getRandomWithHttpInfo({ num? count, }) async {
// ignore: prefer_const_declarations
final path = r'/assets/random';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (count != null) {
queryParams.addAll(_queryParams('', 'count', count));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// This property was deprecated in v1.116.0
///
/// Parameters:
///
/// * [num] count:
Future<List<AssetResponseDto>?> getRandom({ num? count, }) async {
final response = await getRandomWithHttpInfo( count: count, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
.cast<AssetResponseDto>()
.toList(growable: false);
}
return null;
}
}

View File

@@ -105,6 +105,62 @@ class MapApi {
return null;
}
/// Performs an HTTP 'GET /map/style.json' operation and returns the [Response].
/// Parameters:
///
/// * [MapTheme] theme (required):
///
/// * [String] key:
Future<Response> getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async {
// ignore: prefer_const_declarations
final path = r'/map/style.json';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (key != null) {
queryParams.addAll(_queryParams('', 'key', key));
}
queryParams.addAll(_queryParams('', 'theme', theme));
const contentTypes = <String>[];
return apiClient.invokeAPI(
path,
'GET',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [MapTheme] theme (required):
///
/// * [String] key:
Future<Object?> getMapStyle(MapTheme theme, { String? key, }) async {
final response = await getMapStyleWithHttpInfo(theme, key: key, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object;
}
return null;
}
/// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response].
/// Parameters:
///

View File

@@ -351,53 +351,6 @@ class SearchApi {
return null;
}
/// Performs an HTTP 'POST /search/random' operation and returns the [Response].
/// Parameters:
///
/// * [RandomSearchDto] randomSearchDto (required):
Future<Response> searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async {
// ignore: prefer_const_declarations
final path = r'/search/random';
// ignore: prefer_final_locals
Object? postBody = randomSearchDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
path,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [RandomSearchDto] randomSearchDto (required):
Future<SearchResponseDto?> searchRandom(RandomSearchDto randomSearchDto,) async {
final response = await searchRandomWithHttpInfo(randomSearchDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchResponseDto',) as SearchResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /search/smart' operation and returns the [Response].
/// Parameters:
///

View File

@@ -343,6 +343,8 @@ class ApiClient {
return JobCommandDto.fromJson(value);
case 'JobCountsDto':
return JobCountsDto.fromJson(value);
case 'JobCreateDto':
return JobCreateDto.fromJson(value);
case 'JobName':
return JobNameTypeTransformer().decode(value);
case 'JobSettingsDto':
@@ -357,8 +359,6 @@ class ApiClient {
return LicenseKeyDto.fromJson(value);
case 'LicenseResponseDto':
return LicenseResponseDto.fromJson(value);
case 'LoadTextualModelOnConnection':
return LoadTextualModelOnConnection.fromJson(value);
case 'LogLevel':
return LogLevelTypeTransformer().decode(value);
case 'LoginCredentialDto':
@@ -367,6 +367,8 @@ class ApiClient {
return LoginResponseDto.fromJson(value);
case 'LogoutResponseDto':
return LogoutResponseDto.fromJson(value);
case 'ManualJobName':
return ManualJobNameTypeTransformer().decode(value);
case 'MapMarkerResponseDto':
return MapMarkerResponseDto.fromJson(value);
case 'MapReverseGeocodeResponseDto':
@@ -583,6 +585,8 @@ class ApiClient {
return TranscodeHWAccelTypeTransformer().decode(value);
case 'TranscodePolicy':
return TranscodePolicyTypeTransformer().decode(value);
case 'TrashResponseDto':
return TrashResponseDto.fromJson(value);
case 'UpdateAlbumDto':
return UpdateAlbumDto.fromJson(value);
case 'UpdateAlbumUserDto':

View File

@@ -100,6 +100,9 @@ String parameterToString(dynamic value) {
if (value is ManualJobName) {
return ManualJobNameTypeTransformer().encode(value).toString();
}
if (value is MapTheme) {
return MapThemeTypeTransformer().encode(value).toString();
}
if (value is MemoryType) {
return MemoryTypeTypeTransformer().encode(value).toString();
}

View File

@@ -78,7 +78,6 @@ class ActivityCreateDto {
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityCreateDto? fromJson(dynamic value) {
upgradeDto(value, "ActivityCreateDto");
if (value is Map) {
final json = value.cast<String, dynamic>();

View File

@@ -78,7 +78,6 @@ class ActivityResponseDto {
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static ActivityResponseDto? fromJson(dynamic value) {
upgradeDto(value, "ActivityResponseDto");
if (value is Map) {
final json = value.cast<String, dynamic>();

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