Compare commits
38 Commits
refactor/e
...
feat/prelo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59300d2097 | ||
|
|
d34d631dd4 | ||
|
|
46fe60693e | ||
|
|
06f1376de3 | ||
|
|
05d8c4c132 | ||
|
|
b45fce8ddf | ||
|
|
af8f3774d0 | ||
|
|
b85d8943e7 | ||
|
|
f031c09687 | ||
|
|
202082f62e | ||
|
|
e0fa3cdbc7 | ||
|
|
56f680ce04 | ||
|
|
ec32a9e610 | ||
|
|
bcd416477b | ||
|
|
e41785b1a1 | ||
|
|
ad33ce5938 | ||
|
|
3008050e4c | ||
|
|
87c54d6659 | ||
|
|
e748945b4f | ||
|
|
9f8a7e0bea | ||
|
|
a7719a94fc | ||
|
|
9a4a320cfb | ||
|
|
0cce7ebf25 | ||
|
|
b1cdf73a24 | ||
|
|
147747de32 | ||
|
|
9abfa6940c | ||
|
|
39ea73d654 | ||
|
|
7c1ea2dc73 | ||
|
|
14169d310a | ||
|
|
5a1a841365 | ||
|
|
af70111645 | ||
|
|
8cd3f6b884 | ||
|
|
124eb8251b | ||
|
|
529d49471f | ||
|
|
3868736799 | ||
|
|
94fc1f213a | ||
|
|
cfc575d89c | ||
|
|
708a53a1eb |
200
cli/package-lock.json
generated
200
cli/package-lock.json
generated
@@ -1353,17 +1353,17 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
|
||||
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.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",
|
||||
"@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",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1387,16 +1387,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
|
||||
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
|
||||
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1416,14 +1416,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0"
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1434,14 +1434,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.5.0",
|
||||
"@typescript-eslint/utils": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0",
|
||||
"@typescript-eslint/utils": "8.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1459,9 +1459,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
|
||||
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
|
||||
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1473,14 +1473,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
|
||||
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.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.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
|
||||
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
|
||||
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.5.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.5.0"
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1525,13 +1525,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
|
||||
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
|
||||
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1543,9 +1543,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
|
||||
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz",
|
||||
"integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1566,8 +1566,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "2.1.0",
|
||||
"vitest": "2.1.0"
|
||||
"@vitest/browser": "2.1.1",
|
||||
"vitest": "2.1.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1576,14 +1576,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
|
||||
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
|
||||
"integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "2.1.0",
|
||||
"@vitest/utils": "2.1.0",
|
||||
"@vitest/spy": "2.1.1",
|
||||
"@vitest/utils": "2.1.1",
|
||||
"chai": "^5.1.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -1592,9 +1592,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
|
||||
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
|
||||
"integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1606,7 +1606,7 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/spy": "2.1.0",
|
||||
"@vitest/spy": "2.1.1",
|
||||
"msw": "^2.3.5",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
@@ -1633,13 +1633,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
|
||||
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
|
||||
"integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "2.1.0",
|
||||
"@vitest/utils": "2.1.1",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -1647,13 +1647,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
|
||||
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
|
||||
"integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.1.0",
|
||||
"@vitest/pretty-format": "2.1.1",
|
||||
"magic-string": "^0.30.11",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
@@ -1661,23 +1661,10 @@
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
|
||||
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
|
||||
"integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1688,13 +1675,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
|
||||
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
|
||||
"integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.1.0",
|
||||
"@vitest/pretty-format": "2.1.1",
|
||||
"loupe": "^3.1.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -1702,19 +1689,6 @@
|
||||
"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",
|
||||
@@ -4241,9 +4215,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
|
||||
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz",
|
||||
"integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -4283,19 +4257,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
|
||||
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz",
|
||||
"integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"chai": "^5.1.1",
|
||||
"debug": "^4.3.6",
|
||||
"magic-string": "^0.30.11",
|
||||
@@ -4306,7 +4280,7 @@
|
||||
"tinypool": "^1.0.0",
|
||||
"tinyrainbow": "^1.2.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "2.1.0",
|
||||
"vite-node": "2.1.1",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -4321,8 +4295,8 @@
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "2.1.0",
|
||||
"@vitest/ui": "2.1.0",
|
||||
"@vitest/browser": "2.1.1",
|
||||
"@vitest/ui": "2.1.1",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
||||
@@ -51,5 +51,4 @@ services:
|
||||
volumes:
|
||||
- /usr/lib/wsl:/usr/lib/wsl
|
||||
environment:
|
||||
- LD_LIBRARY_PATH=/usr/lib/wsl/lib
|
||||
- LIBVA_DRIVER_NAME=d3d12
|
||||
|
||||
@@ -64,3 +64,43 @@ 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`.
|
||||
|
||||
@@ -3,6 +3,7 @@ sidebar_position: 1
|
||||
---
|
||||
|
||||
import AppArchitecture from './img/app-architecture.png';
|
||||
import MobileArchitecture from './img/immich_mobile_architecture.svg';
|
||||
|
||||
# Architecture
|
||||
|
||||
@@ -28,7 +29,14 @@ All three clients use [OpenAPI](./open-api.md) to auto-generate rest clients for
|
||||
|
||||
### Mobile App
|
||||
|
||||
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.
|
||||
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!
|
||||
|
||||
### Web Client
|
||||
|
||||
|
||||
104
docs/docs/developer/img/immich_mobile_architecture.drawio
Normal file
104
docs/docs/developer/img/immich_mobile_architecture.drawio
Normal file
@@ -0,0 +1,104 @@
|
||||
<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="<b><font style="font-size: 22px;">Mobile App</font></b>" 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<div>system</div>" 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<div>database</div>" 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>
|
||||
3
docs/docs/developer/img/immich_mobile_architecture.svg
Normal file
3
docs/docs/developer/img/immich_mobile_architecture.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 20 KiB |
@@ -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 toturial
|
||||
:::tip Video tutorial
|
||||
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.
|
||||
:::
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ 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
|
||||
|
||||
|
||||
@@ -27,23 +27,14 @@ 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 | | server | microservices |
|
||||
| `TZ` | Timezone | <sup>\*1</sup> | 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>\*1</sup>⚠️ | `./upload`<sup>\*2</sup> | server | 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_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 | |
|
||||
@@ -52,16 +43,13 @@ Regardless of filesystem, it is not recommended to use a network share for your
|
||||
| `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: 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: 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"`.
|
||||
|
||||
\*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.
|
||||
:::
|
||||
|
||||
\*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`.
|
||||
It only need to be set if the Immich deployment method is changing.
|
||||
|
||||
## Workers
|
||||
|
||||
|
||||
@@ -23,7 +23,33 @@ 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 [here](/docs/install/environment-variables#supported-filesystems)
|
||||
for more details and alternatives.
|
||||
- This can present an issue for Windows users. See below for details and an alternative setup.
|
||||
- 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.
|
||||
- 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>
|
||||
|
||||
6
docs/package-lock.json
generated
6
docs/package-lock.json
generated
@@ -16091,9 +16091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.11",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.11.tgz",
|
||||
"integrity": "sha512-qhEuBcLemjSJk5ajccN9xJFtM/h0AVCPaA6C92jNP+M2J8kX+eMJHI7R2HFKUvvAsMpcfLILMCFYSeDwpMmlUg==",
|
||||
"version": "3.4.12",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
|
||||
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
|
||||
@@ -22,7 +22,6 @@ services:
|
||||
- IMMICH_METRICS=true
|
||||
- IMMICH_ENV=testing
|
||||
volumes:
|
||||
- upload:/usr/src/app/upload
|
||||
- ./test-assets:/test-assets
|
||||
extra_hosts:
|
||||
- 'auth-server:host-gateway'
|
||||
@@ -44,7 +43,3 @@ services:
|
||||
POSTGRES_DB: immich
|
||||
ports:
|
||||
- 5435:5432
|
||||
|
||||
volumes:
|
||||
model-cache:
|
||||
upload:
|
||||
|
||||
430
e2e/package-lock.json
generated
430
e2e/package-lock.json
generated
@@ -1149,13 +1149,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.0.tgz",
|
||||
"integrity": "sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==",
|
||||
"version": "1.47.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.1.tgz",
|
||||
"integrity": "sha512-dbWpcNQZ5nj16m+A5UNScYx7HX5trIy7g4phrcitn+Nk83S32EBX/CLU4hiF4RGKX/yRc93AAqtfaXB7JWBd4Q==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.47.0"
|
||||
"playwright": "1.47.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -1165,9 +1165,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1179,9 +1179,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1193,9 +1193,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1207,9 +1207,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1221,9 +1221,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1235,9 +1235,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1249,9 +1249,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1263,9 +1263,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1277,9 +1277,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -1291,9 +1291,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1305,9 +1305,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -1319,9 +1319,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1333,9 +1333,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1347,9 +1347,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1361,9 +1361,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1375,9 +1375,9 @@
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"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==",
|
||||
"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==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1471,9 +1471,9 @@
|
||||
}
|
||||
},
|
||||
"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==",
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1596,9 +1596,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.9.tgz",
|
||||
"integrity": "sha512-M4mYeJZRBD9lCBCGa72F44uKSV9eJrAFfjlPJagdA6pgIr2OPJULFB7nqnZzOdqXG0qzHlgtZKzTdIgbmHitSg==",
|
||||
"version": "8.11.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz",
|
||||
"integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1733,17 +1733,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.5.0.tgz",
|
||||
"integrity": "sha512-lHS5hvz33iUFQKuPFGheAB84LwcJ60G8vKnEhnfcK1l8kGVLro2SFYW6K0/tj8FUhRJ0VHyg1oAfg50QGbPPHw==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.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",
|
||||
"@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",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
@@ -1767,16 +1767,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.5.0.tgz",
|
||||
"integrity": "sha512-gF77eNv0Xz2UJg/NbpWJ0kqAm35UMsvZf1GHj8D9MRFTj/V3tAciIWXfmPLsAAF/vUlpWPvUDyH1jjsr0cMVWw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.6.0.tgz",
|
||||
"integrity": "sha512-eQcbCuA2Vmw45iGfcyG4y6rS7BhWfz9MQuk409WD47qMM+bKCGQWXxvoOs1DUp+T7UBMTtRTVT+kXr7Sh4O9Ow==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1796,14 +1796,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0"
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1814,14 +1814,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "8.5.0",
|
||||
"@typescript-eslint/utils": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0",
|
||||
"@typescript-eslint/utils": "8.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
@@ -1839,9 +1839,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.5.0.tgz",
|
||||
"integrity": "sha512-qjkormnQS5wF9pjSi6q60bKUHH44j2APxfh9TQRXK8wbYVeDYYdYJGIROL87LGZZ2gz3Rbmjc736qyL8deVtdw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.6.0.tgz",
|
||||
"integrity": "sha512-rojqFZGd4MQxw33SrOy09qIDS8WEldM8JWtKQLAjf/X5mGSeEFh5ixQlxssMNyPslVIk9yzWqXCsV2eFhYrYUw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -1853,14 +1853,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.5.0.tgz",
|
||||
"integrity": "sha512-vEG2Sf9P8BPQ+d0pxdfndw3xIXaoSjliG0/Ejk7UggByZPKXmJmw3GW5jV2gHNQNawBUyfahoSiCFVov0Ruf7Q==",
|
||||
"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==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/visitor-keys": "8.5.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/visitor-keys": "8.6.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.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.5.0.tgz",
|
||||
"integrity": "sha512-6yyGYVL0e+VzGYp60wvkBHiqDWOpT63pdMV2CVG4LVDd5uR6q1qQN/7LafBZtAtNIn/mqXjsSeS5ggv/P0iECw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.6.0.tgz",
|
||||
"integrity": "sha512-eNp9cWnYf36NaOVjkEUznf6fEgVy1TWpE0o52e4wtojjBx7D1UV2WAWGzR+8Y5lVFtpMLPwNbC67T83DWSph4A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "8.5.0",
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/typescript-estree": "8.5.0"
|
||||
"@typescript-eslint/scope-manager": "8.6.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"@typescript-eslint/typescript-estree": "8.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1931,13 +1931,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.5.0.tgz",
|
||||
"integrity": "sha512-yTPqMnbAZJNy2Xq2XU8AdtOW9tJIr+UQb64aXB9f3B1498Zx9JorVgFJcZpEc9UBuCCrdzKID2RGAMkYcDtZOw==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.6.0.tgz",
|
||||
"integrity": "sha512-wapVFfZg9H0qOYh4grNVQiMklJGluQrOUiOhYRrQWhx7BY/+I1IYb8BczWNbbUpO+pqy0rDciv3lQH5E1bCLrg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.5.0",
|
||||
"@typescript-eslint/types": "8.6.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1949,9 +1949,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.0.tgz",
|
||||
"integrity": "sha512-yqCkr2nrV4o58VcVMxTVkS6Ggxzy7pmSD8JbTbhbH5PsQfUIES1QT716VUzo33wf2lX9EcWYdT3Vl2MMmjR59g==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.1.tgz",
|
||||
"integrity": "sha512-md/A7A3c42oTT8JUHSqjP5uKTWJejzUW4jalpvs+rZ27gsURsMU8DEb+8Jf8C6Kj2gwfSHJqobDNBuoqlm0cFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1972,8 +1972,8 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "2.1.0",
|
||||
"vitest": "2.1.0"
|
||||
"@vitest/browser": "2.1.1",
|
||||
"vitest": "2.1.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
@@ -1982,14 +1982,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.0.tgz",
|
||||
"integrity": "sha512-N3/xR4fSu0+6sVZETEtPT1orUs2+Y477JOXTcU3xKuu3uBlsgbD7/7Mz2LZ1Jr1XjwilEWlrIgSCj4N1+5ZmsQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz",
|
||||
"integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/spy": "2.1.0",
|
||||
"@vitest/utils": "2.1.0",
|
||||
"@vitest/spy": "2.1.1",
|
||||
"@vitest/utils": "2.1.1",
|
||||
"chai": "^5.1.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -1998,9 +1998,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/mocker": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.0.tgz",
|
||||
"integrity": "sha512-ZxENovUqhzl+QiOFpagiHUNUuZ1qPd5yYTCYHomGIZOFArzn4mgX2oxZmiAItJWAaXHG6bbpb/DpSPhlk5DgtA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
|
||||
"integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2012,7 +2012,7 @@
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/spy": "2.1.0",
|
||||
"@vitest/spy": "2.1.1",
|
||||
"msw": "^2.3.5",
|
||||
"vite": "^5.0.0"
|
||||
},
|
||||
@@ -2039,13 +2039,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.0.tgz",
|
||||
"integrity": "sha512-D9+ZiB8MbMt7qWDRJc4CRNNUlne/8E1X7dcKhZVAbcOKG58MGGYVDqAq19xlhNfMFZsW0bpVKgztBwks38Ko0w==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz",
|
||||
"integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/utils": "2.1.0",
|
||||
"@vitest/utils": "2.1.1",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
"funding": {
|
||||
@@ -2053,13 +2053,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.0.tgz",
|
||||
"integrity": "sha512-x69CygGMzt9VCO283K2/FYQ+nBrOj66OTKpsPykjCR4Ac3lLV+m85hj9reaIGmjBSsKzVvbxWmjWE3kF5ha3uQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz",
|
||||
"integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.1.0",
|
||||
"@vitest/pretty-format": "2.1.1",
|
||||
"magic-string": "^0.30.11",
|
||||
"pathe": "^1.1.2"
|
||||
},
|
||||
@@ -2067,23 +2067,10 @@
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.0.tgz",
|
||||
"integrity": "sha512-IXX5NkbdgTYTog3F14i2LgnBc+20YmkXMx0IWai84mcxySUDRgm0ihbOfR4L0EVRBDFG85GjmQQEZNNKVVpkZw==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz",
|
||||
"integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2094,13 +2081,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.0.tgz",
|
||||
"integrity": "sha512-rreyfVe0PuNqJfKYUwfPDfi6rrp0VSu0Wgvp5WBqJonP+4NvXHk48X6oBam1Lj47Hy6jbJtnMj3OcRdrkTP0tA==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz",
|
||||
"integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/pretty-format": "2.1.0",
|
||||
"@vitest/pretty-format": "2.1.1",
|
||||
"loupe": "^3.1.1",
|
||||
"tinyrainbow": "^1.2.0"
|
||||
},
|
||||
@@ -2108,19 +2095,6 @@
|
||||
"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",
|
||||
@@ -4168,9 +4142,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "5.8.0",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
|
||||
"integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
|
||||
"version": "5.9.2",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-5.9.2.tgz",
|
||||
"integrity": "sha512-ILI2xx/I57b20sd7rHZvgiiQrmp2mcotwsAH+5ajbpFQbrYVQdNHYlQhoA5cFb78CgtBOxtC05TeA+mcgkuCqQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
@@ -5016,9 +4990,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"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==",
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pathe": {
|
||||
@@ -5039,15 +5013,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
|
||||
"integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.0.tgz",
|
||||
"integrity": "sha512-34wkUTh3SxTClfoHB3pQ7bIMvw9dpFU1audQQeZG837fmHfHpr14n/AELVDoOYVDW2h5RDWU78tFjkD+erSBsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.6.4",
|
||||
"pg-pool": "^3.6.2",
|
||||
"pg-protocol": "^1.6.1",
|
||||
"pg-connection-string": "^2.7.0",
|
||||
"pg-pool": "^3.7.0",
|
||||
"pg-protocol": "^1.7.0",
|
||||
"pg-types": "^2.1.0",
|
||||
"pgpass": "1.x"
|
||||
},
|
||||
@@ -5074,10 +5048,11 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"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
|
||||
"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"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
@@ -5098,19 +5073,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"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==",
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.7.0.tgz",
|
||||
"integrity": "sha512-ZOBQForurqh4zZWjrgSwwAtzJ7QiRX0ovFkZr2klsen3Nm0aoh33Ls0fzfv3imeH/nw/O27cjdz5kzYJfeGp/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz",
|
||||
"integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==",
|
||||
"dev": true
|
||||
"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"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
@@ -5158,13 +5135,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.47.0",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.0.tgz",
|
||||
"integrity": "sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==",
|
||||
"version": "1.47.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.1.tgz",
|
||||
"integrity": "sha512-SUEKi6947IqYbKxRiqnbUobVZY4bF1uu+ZnZNJX9DfU1tlf2UhWfvVjLf01pQx9URsOr18bFVUKXmanYWhbfkw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.47.0"
|
||||
"playwright-core": "1.47.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
@@ -5177,9 +5154,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"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==",
|
||||
"version": "1.47.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.1.tgz",
|
||||
"integrity": "sha512-i1iyJdLftqtt51mEk6AhYFaAJCDx0xQ/O5NU8EKaWFgMjItPVma542Nh/Aq8aLCjIJSzjaiEQGW/nyqLkGF1OQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
@@ -5629,9 +5606,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.21.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.3.tgz",
|
||||
"integrity": "sha512-7sqRtBNnEbcBtMeRVc6VRsJMmpI+JU1z9VTvW8D4gXIYQFz0aLcsE6rRkyghZkLfEgUZgVvOG7A5CVz/VW5GIA==",
|
||||
"version": "4.22.4",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
|
||||
"integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5645,25 +5622,32 @@
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"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",
|
||||
@@ -6403,9 +6387,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.6",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz",
|
||||
"integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==",
|
||||
"version": "5.4.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz",
|
||||
"integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6463,9 +6447,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.0.tgz",
|
||||
"integrity": "sha512-+ybYqBVUjYyIscoLzMWodus2enQDZOpGhcU6HdOVD6n8WZdk12w1GFL3mbnxLs7hPtRtqs1Wo5YF6/Tsr6fmhg==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz",
|
||||
"integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6500,19 +6484,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.0.tgz",
|
||||
"integrity": "sha512-XuuEeyNkqbfr0FtAvd9vFbInSSNY1ykCQTYQ0sj9wPy4hx+1gR7gqVNdW0AX2wrrM1wWlN5fnJDjF9xG6mYRSQ==",
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz",
|
||||
"integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@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",
|
||||
"@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",
|
||||
"chai": "^5.1.1",
|
||||
"debug": "^4.3.6",
|
||||
"magic-string": "^0.30.11",
|
||||
@@ -6523,7 +6507,7 @@
|
||||
"tinypool": "^1.0.0",
|
||||
"tinyrainbow": "^1.2.0",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "2.1.0",
|
||||
"vite-node": "2.1.1",
|
||||
"why-is-node-running": "^2.3.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -6538,8 +6522,8 @@
|
||||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "2.1.0",
|
||||
"@vitest/ui": "2.1.0",
|
||||
"@vitest/browser": "2.1.1",
|
||||
"@vitest/ui": "2.1.1",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
||||
@@ -53,8 +53,10 @@ export default defineConfig({
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'docker compose up --build -V --remove-orphans',
|
||||
command: 'docker compose up --build --renew-anon-volumes --force-recreate --remove-orphans',
|
||||
url: 'http://127.0.0.1:2285',
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||
import { LoginResponseDto } 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';
|
||||
@@ -11,18 +10,13 @@ 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) => {
|
||||
@@ -103,63 +97,6 @@ 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');
|
||||
|
||||
@@ -128,6 +128,8 @@ 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,8 @@ 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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,7 +12,8 @@ const setup = async () => {
|
||||
|
||||
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
|
||||
|
||||
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
|
||||
const command = 'compose up --build --renew-anon-volumes --force-recreate --remove-orphans';
|
||||
const child = spawn('docker', command.split(' '), { stdio: 'pipe' });
|
||||
|
||||
child.stdout.on('data', (data) => {
|
||||
const input = data.toString();
|
||||
|
||||
@@ -156,8 +156,7 @@ export const utils = {
|
||||
|
||||
for (const table of tables) {
|
||||
if (table === 'system_metadata') {
|
||||
// prevent reverse geocoder from being re-initialized
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" NOT IN ('reverse-geocoding-state', 'system-flags');`);
|
||||
} else {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
from fastapi import Depends, FastAPI, File, Form, HTTPException, Response
|
||||
from fastapi.responses import ORJSONResponse
|
||||
from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile
|
||||
from PIL.Image import Image
|
||||
@@ -124,6 +124,23 @@ 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)
|
||||
|
||||
|
||||
@@ -137,6 +154,20 @@ 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),
|
||||
|
||||
@@ -58,3 +58,10 @@ 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)
|
||||
|
||||
50
machine-learning/poetry.lock
generated
50
machine-learning/poetry.lock
generated
@@ -1237,13 +1237,13 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "huggingface-hub"
|
||||
version = "0.24.6"
|
||||
version = "0.25.0"
|
||||
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.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"},
|
||||
{file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"},
|
||||
{file = "huggingface_hub-0.25.0-py3-none-any.whl", hash = "sha256:e2f357b35d72d5012cfd127108c4e14abcd61ba4ebc90a5a374dc2456cb34e12"},
|
||||
{file = "huggingface_hub-0.25.0.tar.gz", hash = "sha256:fb5fbe6c12fcd99d187ec7db95db9110fb1a20505f23040a5449a717c1a0db4d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -1531,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "locust"
|
||||
version = "2.31.5"
|
||||
version = "2.31.6"
|
||||
description = "Developer-friendly load testing framework"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
|
||||
{file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
|
||||
{file = "locust-2.31.6-py3-none-any.whl", hash = "sha256:004c963c7a588dc15d57d710cdc6a262d85b57936d7fad3c38ac0657aa98fc3b"},
|
||||
{file = "locust-2.31.6.tar.gz", hash = "sha256:03b6da0491d6a0b905692d9ac128d9deec403f40dc605c481a90dbab5126318c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -2834,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.6.4"
|
||||
version = "0.6.6"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{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"},
|
||||
{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"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"flutter": "3.24.0"
|
||||
"flutter": "3.24.3"
|
||||
}
|
||||
|
||||
2
mobile/.vscode/settings.json
vendored
2
mobile/.vscode/settings.json
vendored
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.24.0",
|
||||
"dart.flutterSdkPath": ".fvm/versions/3.24.3",
|
||||
"search.exclude": {
|
||||
"**/.fvm": true
|
||||
},
|
||||
|
||||
@@ -46,21 +46,63 @@ custom_lint:
|
||||
- avoid_public_notifier_properties: false
|
||||
- avoid_manual_providers_as_generated_provider_dependency: false
|
||||
- unsupported_provider_value: false
|
||||
- photo_manager:
|
||||
exclude:
|
||||
- import_rule_photo_manager:
|
||||
message: photo_manager must only be used in MediaRepositories
|
||||
restrict: package:photo_manager
|
||||
allowed:
|
||||
# required / wanted
|
||||
- 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
|
||||
- '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
|
||||
|
||||
dart_code_metrics:
|
||||
metrics:
|
||||
|
||||
@@ -1,36 +1,61 @@
|
||||
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) => [
|
||||
PhotoManagerRule(configs.rules[PhotoManagerRule._code.name]),
|
||||
];
|
||||
}
|
||||
|
||||
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));
|
||||
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;
|
||||
}
|
||||
|
||||
final Set<String> _excludePaths = HashSet();
|
||||
static makeCode(String name, LintOptions options) => LintCode(
|
||||
name: name,
|
||||
problemMessage: options.json["message"] as String,
|
||||
errorSeverity: ErrorSeverity.WARNING,
|
||||
);
|
||||
|
||||
static const _code = LintCode(
|
||||
name: 'photo_manager',
|
||||
problemMessage:
|
||||
'photo_manager library must only be used in MediaRepository',
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class ImportRule extends DartLintRule {
|
||||
ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
|
||||
: super(code: code);
|
||||
|
||||
final Glob? _allowed;
|
||||
final Glob? _forbidden;
|
||||
final List<String> _restrict;
|
||||
int _rootOffset = -1;
|
||||
|
||||
@override
|
||||
void run(
|
||||
@@ -38,11 +63,23 @@ class PhotoManagerRule extends DartLintRule {
|
||||
ErrorReporter reporter,
|
||||
CustomLintContext context,
|
||||
) {
|
||||
if (_excludePaths.contains(resolver.source.shortName)) return;
|
||||
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;
|
||||
|
||||
context.registry.addImportDirective((node) {
|
||||
if (node.uri.stringValue?.startsWith("package:photo_manager") == true) {
|
||||
reporter.atNode(node, code);
|
||||
final uri = node.uri.stringValue;
|
||||
if (uri == null) return;
|
||||
for (final restricted in _restrict) {
|
||||
if (uri.startsWith(restricted) == true) {
|
||||
reporter.atNode(node, code);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ packages:
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
glob:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: glob
|
||||
sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
|
||||
|
||||
@@ -8,6 +8,7 @@ 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
|
||||
|
||||
1
mobile/lib/constants/constants.dart
Normal file
1
mobile/lib/constants/constants.dart
Normal file
@@ -0,0 +1 @@
|
||||
const int noDbId = -9223372036854775808; // from Isar
|
||||
@@ -3,6 +3,8 @@ 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';
|
||||
@@ -23,6 +25,7 @@ class Album {
|
||||
required this.activityEnabled,
|
||||
});
|
||||
|
||||
// fields stored in DB
|
||||
Id id = Isar.autoIncrement;
|
||||
@Index(unique: false, replace: false, type: IndexType.hash)
|
||||
String? remoteId;
|
||||
@@ -41,9 +44,17 @@ 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;
|
||||
|
||||
@@ -74,6 +85,18 @@ 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;
|
||||
@@ -129,6 +152,7 @@ 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
|
||||
@@ -164,7 +188,3 @@ extension AssetsHelper on IsarCollection<Album> {
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
extension AlbumResponseDtoHelper on AlbumResponseDto {
|
||||
List<Asset> getAssets() => assets.map(Asset.remote).toList();
|
||||
}
|
||||
|
||||
16
mobile/lib/interfaces/activity_api.interface.dart
Normal file
16
mobile/lib/interfaces/activity_api.interface.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
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});
|
||||
}
|
||||
40
mobile/lib/interfaces/album_api.interface.dart
Normal file
40
mobile/lib/interfaces/album_api.interface.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
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});
|
||||
}
|
||||
@@ -1,8 +1,27 @@
|
||||
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);
|
||||
}
|
||||
|
||||
18
mobile/lib/interfaces/asset_api.interface.dart
Normal file
18
mobile/lib/interfaces/asset_api.interface.dart
Normal file
@@ -0,0 +1,18 @@
|
||||
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 []});
|
||||
}
|
||||
9
mobile/lib/interfaces/exif_info.interface.dart
Normal file
9
mobile/lib/interfaces/exif_info.interface.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
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);
|
||||
}
|
||||
13
mobile/lib/interfaces/partner_api.interface.dart
Normal file
13
mobile/lib/interfaces/partner_api.interface.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
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,
|
||||
}
|
||||
22
mobile/lib/interfaces/person_api.interface.dart
Normal file
22
mobile/lib/interfaces/person_api.interface.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
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;
|
||||
}
|
||||
@@ -2,4 +2,7 @@ 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);
|
||||
}
|
||||
|
||||
11
mobile/lib/interfaces/user_api.interface.dart
Normal file
11
mobile/lib/interfaces/user_api.interface.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
enum ActivityType { comment, like }
|
||||
|
||||
@@ -38,16 +37,6 @@ 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)';
|
||||
@@ -75,3 +64,9 @@ class Activity {
|
||||
user.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityStats {
|
||||
final int comments;
|
||||
|
||||
const ActivityStats({required this.comments});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/interfaces/person_api.interface.dart';
|
||||
|
||||
class SearchLocationFilter {
|
||||
String? country;
|
||||
@@ -235,7 +235,7 @@ class SearchDisplayFilters {
|
||||
class SearchFilter {
|
||||
String? context;
|
||||
String? filename;
|
||||
Set<PersonResponseDto> people;
|
||||
Set<Person> people;
|
||||
SearchLocationFilter location;
|
||||
SearchCameraFilter camera;
|
||||
SearchDateFilter date;
|
||||
@@ -258,7 +258,7 @@ class SearchFilter {
|
||||
SearchFilter copyWith({
|
||||
String? context,
|
||||
String? filename,
|
||||
Set<PersonResponseDto>? people,
|
||||
Set<Person>? people,
|
||||
SearchLocationFilter? location,
|
||||
SearchCameraFilter? camera,
|
||||
SearchDateFilter? date,
|
||||
|
||||
@@ -4,11 +4,15 @@ 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({
|
||||
@@ -20,6 +24,8 @@ class ServerConfig {
|
||||
trashDays: trashDays ?? this.trashDays,
|
||||
oauthButtonText: oauthButtonText ?? this.oauthButtonText,
|
||||
externalDomain: externalDomain ?? this.externalDomain,
|
||||
mapDarkStyleUrl: mapDarkStyleUrl,
|
||||
mapLightStyleUrl: mapLightStyleUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +36,9 @@ class ServerConfig {
|
||||
ServerConfig.fromDto(ServerConfigDto dto)
|
||||
: trashDays = dto.trashDays,
|
||||
oauthButtonText = dto.oauthButtonText,
|
||||
externalDomain = dto.externalDomain;
|
||||
externalDomain = dto.externalDomain,
|
||||
mapDarkStyleUrl = dto.mapDarkStyleUrl,
|
||||
mapLightStyleUrl = dto.mapLightStyleUrl;
|
||||
|
||||
@override
|
||||
bool operator ==(covariant ServerConfig other) {
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -30,7 +31,6 @@ 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 == Isar.autoIncrement;
|
||||
final isFromDto = currentAsset.id == noDbId;
|
||||
|
||||
Asset asset = stackIndex.value == -1
|
||||
? currentAsset
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -7,27 +8,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()
|
||||
@@ -304,7 +305,7 @@ class MapPage extends HookConsumerWidget {
|
||||
),
|
||||
Positioned(
|
||||
right: 0,
|
||||
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||
bottom: MediaQuery.paddingOf(context).bottom + 16,
|
||||
child: ElevatedButton(
|
||||
onPressed: onZoomToLocation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -19,7 +20,6 @@ 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<PersonResponseDto> value) {
|
||||
handleOnSelect(Set<Person> value) {
|
||||
filter.value = filter.value.copyWith(
|
||||
people: value,
|
||||
);
|
||||
|
||||
@@ -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(apiServiceProvider));
|
||||
ActivityService(ref.watch(activityApiRepositoryProvider));
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'activity_service.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0';
|
||||
String _$activityServiceHash() => r'23a3ee7db71676d2719daa64217a683cc5c7eab0';
|
||||
|
||||
/// See also [activityService].
|
||||
@ProviderFor(activityService)
|
||||
|
||||
@@ -11,7 +11,7 @@ class ActivityStatistics extends _$ActivityStatistics {
|
||||
ref
|
||||
.watch(activityServiceProvider)
|
||||
.getStatistics(albumId, assetId: assetId)
|
||||
.then((comments) => state = comments);
|
||||
.then((stats) => state = stats.comments);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'activity_statistics.provider.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$activityStatisticsHash() =>
|
||||
r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf';
|
||||
r'1f43f0bcb11c754ca3cb586a13570db25023b9a8';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
|
||||
@@ -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.getUsersInDb();
|
||||
return userService.getUsers();
|
||||
});
|
||||
|
||||
@@ -313,6 +313,9 @@ 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 = {};
|
||||
@@ -408,9 +411,6 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
|
||||
);
|
||||
}
|
||||
|
||||
// Save to persistent storage
|
||||
await _updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
/// Get all necessary information for calculating the available albums,
|
||||
|
||||
@@ -1,28 +1,23 @@
|
||||
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);
|
||||
|
||||
// Fetch and save the Style JSONs
|
||||
loadStyles();
|
||||
final lightStyleUrl =
|
||||
ref.read(serverInfoProvider).serverConfig.mapLightStyleUrl;
|
||||
final darkStyleUrl =
|
||||
ref.read(serverInfoProvider).serverConfig.mapDarkStyleUrl;
|
||||
|
||||
return MapState(
|
||||
themeMode: ThemeMode.values[
|
||||
appSettingsProvider.getSetting<int>(AppSettingsEnum.mapThemeMode)],
|
||||
@@ -34,65 +29,11 @@ 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,
|
||||
|
||||
@@ -6,7 +6,7 @@ part of 'map_state.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$mapStateNotifierHash() => r'31fafe17aa85c48379a22ed3db3cc94af59ce5b8';
|
||||
String _$mapStateNotifierHash() => r'22e4e571bd0730dbc34b109255a62b920e9c7d66';
|
||||
|
||||
/// See also [MapStateNotifier].
|
||||
@ProviderFor(MapStateNotifier)
|
||||
|
||||
@@ -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<PersonResponseDto>> getAllPeople(
|
||||
Future<List<Person>> getAllPeople(
|
||||
GetAllPeopleRef ref,
|
||||
) async {
|
||||
final PersonService personService = ref.read(personServiceProvider);
|
||||
|
||||
@@ -6,12 +6,11 @@ part of 'people.provider.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd';
|
||||
String _$getAllPeopleHash() => r'3417b7e0c211382d4480a415e352139995d57b6d';
|
||||
|
||||
/// See also [getAllPeople].
|
||||
@ProviderFor(getAllPeople)
|
||||
final getAllPeopleProvider =
|
||||
AutoDisposeFutureProvider<List<PersonResponseDto>>.internal(
|
||||
final getAllPeopleProvider = AutoDisposeFutureProvider<List<Person>>.internal(
|
||||
getAllPeople,
|
||||
name: r'getAllPeopleProvider',
|
||||
debugGetCreateSourceHash:
|
||||
@@ -20,7 +19,7 @@ final getAllPeopleProvider =
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<PersonResponseDto>>;
|
||||
typedef GetAllPeopleRef = AutoDisposeFutureProviderRef<List<Person>>;
|
||||
String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
|
||||
@@ -34,6 +34,9 @@ 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",
|
||||
|
||||
67
mobile/lib/repositories/activity_api.repository.dart
Normal file
67
mobile/lib/repositories/activity_api.repository.dart
Normal file
@@ -0,0 +1,67 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
167
mobile/lib/repositories/album_api.repository.dart
Normal file
167
mobile/lib/repositories/album_api.repository.dart
Normal file
@@ -0,0 +1,167 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
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';
|
||||
@@ -28,4 +33,111 @@ 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();
|
||||
|
||||
52
mobile/lib/repositories/asset_api.repository.dart
Normal file
52
mobile/lib/repositories/asset_api.repository.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
11
mobile/lib/repositories/base_api.repository.dart
Normal file
11
mobile/lib/repositories/base_api.repository.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
28
mobile/lib/repositories/exif_info.repository.dart
Normal file
28
mobile/lib/repositories/exif_info.repository.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
51
mobile/lib/repositories/partner_api.repository.dart
Normal file
51
mobile/lib/repositories/partner_api.repository.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
38
mobile/lib/repositories/person_api.repository.dart
Normal file
38
mobile/lib/repositories/person_api.repository.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
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,
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
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';
|
||||
@@ -17,4 +18,22 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
41
mobile/lib/repositories/user_api.repository.dart
Normal file
41
mobile/lib/repositories/user_api.repository.dart
Normal file
@@ -0,0 +1,41 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +1,31 @@
|
||||
import 'package:immich_mobile/constants/errors.dart';
|
||||
import 'package:immich_mobile/interfaces/activity_api.interface.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 ApiService _apiService;
|
||||
final IActivityApiRepository _activityApiRepository;
|
||||
|
||||
@override
|
||||
final Logger logger = Logger("ActivityService");
|
||||
|
||||
ActivityService(this._apiService);
|
||||
ActivityService(this._activityApiRepository);
|
||||
|
||||
Future<List<Activity>> getAllActivities(
|
||||
String albumId, {
|
||||
String? assetId,
|
||||
}) async {
|
||||
return logError(
|
||||
() async {
|
||||
final list = await _apiService.activitiesApi
|
||||
.getActivities(albumId, assetId: assetId);
|
||||
return list != null ? list.map(Activity.fromDto).toList() : [];
|
||||
},
|
||||
() => _activityApiRepository.getAll(albumId, assetId: assetId),
|
||||
defaultValue: [],
|
||||
errorMessage: "Failed to get all activities for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> getStatistics(String albumId, {String? assetId}) async {
|
||||
Future<ActivityStats> getStatistics(String albumId, {String? assetId}) async {
|
||||
return logError(
|
||||
() async {
|
||||
final dto = await _apiService.activitiesApi
|
||||
.getActivityStatistics(albumId, assetId: assetId);
|
||||
return dto?.comments ?? 0;
|
||||
},
|
||||
defaultValue: 0,
|
||||
() => _activityApiRepository.getStats(albumId, assetId: assetId),
|
||||
defaultValue: const ActivityStats(comments: 0),
|
||||
errorMessage: "Failed to statistics for album $albumId",
|
||||
);
|
||||
}
|
||||
@@ -43,7 +33,7 @@ class ActivityService with ErrorLoggerMixin {
|
||||
Future<bool> removeActivity(String id) async {
|
||||
return logError(
|
||||
() async {
|
||||
await _apiService.activitiesApi.deleteActivity(id);
|
||||
await _activityApiRepository.delete(id);
|
||||
return true;
|
||||
},
|
||||
defaultValue: false,
|
||||
@@ -58,22 +48,12 @@ class ActivityService with ErrorLoggerMixin {
|
||||
String? comment,
|
||||
}) async {
|
||||
return guardError(
|
||||
() 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();
|
||||
},
|
||||
() => _activityApiRepository.create(
|
||||
albumId,
|
||||
type,
|
||||
assetId: assetId,
|
||||
comment: comment,
|
||||
),
|
||||
errorMessage: "Failed to create $type for album $albumId",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,63 +6,61 @@ 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/repositories/user.repository.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/entity.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
|
||||
@@ -164,17 +162,11 @@ class AlbumService {
|
||||
bool changes = false;
|
||||
try {
|
||||
await _userService.refreshUsers();
|
||||
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
|
||||
.getAllAlbums(shared: isShared ? true : null);
|
||||
if (serverAlbums == null) {
|
||||
return false;
|
||||
}
|
||||
final List<Album> serverAlbums =
|
||||
await _albumApiRepository.getAll(shared: isShared ? true : null);
|
||||
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);
|
||||
@@ -188,30 +180,13 @@ class AlbumService {
|
||||
Iterable<Asset> assets, [
|
||||
Iterable<User> sharedUsers = const [],
|
||||
]) async {
|
||||
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;
|
||||
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);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -243,32 +218,21 @@ class AlbumService {
|
||||
Album album,
|
||||
) async {
|
||||
try {
|
||||
var response = await _apiService.albumsApi.addAssetsToAlbum(
|
||||
final result = await _albumApiRepository.addAssets(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
|
||||
assets.map((asset) => asset.remoteId!),
|
||||
);
|
||||
|
||||
if (response != null) {
|
||||
List<Asset> successAssets = [];
|
||||
List<String> duplicatedAssets = [];
|
||||
final List<Asset> addedAssets = result.added
|
||||
.map((id) => assets.firstWhere((asset) => asset.remoteId == id))
|
||||
.toList();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
await _updateAssets(album.id, add: addedAssets);
|
||||
|
||||
await _updateAssets(album.id, add: successAssets);
|
||||
|
||||
return AlbumAddAssetsResponse(
|
||||
alreadyInAlbum: duplicatedAssets,
|
||||
successfullyAdded: successAssets.length,
|
||||
);
|
||||
}
|
||||
return AlbumAddAssetsResponse(
|
||||
alreadyInAlbum: result.duplicates,
|
||||
successfullyAdded: addedAssets.length,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Error addAdditionalAssetToAlbum ${e.toString()}");
|
||||
}
|
||||
@@ -293,20 +257,11 @@ class AlbumService {
|
||||
Album album,
|
||||
) async {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
final updatedAlbum =
|
||||
await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds);
|
||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||
await _albumRepository.update(updatedAlbum);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error addAdditionalUserToAlbum ${e.toString()}");
|
||||
}
|
||||
@@ -315,15 +270,13 @@ class AlbumService {
|
||||
|
||||
Future<bool> setActivityEnabled(Album album, bool enabled) async {
|
||||
try {
|
||||
final result = await _apiService.albumsApi.updateAlbumInfo(
|
||||
final updatedAlbum = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
UpdateAlbumDto(isActivityEnabled: enabled),
|
||||
activityEnabled: enabled,
|
||||
);
|
||||
if (result != null) {
|
||||
album.activityEnabled = enabled;
|
||||
await _albumRepository.update(album);
|
||||
return true;
|
||||
}
|
||||
await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum);
|
||||
await _albumRepository.update(updatedAlbum);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error setActivityEnabled ${e.toString()}");
|
||||
}
|
||||
@@ -334,7 +287,7 @@ class AlbumService {
|
||||
try {
|
||||
final user = Store.get(StoreKey.currentUser);
|
||||
if (album.owner.value?.isarId == user.isarId) {
|
||||
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
|
||||
await _albumApiRepository.delete(album.remoteId!);
|
||||
}
|
||||
if (album.shared) {
|
||||
final foreignAssets =
|
||||
@@ -365,7 +318,7 @@ class AlbumService {
|
||||
|
||||
Future<bool> leaveAlbum(Album album) async {
|
||||
try {
|
||||
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
|
||||
await _albumApiRepository.removeUser(album.remoteId!, userId: "me");
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error leaveAlbum ${e.toString()}");
|
||||
@@ -378,21 +331,14 @@ class AlbumService {
|
||||
Iterable<Asset> assets,
|
||||
) async {
|
||||
try {
|
||||
final response = await _apiService.albumsApi.removeAssetFromAlbum(
|
||||
final result = await _albumApiRepository.removeAssets(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(
|
||||
ids: assets.map((asset) => asset.remoteId!).toList(),
|
||||
),
|
||||
assets.map((asset) => asset.remoteId!),
|
||||
);
|
||||
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;
|
||||
}
|
||||
final toRemove = result.removed
|
||||
.map((id) => assets.firstWhere((asset) => asset.remoteId == id));
|
||||
await _updateAssets(album.id, remove: toRemove.toList());
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error removeAssetFromAlbum ${e.toString()}");
|
||||
}
|
||||
@@ -404,9 +350,9 @@ class AlbumService {
|
||||
User user,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumsApi.removeUserFromAlbum(
|
||||
await _albumApiRepository.removeUser(
|
||||
album.remoteId!,
|
||||
user.id,
|
||||
userId: user.id,
|
||||
);
|
||||
|
||||
album.sharedUsers.remove(user);
|
||||
@@ -427,15 +373,12 @@ class AlbumService {
|
||||
String newAlbumTitle,
|
||||
) async {
|
||||
try {
|
||||
await _apiService.albumsApi.updateAlbumInfo(
|
||||
album = await _albumApiRepository.update(
|
||||
album.remoteId!,
|
||||
UpdateAlbumDto(
|
||||
albumName: newAlbumTitle,
|
||||
),
|
||||
name: newAlbumTitle,
|
||||
);
|
||||
album.name = newAlbumTitle;
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _albumRepository.update(album);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint("Error changeTitleAlbum ${e.toString()}");
|
||||
@@ -456,12 +399,8 @@ class AlbumService {
|
||||
for (final albumName in albumNames) {
|
||||
Album? album = await getAlbumByName(albumName, true);
|
||||
album ??= await createAlbum(albumName, []);
|
||||
|
||||
if (album != null && album.remoteId != null) {
|
||||
await _apiService.albumsApi.addAssetsToAlbum(
|
||||
album.remoteId!,
|
||||
BulkIdsDto(ids: assetIds),
|
||||
);
|
||||
await _albumApiRepository.addAssets(album.remoteId!, assetIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,9 +9,13 @@ 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';
|
||||
@@ -24,6 +28,8 @@ 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),
|
||||
@@ -34,6 +40,8 @@ final assetServiceProvider = Provider(
|
||||
);
|
||||
|
||||
class AssetService {
|
||||
final IAssetApiRepository _assetApiRepository;
|
||||
final IExifInfoRepository _exifInfoRepository;
|
||||
final ApiService _apiService;
|
||||
final SyncService _syncService;
|
||||
final UserService _userService;
|
||||
@@ -43,6 +51,8 @@ class AssetService {
|
||||
final Isar _db;
|
||||
|
||||
AssetService(
|
||||
this._assetApiRepository,
|
||||
this._exifInfoRepository,
|
||||
this._apiService,
|
||||
this._syncService,
|
||||
this._userService,
|
||||
@@ -342,4 +352,46 @@ 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 ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
@@ -13,12 +13,16 @@ 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';
|
||||
@@ -28,7 +32,6 @@ 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';
|
||||
@@ -360,26 +363,44 @@ 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();
|
||||
HashService hashService = HashService(db, this, albumMediaRepository);
|
||||
SyncService syncSerive = SyncService(db, hashService, albumMediaRepository);
|
||||
UserService userService =
|
||||
UserService(apiService, db, syncSerive, partnerService);
|
||||
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,
|
||||
);
|
||||
AlbumService albumService = AlbumService(
|
||||
apiService,
|
||||
userService,
|
||||
syncSerive,
|
||||
entityService,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
userRepository,
|
||||
backupAlbumRepository,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
);
|
||||
BackupService backupService = BackupService(
|
||||
apiService,
|
||||
|
||||
@@ -8,41 +8,46 @@ 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/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/exif_info.repository.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._db, this._fileMediaRepository);
|
||||
BackupVerificationService(
|
||||
this._fileMediaRepository,
|
||||
this._assetRepository,
|
||||
this._exifInfoRepository,
|
||||
);
|
||||
|
||||
/// 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 _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> onlyLocal = await _assetRepository.getAll(
|
||||
ownerId: owner,
|
||||
remote: false,
|
||||
limit: limit,
|
||||
);
|
||||
final List<Asset> localMatches = await _getMatches(
|
||||
_db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(),
|
||||
owner,
|
||||
remoteMatches,
|
||||
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> deleteCandidates = [], originals = [];
|
||||
@@ -52,7 +57,7 @@ class BackupVerificationService {
|
||||
localMatches,
|
||||
compare: (a, b) => a.fileName.compareTo(b.fileName),
|
||||
both: (a, b) async {
|
||||
a.exifInfo = await _db.exifInfos.get(a.id);
|
||||
a.exifInfo = await _exifInfoRepository.get(a.id);
|
||||
deleteCandidates.add(a);
|
||||
originals.add(b);
|
||||
return false;
|
||||
@@ -192,35 +197,6 @@ 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
|
||||
@@ -233,7 +209,8 @@ class BackupVerificationService {
|
||||
|
||||
final backupVerificationServiceProvider = Provider(
|
||||
(ref) => BackupVerificationService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(fileMediaRepositoryProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(exifInfoRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
52
mobile/lib/services/entity.service.dart
Normal file
52
mobile/lib/services/entity.service.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
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),
|
||||
),
|
||||
);
|
||||
@@ -4,20 +4,24 @@ 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._db, this._backgroundService, this._albumMediaRepository);
|
||||
final Isar _db;
|
||||
HashService(
|
||||
this._assetRepository,
|
||||
this._backgroundService,
|
||||
this._albumMediaRepository,
|
||||
);
|
||||
final IAssetRepository _assetRepository;
|
||||
final BackgroundService _backgroundService;
|
||||
final IAlbumMediaRepository _albumMediaRepository;
|
||||
final _log = Logger('HashService');
|
||||
@@ -55,7 +59,8 @@ class HashService {
|
||||
final ids = assets
|
||||
.map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
|
||||
.toList();
|
||||
final List<DeviceAsset?> hashes = await _lookupHashes(ids);
|
||||
final List<DeviceAsset?> hashes =
|
||||
await _assetRepository.getDeviceAssetsById(ids);
|
||||
final List<DeviceAsset> toAdd = [];
|
||||
final List<String> toHash = [];
|
||||
|
||||
@@ -65,7 +70,19 @@ class HashService {
|
||||
if (hashes[i] != null) {
|
||||
continue;
|
||||
}
|
||||
final file = await assets[i].local!.originFile;
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
if (file == null) {
|
||||
final fileName = assets[i].fileName;
|
||||
|
||||
@@ -94,12 +111,6 @@ 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(
|
||||
@@ -119,11 +130,7 @@ class HashService {
|
||||
final validHashes = anyNull
|
||||
? toAdd.where((e) => e.hash.length == 20).toList(growable: false)
|
||||
: toAdd;
|
||||
await _db.writeTxn(
|
||||
() => Platform.isAndroid
|
||||
? _db.androidDeviceAssets.putAll(validHashes.cast())
|
||||
: _db.iOSDeviceAssets.putAll(validHashes.cast()),
|
||||
);
|
||||
await _assetRepository.upsertDeviceAssets(validHashes);
|
||||
_log.fine("Hashed ${validHashes.length}/${toHash.length} assets");
|
||||
}
|
||||
|
||||
@@ -156,7 +163,7 @@ class HashService {
|
||||
|
||||
final hashServiceProvider = Provider(
|
||||
(ref) => HashService(
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
ref.watch(backgroundServiceProvider),
|
||||
ref.watch(albumMediaRepositoryProvider),
|
||||
),
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import 'package:easy_localization/easy_localization.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/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.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(dbProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -20,9 +19,9 @@ class MemoryService {
|
||||
final log = Logger("MemoryService");
|
||||
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final IAssetRepository _assetRepository;
|
||||
|
||||
MemoryService(this._apiService, this._db);
|
||||
MemoryService(this._apiService, this._assetRepository);
|
||||
|
||||
Future<List<Memory>?> getMemoryLane() async {
|
||||
try {
|
||||
@@ -39,7 +38,7 @@ class MemoryService {
|
||||
List<Memory> memories = [];
|
||||
for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) {
|
||||
final dbAssets =
|
||||
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
|
||||
await _assetRepository.getAllByRemoteId(assets.map((e) => e.id));
|
||||
if (dbAssets.isNotEmpty) {
|
||||
final String title = yearsAgo <= 1
|
||||
? 'memories_year_ago'.tr()
|
||||
|
||||
@@ -1,43 +1,33 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.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:isar/isar.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:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
final partnerServiceProvider = Provider(
|
||||
(ref) => PartnerService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(partnerApiRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class PartnerService {
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final IPartnerApiRepository _partnerApiRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final Logger _log = Logger("PartnerService");
|
||||
|
||||
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;
|
||||
}
|
||||
PartnerService(
|
||||
this._partnerApiRepository,
|
||||
this._userRepository,
|
||||
);
|
||||
|
||||
Future<bool> removePartner(User partner) async {
|
||||
try {
|
||||
await _apiService.partnersApi.removePartner(partner.id);
|
||||
await _partnerApiRepository.delete(partner.id);
|
||||
partner.isPartnerSharedBy = false;
|
||||
await _db.writeTxn(() => _db.users.put(partner));
|
||||
await _userRepository.update(partner);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to remove partner ${partner.id}", e);
|
||||
return false;
|
||||
@@ -47,12 +37,10 @@ class PartnerService {
|
||||
|
||||
Future<bool> addPartner(User partner) async {
|
||||
try {
|
||||
final dto = await _apiService.partnersApi.createPartner(partner.id);
|
||||
if (dto != null) {
|
||||
partner.isPartnerSharedBy = true;
|
||||
await _db.writeTxn(() => _db.users.put(partner));
|
||||
return true;
|
||||
}
|
||||
await _partnerApiRepository.create(partner.id);
|
||||
partner.isPartnerSharedBy = true;
|
||||
await _userRepository.update(partner);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.warning("Failed to add partner ${partner.id}", e);
|
||||
}
|
||||
@@ -61,13 +49,13 @@ class PartnerService {
|
||||
|
||||
Future<bool> updatePartner(User partner, {required bool inTimeline}) async {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
final dto = await _partnerApiRepository.update(
|
||||
partner.id,
|
||||
inTimeline: inTimeline,
|
||||
);
|
||||
partner.inTimeline = dto.inTimeline;
|
||||
await _userRepository.update(partner);
|
||||
return true;
|
||||
} catch (e) {
|
||||
_log.warning("Failed to update partner ${partner.id}", e);
|
||||
}
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
import 'package:immich_mobile/entities/asset.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: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: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.read(apiServiceProvider), ref.read(dbProvider));
|
||||
PersonService personService(PersonServiceRef ref) => PersonService(
|
||||
ref.watch(personApiRepositoryProvider),
|
||||
ref.watch(assetApiRepositoryProvider),
|
||||
ref.read(assetRepositoryProvider),
|
||||
);
|
||||
|
||||
class PersonService {
|
||||
final Logger _log = Logger("PersonService");
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final IPersonApiRepository _personApiRepository;
|
||||
final IAssetApiRepository _assetApiRepository;
|
||||
final IAssetRepository _assetRepository;
|
||||
|
||||
PersonService(this._apiService, this._db);
|
||||
PersonService(
|
||||
this._personApiRepository,
|
||||
this._assetApiRepository,
|
||||
this._assetRepository,
|
||||
);
|
||||
|
||||
Future<List<PersonResponseDto>> getAllPeople() async {
|
||||
Future<List<Person>> getAllPeople() async {
|
||||
try {
|
||||
final peopleResponseDto = await _apiService.peopleApi.getAllPeople();
|
||||
return peopleResponseDto?.people ?? [];
|
||||
return await _personApiRepository.getAll();
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error while fetching curated people", error, stack);
|
||||
return [];
|
||||
@@ -31,50 +39,19 @@ class PersonService {
|
||||
}
|
||||
|
||||
Future<List<Asset>> getPersonAssets(String id) async {
|
||||
List<Asset> result = [];
|
||||
var hasNext = true;
|
||||
var currentPage = 1;
|
||||
|
||||
try {
|
||||
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++;
|
||||
}
|
||||
final assets = await _assetApiRepository.search(personIds: [id]);
|
||||
return await _assetRepository
|
||||
.getAllByRemoteId(assets.map((a) => a.remoteId!));
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error while fetching person assets", error, stack);
|
||||
}
|
||||
|
||||
return result;
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<PersonResponseDto?> updateName(String id, String name) async {
|
||||
Future<Person?> updateName(String id, String name) async {
|
||||
try {
|
||||
return await _apiService.peopleApi.updatePerson(
|
||||
id,
|
||||
PersonUpdateDto(
|
||||
name: name,
|
||||
),
|
||||
);
|
||||
return await _personApiRepository.update(id, name: name);
|
||||
} catch (error, stack) {
|
||||
_log.severe("Error while updating person name", error, stack);
|
||||
}
|
||||
|
||||
2
mobile/lib/services/person.service.g.dart
generated
2
mobile/lib/services/person.service.g.dart
generated
@@ -6,7 +6,7 @@ part of 'person.service.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$personServiceHash() => r'54e6df4b8eea744f6de009f8315c9fe6230f6798';
|
||||
String _$personServiceHash() => r'32f28cb5a3de0553c17447e33a0efde7409a43ed';
|
||||
|
||||
/// See also [personService].
|
||||
@ProviderFor(personService)
|
||||
|
||||
@@ -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/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.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(dbProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class SearchService {
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final IAssetRepository _assetRepository;
|
||||
|
||||
final _log = Logger("SearchService");
|
||||
SearchService(this._apiService, this._db);
|
||||
SearchService(this._apiService, this._assetRepository);
|
||||
|
||||
Future<List<String>?> getSearchSuggestions(
|
||||
SearchSuggestionType type, {
|
||||
@@ -103,7 +103,7 @@ class SearchService {
|
||||
return null;
|
||||
}
|
||||
|
||||
return _db.assets
|
||||
return _assetRepository
|
||||
.getAllByRemoteId(response.assets.items.map((e) => e.id));
|
||||
} catch (error, stackTrace) {
|
||||
_log.severe("Failed to search for assets", error, stackTrace);
|
||||
|
||||
@@ -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/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.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._db);
|
||||
StackService(this._api, this._assetRepository);
|
||||
|
||||
final ApiService _api;
|
||||
final Isar _db;
|
||||
final IAssetRepository _assetRepository;
|
||||
|
||||
Future<StackResponseDto?> getStack(String stackId) async {
|
||||
try {
|
||||
@@ -61,10 +61,7 @@ class StackService {
|
||||
|
||||
removeAssets.add(asset);
|
||||
}
|
||||
|
||||
_db.writeTxn(() async {
|
||||
await _db.assets.putAll(removeAssets);
|
||||
});
|
||||
await _assetRepository.updateAll(removeAssets);
|
||||
} catch (error) {
|
||||
debugPrint("Error while deleting stack: $error");
|
||||
}
|
||||
@@ -74,6 +71,6 @@ class StackService {
|
||||
final stackServiceProvider = Provider(
|
||||
(ref) => StackService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(assetRepositoryProvider),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,9 +8,12 @@ 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';
|
||||
@@ -18,24 +21,33 @@ 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._albumMediaRepository);
|
||||
SyncService(
|
||||
this._db,
|
||||
this._hashService,
|
||||
this._entityService,
|
||||
this._albumMediaRepository,
|
||||
this._albumApiRepository,
|
||||
);
|
||||
|
||||
// public methods:
|
||||
|
||||
@@ -65,11 +77,10 @@ class SyncService {
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAlbumsToDb(
|
||||
List<AlbumResponseDto> remote, {
|
||||
List<Album> remote, {
|
||||
required bool isShared,
|
||||
required FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
}) =>
|
||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails));
|
||||
_lock.run(() => _syncRemoteAlbumsToDb(remote, isShared));
|
||||
|
||||
/// Syncs all device albums and their assets to the database
|
||||
/// Returns `true` if there were any changes
|
||||
@@ -289,11 +300,10 @@ class SyncService {
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
Future<bool> _syncRemoteAlbumsToDb(
|
||||
List<AlbumResponseDto> remote,
|
||||
List<Album> remoteAlbums,
|
||||
bool isShared,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
) async {
|
||||
remote.sortBy((e) => e.id);
|
||||
remoteAlbums.sortBy((e) => e.remoteId!);
|
||||
|
||||
final baseQuery = _db.albums.where().remoteIdIsNotNull().filter();
|
||||
final QueryBuilder<Album, Album, QAfterFilterCondition> query;
|
||||
@@ -310,14 +320,14 @@ class SyncService {
|
||||
final List<Asset> existing = [];
|
||||
|
||||
final bool changes = await diffSortedLists(
|
||||
remote,
|
||||
remoteAlbums,
|
||||
dbAlbums,
|
||||
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),
|
||||
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),
|
||||
);
|
||||
|
||||
if (isShared && toDelete.isNotEmpty) {
|
||||
@@ -338,26 +348,22 @@ class SyncService {
|
||||
/// syncing changes from local back to server)
|
||||
/// accumulates
|
||||
Future<bool> _syncRemoteAlbum(
|
||||
AlbumResponseDto dto,
|
||||
Album dto,
|
||||
Album album,
|
||||
List<Asset> deleteCandidates,
|
||||
List<Asset> existing,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
) async {
|
||||
if (!_hasAlbumResponseDtoChanged(dto, album)) {
|
||||
if (!_hasRemoteAlbumChanged(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 loadDetails(dto);
|
||||
if (dto.assetCount != dto.assets.length) {
|
||||
return false;
|
||||
}
|
||||
dto = await _albumApiRepository.get(dto.remoteId!);
|
||||
final assetsInDb =
|
||||
await album.assets.filter().sortByOwnerId().thenByChecksum().findAll();
|
||||
assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!");
|
||||
final List<Asset> assetsOnRemote = dto.getAssets();
|
||||
final List<Asset> assetsOnRemote = dto.remoteAssets.toList();
|
||||
assetsOnRemote.sort(Asset.compareByOwnerChecksum);
|
||||
final (toAdd, toUpdate, toUnlink) = _diffAssets(
|
||||
assetsOnRemote,
|
||||
@@ -368,15 +374,16 @@ class SyncService {
|
||||
// update shared users
|
||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
|
||||
final List<User> users = dto.remoteUsers.toList()
|
||||
..sort((a, b) => a.id.compareTo(b.id));
|
||||
final List<String> userIdsToAdd = [];
|
||||
final List<User> usersToUnlink = [];
|
||||
diffSortedListsSync(
|
||||
dto.albumUsers,
|
||||
users,
|
||||
sharedUsers,
|
||||
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
|
||||
compare: (User a, User b) => a.id.compareTo(b.id),
|
||||
both: (a, b) => false,
|
||||
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
|
||||
onlyFirst: (User a) => userIdsToAdd.add(a.id),
|
||||
onlySecond: (User a) => usersToUnlink.add(a),
|
||||
);
|
||||
|
||||
@@ -386,19 +393,19 @@ class SyncService {
|
||||
final assetsToLink = existingInDb + updated;
|
||||
final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast<User>();
|
||||
|
||||
album.name = dto.albumName;
|
||||
album.name = dto.name;
|
||||
album.shared = dto.shared;
|
||||
album.createdAt = dto.createdAt;
|
||||
album.modifiedAt = dto.updatedAt;
|
||||
album.modifiedAt = dto.modifiedAt;
|
||||
album.startDate = dto.startDate;
|
||||
album.endDate = dto.endDate;
|
||||
album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp;
|
||||
album.shared = dto.shared;
|
||||
album.activityEnabled = dto.isActivityEnabled;
|
||||
if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) {
|
||||
album.activityEnabled = dto.activityEnabled;
|
||||
if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) {
|
||||
album.thumbnail.value = await _db.assets
|
||||
.where()
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.remoteIdEqualTo(dto.remoteThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
@@ -434,27 +441,26 @@ class SyncService {
|
||||
/// (shared) assets to the database beforehand
|
||||
/// accumulates assets already existing in the database
|
||||
Future<void> _addAlbumFromServer(
|
||||
AlbumResponseDto dto,
|
||||
Album album,
|
||||
List<Asset> existing,
|
||||
FutureOr<AlbumResponseDto> Function(AlbumResponseDto) loadDetails,
|
||||
) async {
|
||||
if (dto.assetCount != dto.assets.length) {
|
||||
dto = await loadDetails(dto);
|
||||
if (album.remoteAssetCount != album.remoteAssets.length) {
|
||||
album = await _albumApiRepository.get(album.remoteId!);
|
||||
}
|
||||
if (dto.assetCount == dto.assets.length) {
|
||||
if (album.remoteAssetCount == album.remoteAssets.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(dto.getAssets());
|
||||
await _linkWithExistingFromDb(album.remoteAssets.toList());
|
||||
existing.addAll(existingInDb);
|
||||
await upsertAssetsWithExif(updated);
|
||||
|
||||
final Album a = await Album.remote(dto);
|
||||
await _db.writeTxn(() => _db.albums.store(a));
|
||||
await _entityService.fillAlbumWithDatabaseEntities(album);
|
||||
await _db.writeTxn(() => _db.albums.store(album));
|
||||
} else {
|
||||
_log.warning(
|
||||
"Failed to add album from server: assetCount ${dto.assetCount} != "
|
||||
"asset array length ${dto.assets.length} for album ${dto.albumName}");
|
||||
"Failed to add album from server: assetCount ${album.remoteAssetCount} != "
|
||||
"asset array length ${album.remoteAssets.length} for album ${album.name}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,17 +925,17 @@ class SyncService {
|
||||
}
|
||||
|
||||
/// returns `true` if the albums differ on the surface
|
||||
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) ||
|
||||
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) ||
|
||||
!isAtSameMomentAs(
|
||||
dto.lastModifiedAssetTimestamp,
|
||||
a.lastModifiedAssetTimestamp,
|
||||
remoteAlbum.lastModifiedAssetTimestamp,
|
||||
dbAlbum.lastModifiedAssetTimestamp,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,68 +1,48 @@
|
||||
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/services/partner.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.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/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(apiServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref.watch(partnerApiRepositoryProvider),
|
||||
ref.watch(userApiRepositoryProvider),
|
||||
ref.watch(userRepositoryProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(partnerServiceProvider),
|
||||
),
|
||||
);
|
||||
|
||||
class UserService {
|
||||
final ApiService _apiService;
|
||||
final Isar _db;
|
||||
final IPartnerApiRepository _partnerApiRepository;
|
||||
final IUserApiRepository _userApiRepository;
|
||||
final IUserRepository _userRepository;
|
||||
final SyncService _syncService;
|
||||
final PartnerService _partnerService;
|
||||
final Logger _log = Logger("UserService");
|
||||
|
||||
UserService(
|
||||
this._apiService,
|
||||
this._db,
|
||||
this._partnerApiRepository,
|
||||
this._userApiRepository,
|
||||
this._userRepository,
|
||||
this._syncService,
|
||||
this._partnerService,
|
||||
);
|
||||
|
||||
Future<List<User>?> _getAllUsers() async {
|
||||
try {
|
||||
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>> getUsers({bool self = false}) =>
|
||||
_userRepository.getAll(self: self);
|
||||
|
||||
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 {
|
||||
Future<({String profileImagePath})?> uploadProfileImage(XFile image) async {
|
||||
try {
|
||||
return await _apiService.usersApi.createProfileImage(
|
||||
MultipartFile.fromBytes(
|
||||
'file',
|
||||
await image.readAsBytes(),
|
||||
filename: image.name,
|
||||
),
|
||||
return await _userApiRepository.createProfileImage(
|
||||
name: image.name,
|
||||
data: await image.readAsBytes(),
|
||||
);
|
||||
} catch (e) {
|
||||
_log.warning("Failed to upload profile image", e);
|
||||
@@ -71,13 +51,19 @@ class UserService {
|
||||
}
|
||||
|
||||
Future<List<User>?> getUsersFromServer() async {
|
||||
final List<User>? users = await _getAllUsers();
|
||||
final List<User>? sharedBy =
|
||||
await _partnerService.getPartners(PartnerDirection.by);
|
||||
final List<User>? sharedWith =
|
||||
await _partnerService.getPartners(PartnerDirection.with_);
|
||||
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);
|
||||
|
||||
if (users == null || sharedBy == null || sharedWith == null) {
|
||||
if (users == null) {
|
||||
_log.warning("Failed to refresh users");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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 == Isar.autoIncrement;
|
||||
final isFromDto = asset.id == noDbId;
|
||||
return '${isFromDto ? asset.remoteId : asset.id}_fullStage';
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,19 @@ 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());
|
||||
|
||||
@@ -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 == Isar.autoIncrement;
|
||||
final isFromDto = asset.id == noDbId;
|
||||
|
||||
Widget buildSelectionIcon(Asset asset) {
|
||||
if (isSelected) {
|
||||
|
||||
@@ -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_description.service.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
@@ -29,14 +29,16 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
final focusNode = useFocusNode();
|
||||
final isFocus = useState(false);
|
||||
final isTextEmpty = useState(controller.text.isEmpty);
|
||||
final descriptionProvider = ref.watch(assetDescriptionServiceProvider);
|
||||
final assetService = ref.watch(assetServiceProvider);
|
||||
final owner = ref.watch(currentUserProvider);
|
||||
final hasError = useState(false);
|
||||
final assetWithExif = ref.watch(assetDetailProvider(asset));
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
controller.text = descriptionProvider.getAssetDescription(asset);
|
||||
assetService
|
||||
.getDescription(asset)
|
||||
.then((value) => controller.text = value);
|
||||
return null;
|
||||
},
|
||||
[assetWithExif.value],
|
||||
@@ -45,7 +47,7 @@ class DescriptionInput extends HookConsumerWidget {
|
||||
submitDescription(String description) async {
|
||||
hasError.value = false;
|
||||
try {
|
||||
await descriptionProvider.setDescription(
|
||||
await assetService.setDescription(
|
||||
asset,
|
||||
description,
|
||||
);
|
||||
|
||||
@@ -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<PersonResponseDto>) onSelect;
|
||||
final Set<PersonResponseDto>? filter;
|
||||
final Function(Set<Person>) onSelect;
|
||||
final Set<Person>? 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<PersonResponseDto>>(filter ?? {});
|
||||
final selectedPeople = useState<Set<Person>>(filter ?? {});
|
||||
|
||||
return people.widgetWhen(
|
||||
onData: (people) {
|
||||
|
||||
5
mobile/openapi/README.md
generated
5
mobile/openapi/README.md
generated
@@ -124,7 +124,6 @@ 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 |
|
||||
@@ -331,7 +330,6 @@ 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)
|
||||
@@ -339,11 +337,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)
|
||||
@@ -452,7 +450,6 @@ 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)
|
||||
|
||||
4
mobile/openapi/lib/api.dart
generated
4
mobile/openapi/lib/api.dart
generated
@@ -144,7 +144,6 @@ 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';
|
||||
@@ -152,11 +151,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';
|
||||
@@ -265,7 +264,6 @@ 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';
|
||||
|
||||
7
mobile/openapi/lib/api/assets_api.dart
generated
7
mobile/openapi/lib/api/assets_api.dart
generated
@@ -449,7 +449,10 @@ class AssetsApi {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Performs an HTTP 'GET /assets/random' operation and returns the [Response].
|
||||
/// This property was deprecated in v1.116.0
|
||||
///
|
||||
/// Note: This method returns the HTTP [Response].
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
@@ -482,6 +485,8 @@ class AssetsApi {
|
||||
);
|
||||
}
|
||||
|
||||
/// This property was deprecated in v1.116.0
|
||||
///
|
||||
/// Parameters:
|
||||
///
|
||||
/// * [num] count:
|
||||
|
||||
59
mobile/openapi/lib/api/deprecated_api.dart
generated
59
mobile/openapi/lib/api/deprecated_api.dart
generated
@@ -71,4 +71,63 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
56
mobile/openapi/lib/api/map_api.dart
generated
56
mobile/openapi/lib/api/map_api.dart
generated
@@ -105,62 +105,6 @@ 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:
|
||||
///
|
||||
|
||||
47
mobile/openapi/lib/api/search_api.dart
generated
47
mobile/openapi/lib/api/search_api.dart
generated
@@ -351,6 +351,53 @@ 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:
|
||||
///
|
||||
|
||||
8
mobile/openapi/lib/api_client.dart
generated
8
mobile/openapi/lib/api_client.dart
generated
@@ -343,8 +343,6 @@ 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':
|
||||
@@ -359,6 +357,8 @@ 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,8 +367,6 @@ 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':
|
||||
@@ -585,8 +583,6 @@ 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':
|
||||
|
||||
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@@ -100,9 +100,6 @@ 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();
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ 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>();
|
||||
|
||||
|
||||
@@ -78,6 +78,7 @@ 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>();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class ActivityStatisticsResponseDto {
|
||||
/// [value] if it's a [Map], null otherwise.
|
||||
// ignore: prefer_constructors_over_static_methods
|
||||
static ActivityStatisticsResponseDto? fromJson(dynamic value) {
|
||||
upgradeDto(value, "ActivityStatisticsResponseDto");
|
||||
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
Reference in New Issue
Block a user