diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 94567c1cd567e..196f8faf599ba 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -56,6 +56,10 @@ jobs:
run: dart format lib/ --set-exit-if-changed
working-directory: ./mobile
+ - name: Run dart custom_lint
+ run: dart run custom_lint
+ working-directory: ./mobile
+
# Enable after riverpod generator migration is completed
# - name: Run dart custom lint
# run: dart run custom_lint
diff --git a/cli/package-lock.json b/cli/package-lock.json
index 3e099b675a06b..6e148fbe09a13 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -825,9 +825,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.9.1",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
- "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+ "version": "9.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
+ "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -844,6 +844,19 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
+ "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1340,17 +1353,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
- "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
+ "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.3.0",
- "@typescript-eslint/type-utils": "8.3.0",
- "@typescript-eslint/utils": "8.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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",
@@ -1374,16 +1387,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
- "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
+ "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.3.0",
- "@typescript-eslint/types": "8.3.0",
- "@typescript-eslint/typescript-estree": "8.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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": {
@@ -1403,14 +1416,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
- "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
+ "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.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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"
@@ -1421,14 +1434,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
- "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
+ "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.3.0",
- "@typescript-eslint/utils": "8.3.0",
+ "@typescript-eslint/typescript-estree": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1446,9 +1459,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
- "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
+ "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": {
@@ -1460,14 +1473,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
- "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
+ "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.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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",
@@ -1489,16 +1502,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
- "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
+ "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.3.0",
- "@typescript-eslint/types": "8.3.0",
- "@typescript-eslint/typescript-estree": "8.3.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"
@@ -1512,13 +1525,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
- "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
+ "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.3.0",
+ "@typescript-eslint/types": "8.6.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1530,19 +1543,20 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
- "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
+ "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": {
"@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^0.2.3",
- "debug": "^4.3.5",
+ "debug": "^4.3.6",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.6",
"istanbul-reports": "^3.1.7",
- "magic-string": "^0.30.10",
+ "magic-string": "^0.30.11",
"magicast": "^0.3.4",
"std-env": "^3.7.0",
"test-exclude": "^7.0.1",
@@ -1552,17 +1566,24 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "2.0.5"
+ "@vitest/browser": "2.1.1",
+ "vitest": "2.1.1"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
}
},
"node_modules/@vitest/expect": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
- "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
+ "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.0.5",
- "@vitest/utils": "2.0.5",
+ "@vitest/spy": "2.1.1",
+ "@vitest/utils": "2.1.1",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1570,11 +1591,40 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/@vitest/pretty-format": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
- "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
+ "node_modules/@vitest/mocker": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
+ "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "^2.1.0-beta.1",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.11"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/spy": "2.1.1",
+ "msw": "^2.3.5",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz",
+ "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
@@ -1583,12 +1633,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
- "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
+ "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.0.5",
+ "@vitest/utils": "2.1.1",
"pathe": "^1.1.2"
},
"funding": {
@@ -1596,13 +1647,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
- "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
+ "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.0.5",
- "magic-string": "^0.30.10",
+ "@vitest/pretty-format": "2.1.1",
+ "magic-string": "^0.30.11",
"pathe": "^1.1.2"
},
"funding": {
@@ -1610,10 +1662,11 @@
}
},
"node_modules/@vitest/spy": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
- "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
+ "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": {
"tinyspy": "^3.0.0"
},
@@ -1622,13 +1675,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
- "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
+ "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.0.5",
- "estree-walker": "^3.0.3",
+ "@vitest/pretty-format": "2.1.1",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1710,6 +1763,7 @@
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -1800,6 +1854,7 @@
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -1838,6 +1893,7 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
@@ -1870,6 +1926,7 @@
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 16"
}
@@ -2014,6 +2071,7 @@
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -2112,9 +2170,9 @@
}
},
"node_modules/eslint": {
- "version": "9.9.1",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
- "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
+ "version": "9.10.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
+ "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2122,7 +2180,8 @@
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.9.1",
+ "@eslint/js": "9.10.0",
+ "@eslint/plugin-kit": "^0.1.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
@@ -2145,7 +2204,6 @@
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
@@ -2383,6 +2441,7 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -2396,29 +2455,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/execa": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
- "dev": true,
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -2582,22 +2618,11 @@
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "*"
}
},
- "node_modules/get-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
- "dev": true,
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
@@ -2688,15 +2713,6 @@
"integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
"dev": true
},
- "node_modules/human-signals": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
- "dev": true,
- "engines": {
- "node": ">=16.17.0"
- }
- },
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@@ -2818,18 +2834,6 @@
"node": ">=8"
}
},
- "node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "dev": true,
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3016,6 +3020,7 @@
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"get-func-name": "^2.0.1"
}
@@ -3061,12 +3066,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "dev": true
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3087,18 +3086,6 @@
"node": ">=8.6"
}
},
- "node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -3219,48 +3206,6 @@
"semver": "bin/semver"
}
},
- "node_modules/npm-run-path": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
- "dev": true,
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
- "dev": true,
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/optionator": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
@@ -3397,13 +3342,15 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 14.16"
}
@@ -3934,18 +3881,6 @@
"node": ">=8"
}
},
- "node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -4032,10 +3967,18 @@
"dev": true
},
"node_modules/tinybench": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
- "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
- "dev": true
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
+ "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/tinypool": {
"version": "1.0.0",
@@ -4056,10 +3999,11 @@
}
},
"node_modules/tinyspy": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
- "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=14.0.0"
}
@@ -4141,9 +4085,9 @@
}
},
"node_modules/typescript": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
- "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+ "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -4271,15 +4215,15 @@
}
},
"node_modules/vite-node": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
- "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
+ "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": {
"cac": "^6.7.14",
- "debug": "^4.3.5",
+ "debug": "^4.3.6",
"pathe": "^1.1.2",
- "tinyrainbow": "^1.2.0",
"vite": "^5.0.0"
},
"bin": {
@@ -4313,29 +4257,30 @@
}
},
"node_modules/vitest": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
- "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
+ "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": {
- "@ampproject/remapping": "^2.3.0",
- "@vitest/expect": "2.0.5",
- "@vitest/pretty-format": "^2.0.5",
- "@vitest/runner": "2.0.5",
- "@vitest/snapshot": "2.0.5",
- "@vitest/spy": "2.0.5",
- "@vitest/utils": "2.0.5",
+ "@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.5",
- "execa": "^8.0.1",
- "magic-string": "^0.30.10",
+ "debug": "^4.3.6",
+ "magic-string": "^0.30.11",
"pathe": "^1.1.2",
"std-env": "^3.7.0",
- "tinybench": "^2.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.0",
"tinypool": "^1.0.0",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
- "vite-node": "2.0.5",
+ "vite-node": "2.1.1",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -4350,8 +4295,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "2.0.5",
- "@vitest/ui": "2.0.5",
+ "@vitest/browser": "2.1.1",
+ "@vitest/ui": "2.1.1",
"happy-dom": "*",
"jsdom": "*"
},
@@ -4536,9 +4481,9 @@
}
},
"node_modules/yaml": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz",
- "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==",
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz",
+ "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==",
"dev": true,
"license": "ISC",
"bin": {
diff --git a/docker/hwaccel.transcoding.yml b/docker/hwaccel.transcoding.yml
index bd4e2a46b8b39..33fb7b3c06273 100644
--- a/docker/hwaccel.transcoding.yml
+++ b/docker/hwaccel.transcoding.yml
@@ -51,5 +51,4 @@ services:
volumes:
- /usr/lib/wsl:/usr/lib/wsl
environment:
- - LD_LIBRARY_PATH=/usr/lib/wsl/lib
- LIBVA_DRIVER_NAME=d3d12
diff --git a/docs/docs/administration/reverse-proxy.md b/docs/docs/administration/reverse-proxy.md
index 1d2488f1192d8..c40fecbdc4c23 100644
--- a/docs/docs/administration/reverse-proxy.md
+++ b/docs/docs/administration/reverse-proxy.md
@@ -64,3 +64,43 @@ Below is an example config for Apache2 site configuration.
ProxyPreserveHost On
```
+
+### 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`.
diff --git a/docs/docs/developer/architecture.mdx b/docs/docs/developer/architecture.mdx
index cf004a1119213..7b5debef4c0da 100644
--- a/docs/docs/developer/architecture.mdx
+++ b/docs/docs/developer/architecture.mdx
@@ -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:
+
+
+
+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
diff --git a/docs/docs/developer/img/immich_mobile_architecture.drawio b/docs/docs/developer/img/immich_mobile_architecture.drawio
new file mode 100644
index 0000000000000..548cda09383f3
--- /dev/null
+++ b/docs/docs/developer/img/immich_mobile_architecture.drawio
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/docs/developer/img/immich_mobile_architecture.svg b/docs/docs/developer/img/immich_mobile_architecture.svg
new file mode 100644
index 0000000000000..71f28235bf649
--- /dev/null
+++ b/docs/docs/developer/img/immich_mobile_architecture.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 05417ce1275a7..3b4e6c4f9546a 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -6068,9 +6068,10 @@
}
},
"node_modules/docusaurus-lunr-search": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.4.0.tgz",
- "integrity": "sha512-GfllnNXCLgTSPH9TAKWmbn8VMfwpdOAZ1xl3T2GgX8Pm26qSDLfrrdVwjguaLfMJfzciFL97RKrAJlgrFM48yw==",
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/docusaurus-lunr-search/-/docusaurus-lunr-search-3.5.0.tgz",
+ "integrity": "sha512-k3zN4jYMi/prWInJILGKOxE+BVcgYinwj9+gcECsYm52tS+4ZKzXQzbPnVJAEXmvKOfFMcDFvS3MSmm6cEaxIQ==",
+ "license": "MIT",
"dependencies": {
"autocomplete.js": "^0.37.0",
"clsx": "^1.2.1",
@@ -6097,14 +6098,16 @@
}
},
"node_modules/docusaurus-lunr-search/node_modules/@types/unist": {
- "version": "2.0.10",
- "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.10.tgz",
- "integrity": "sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA=="
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
+ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
+ "license": "MIT"
},
"node_modules/docusaurus-lunr-search/node_modules/bail": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
"integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
+ "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -6114,6 +6117,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -6122,6 +6126,7 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
"integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -6130,6 +6135,7 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz",
"integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==",
+ "license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
@@ -6139,6 +6145,7 @@
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz",
"integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==",
+ "license": "MIT",
"dependencies": {
"bail": "^1.0.0",
"extend": "^3.0.0",
@@ -6156,6 +6163,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
"integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
+ "license": "MIT",
"dependencies": {
"@types/unist": "^2.0.2"
},
@@ -6168,6 +6176,7 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz",
"integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==",
+ "license": "MIT",
"dependencies": {
"@types/unist": "^2.0.0",
"is-buffer": "^2.0.0",
@@ -6183,6 +6192,7 @@
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
"integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
+ "license": "MIT",
"dependencies": {
"@types/unist": "^2.0.0",
"unist-util-stringify-position": "^2.0.0"
@@ -16081,9 +16091,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.10",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
- "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
+ "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",
@@ -16443,9 +16453,9 @@
}
},
"node_modules/typescript": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
- "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+ "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index dbb95f176d7e7..6169a4bfa1725 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -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:
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index 8347bb12c6bb6..73c6ac61753e9 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -361,6 +361,7 @@
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"aix"
@@ -377,6 +378,7 @@
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
@@ -393,6 +395,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
@@ -409,6 +412,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
@@ -425,6 +429,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -441,6 +446,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -457,6 +463,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -473,6 +480,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"freebsd"
@@ -489,6 +497,7 @@
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -505,6 +514,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -521,6 +531,7 @@
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -537,6 +548,7 @@
"loong64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -553,6 +565,7 @@
"mips64el"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -569,6 +582,7 @@
"ppc64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -585,6 +599,7 @@
"riscv64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -601,6 +616,7 @@
"s390x"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -617,6 +633,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
@@ -633,6 +650,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"netbsd"
@@ -649,6 +667,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"openbsd"
@@ -665,6 +684,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"sunos"
@@ -681,6 +701,7 @@
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -697,6 +718,7 @@
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -713,6 +735,7 @@
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -799,9 +822,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.9.1",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
- "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
+ "version": "9.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.10.0.tgz",
+ "integrity": "sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==",
"dev": true,
"license": "MIT",
"engines": {
@@ -818,6 +841,19 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.1.0.tgz",
+ "integrity": "sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1113,13 +1149,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.46.1",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz",
- "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==",
+ "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.46.1"
+ "playwright": "1.47.1"
},
"bin": {
"playwright": "cli.js"
@@ -1129,208 +1165,224 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz",
- "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz",
- "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz",
- "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz",
- "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz",
- "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz",
- "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz",
- "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz",
- "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz",
- "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz",
- "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz",
- "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz",
- "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz",
- "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz",
- "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz",
- "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz",
- "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==",
+ "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"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1419,10 +1471,11 @@
}
},
"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
+ "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"
},
"node_modules/@types/express": {
"version": "4.17.21",
@@ -1543,9 +1596,9 @@
}
},
"node_modules/@types/pg": {
- "version": "8.11.8",
- "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.8.tgz",
- "integrity": "sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA==",
+ "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": {
@@ -1680,17 +1733,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
- "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
+ "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.3.0",
- "@typescript-eslint/type-utils": "8.3.0",
- "@typescript-eslint/utils": "8.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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",
@@ -1714,16 +1767,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
- "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
+ "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.3.0",
- "@typescript-eslint/types": "8.3.0",
- "@typescript-eslint/typescript-estree": "8.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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": {
@@ -1743,14 +1796,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
- "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
+ "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.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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"
@@ -1761,14 +1814,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
- "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
+ "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.3.0",
- "@typescript-eslint/utils": "8.3.0",
+ "@typescript-eslint/typescript-estree": "8.6.0",
+ "@typescript-eslint/utils": "8.6.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1786,9 +1839,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
- "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
+ "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": {
@@ -1800,14 +1853,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
- "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
+ "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.3.0",
- "@typescript-eslint/visitor-keys": "8.3.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",
@@ -1855,16 +1908,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
- "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
+ "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.3.0",
- "@typescript-eslint/types": "8.3.0",
- "@typescript-eslint/typescript-estree": "8.3.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"
@@ -1878,13 +1931,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
- "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
+ "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.3.0",
+ "@typescript-eslint/types": "8.6.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1896,19 +1949,20 @@
}
},
"node_modules/@vitest/coverage-v8": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz",
- "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==",
+ "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": {
"@ampproject/remapping": "^2.3.0",
"@bcoe/v8-coverage": "^0.2.3",
- "debug": "^4.3.5",
+ "debug": "^4.3.6",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-lib-source-maps": "^5.0.6",
"istanbul-reports": "^3.1.7",
- "magic-string": "^0.30.10",
+ "magic-string": "^0.30.11",
"magicast": "^0.3.4",
"std-env": "^3.7.0",
"test-exclude": "^7.0.1",
@@ -1918,17 +1972,24 @@
"url": "https://opencollective.com/vitest"
},
"peerDependencies": {
- "vitest": "2.0.5"
+ "@vitest/browser": "2.1.1",
+ "vitest": "2.1.1"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
}
},
"node_modules/@vitest/expect": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz",
- "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==",
+ "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.0.5",
- "@vitest/utils": "2.0.5",
+ "@vitest/spy": "2.1.1",
+ "@vitest/utils": "2.1.1",
"chai": "^5.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -1936,11 +1997,40 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/@vitest/pretty-format": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz",
- "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==",
+ "node_modules/@vitest/mocker": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz",
+ "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==",
"dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "^2.1.0-beta.1",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.11"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/spy": "2.1.1",
+ "msw": "^2.3.5",
+ "vite": "^5.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz",
+ "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
"tinyrainbow": "^1.2.0"
},
@@ -1949,12 +2039,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz",
- "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==",
+ "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.0.5",
+ "@vitest/utils": "2.1.1",
"pathe": "^1.1.2"
},
"funding": {
@@ -1962,13 +2053,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz",
- "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==",
+ "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.0.5",
- "magic-string": "^0.30.10",
+ "@vitest/pretty-format": "2.1.1",
+ "magic-string": "^0.30.11",
"pathe": "^1.1.2"
},
"funding": {
@@ -1976,10 +2068,11 @@
}
},
"node_modules/@vitest/spy": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz",
- "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==",
+ "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": {
"tinyspy": "^3.0.0"
},
@@ -1988,13 +2081,13 @@
}
},
"node_modules/@vitest/utils": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz",
- "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==",
+ "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.0.5",
- "estree-walker": "^3.0.3",
+ "@vitest/pretty-format": "2.1.1",
"loupe": "^3.1.1",
"tinyrainbow": "^1.2.0"
},
@@ -2129,6 +2222,7 @@
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -2235,6 +2329,7 @@
"resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=8"
}
@@ -2344,6 +2439,7 @@
"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
@@ -2391,6 +2487,7 @@
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 16"
}
@@ -2578,12 +2675,13 @@
}
},
"node_modules/debug": {
- "version": "4.3.5",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
- "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "ms": "2.1.2"
+ "ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
@@ -2626,6 +2724,7 @@
"resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -2812,6 +2911,7 @@
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
@@ -2872,9 +2972,9 @@
}
},
"node_modules/eslint": {
- "version": "9.9.1",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
- "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
+ "version": "9.10.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.10.0.tgz",
+ "integrity": "sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2882,7 +2982,8 @@
"@eslint-community/regexpp": "^4.11.0",
"@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.9.1",
+ "@eslint/js": "9.10.0",
+ "@eslint/plugin-kit": "^0.1.0",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
@@ -2905,7 +3006,6 @@
"is-glob": "^4.0.0",
"is-path-inside": "^3.0.3",
"json-stable-stringify-without-jsonify": "^1.0.1",
- "levn": "^0.4.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
@@ -3119,6 +3219,7 @@
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -3144,29 +3245,6 @@
"url": "https://github.com/eta-dev/eta?sponsor=1"
}
},
- "node_modules/execa": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
- "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
- "dev": true,
- "dependencies": {
- "cross-spawn": "^7.0.3",
- "get-stream": "^8.0.1",
- "human-signals": "^5.0.0",
- "is-stream": "^3.0.0",
- "merge-stream": "^2.0.0",
- "npm-run-path": "^5.1.0",
- "onetime": "^6.0.0",
- "signal-exit": "^4.1.0",
- "strip-final-newline": "^3.0.0"
- },
- "engines": {
- "node": ">=16.17"
- },
- "funding": {
- "url": "https://github.com/sindresorhus/execa?sponsor=1"
- }
- },
"node_modules/exiftool-vendored": {
"version": "28.2.1",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz",
@@ -3485,6 +3563,7 @@
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
"integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": "*"
}
@@ -3508,18 +3587,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/get-stream": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
- "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
- "dev": true,
- "engines": {
- "node": ">=16"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -3835,15 +3902,6 @@
"node": ">= 6"
}
},
- "node_modules/human-signals": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
- "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
- "dev": true,
- "engines": {
- "node": ">=16.17.0"
- }
- },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -4012,18 +4070,6 @@
"node": ">=8"
}
},
- "node_modules/is-stream": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
- "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
- "dev": true,
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4096,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": {
@@ -4306,6 +4352,7 @@
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz",
"integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"get-func-name": "^2.0.1"
}
@@ -4382,12 +4429,6 @@
"node": ">= 0.6"
}
},
- "node_modules/merge-stream": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
- "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
- "dev": true
- },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -4454,18 +4495,6 @@
"node": ">= 0.6"
}
},
- "node_modules/mimic-fn": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
- "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/mimic-response": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz",
@@ -4546,10 +4575,11 @@
}
},
"node_modules/ms": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
- "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
- "dev": true
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.7",
@@ -4562,6 +4592,7 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -4664,33 +4695,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/npm-run-path": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz",
- "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==",
- "dev": true,
- "dependencies": {
- "path-key": "^4.0.0"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/npm-run-path/node_modules/path-key": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz",
- "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/npmlog": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz",
@@ -4808,21 +4812,6 @@
"wrappy": "1"
}
},
- "node_modules/onetime": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz",
- "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==",
- "dev": true,
- "dependencies": {
- "mimic-fn": "^4.0.0"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/only": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz",
@@ -5001,36 +4990,38 @@
}
},
"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": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
"integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/pathval": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz",
"integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">= 14.16"
}
},
"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"
},
@@ -5057,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",
@@ -5081,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",
@@ -5121,10 +5115,11 @@
}
},
"node_modules/picocolors": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
- "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
- "dev": true
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
+ "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "dev": true,
+ "license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -5140,13 +5135,13 @@
}
},
"node_modules/playwright": {
- "version": "1.46.1",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz",
- "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==",
+ "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.46.1"
+ "playwright-core": "1.47.1"
},
"bin": {
"playwright": "cli.js"
@@ -5159,9 +5154,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.46.1",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz",
- "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==",
+ "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": {
@@ -5190,9 +5185,9 @@
}
},
"node_modules/postcss": {
- "version": "8.4.40",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
- "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
+ "version": "8.4.47",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
+ "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@@ -5208,10 +5203,11 @@
"url": "https://github.com/sponsors/ai"
}
],
+ "license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
- "picocolors": "^1.0.1",
- "source-map-js": "^1.2.0"
+ "picocolors": "^1.1.0",
+ "source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@@ -5610,10 +5606,11 @@
}
},
"node_modules/rollup": {
- "version": "4.19.1",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz",
- "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==",
+ "version": "4.22.4",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz",
+ "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
},
@@ -5625,25 +5622,32 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.19.1",
- "@rollup/rollup-android-arm64": "4.19.1",
- "@rollup/rollup-darwin-arm64": "4.19.1",
- "@rollup/rollup-darwin-x64": "4.19.1",
- "@rollup/rollup-linux-arm-gnueabihf": "4.19.1",
- "@rollup/rollup-linux-arm-musleabihf": "4.19.1",
- "@rollup/rollup-linux-arm64-gnu": "4.19.1",
- "@rollup/rollup-linux-arm64-musl": "4.19.1",
- "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1",
- "@rollup/rollup-linux-riscv64-gnu": "4.19.1",
- "@rollup/rollup-linux-s390x-gnu": "4.19.1",
- "@rollup/rollup-linux-x64-gnu": "4.19.1",
- "@rollup/rollup-linux-x64-musl": "4.19.1",
- "@rollup/rollup-win32-arm64-msvc": "4.19.1",
- "@rollup/rollup-win32-ia32-msvc": "4.19.1",
- "@rollup/rollup-win32-x64-msvc": "4.19.1",
+ "@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",
@@ -5820,10 +5824,11 @@
}
},
"node_modules/source-map-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
- "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
+ "license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
@@ -5953,18 +5958,6 @@
"node": ">=8"
}
},
- "node_modules/strip-final-newline": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz",
- "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==",
- "dev": true,
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
@@ -6155,10 +6148,18 @@
"dev": true
},
"node_modules/tinybench": {
- "version": "2.8.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz",
- "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==",
- "dev": true
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz",
+ "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/tinypool": {
"version": "1.0.0",
@@ -6179,10 +6180,11 @@
}
},
"node_modules/tinyspy": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz",
- "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz",
+ "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=14.0.0"
}
@@ -6277,9 +6279,9 @@
}
},
"node_modules/typescript": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
- "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
+ "integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -6385,14 +6387,15 @@
}
},
"node_modules/vite": {
- "version": "5.3.5",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz",
- "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==",
+ "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": {
"esbuild": "^0.21.3",
- "postcss": "^8.4.39",
- "rollup": "^4.13.0"
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
@@ -6411,6 +6414,7 @@
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
+ "sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
@@ -6428,6 +6432,9 @@
"sass": {
"optional": true
},
+ "sass-embedded": {
+ "optional": true
+ },
"stylus": {
"optional": true
},
@@ -6440,15 +6447,15 @@
}
},
"node_modules/vite-node": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz",
- "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==",
+ "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": {
"cac": "^6.7.14",
- "debug": "^4.3.5",
+ "debug": "^4.3.6",
"pathe": "^1.1.2",
- "tinyrainbow": "^1.2.0",
"vite": "^5.0.0"
},
"bin": {
@@ -6467,6 +6474,7 @@
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
@@ -6476,29 +6484,30 @@
}
},
"node_modules/vitest": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz",
- "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==",
+ "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": {
- "@ampproject/remapping": "^2.3.0",
- "@vitest/expect": "2.0.5",
- "@vitest/pretty-format": "^2.0.5",
- "@vitest/runner": "2.0.5",
- "@vitest/snapshot": "2.0.5",
- "@vitest/spy": "2.0.5",
- "@vitest/utils": "2.0.5",
+ "@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.5",
- "execa": "^8.0.1",
- "magic-string": "^0.30.10",
+ "debug": "^4.3.6",
+ "magic-string": "^0.30.11",
"pathe": "^1.1.2",
"std-env": "^3.7.0",
- "tinybench": "^2.8.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^0.3.0",
"tinypool": "^1.0.0",
"tinyrainbow": "^1.2.0",
"vite": "^5.0.0",
- "vite-node": "2.0.5",
+ "vite-node": "2.1.1",
"why-is-node-running": "^2.3.0"
},
"bin": {
@@ -6513,8 +6522,8 @@
"peerDependencies": {
"@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0",
- "@vitest/browser": "2.0.5",
- "@vitest/ui": "2.0.5",
+ "@vitest/browser": "2.1.1",
+ "@vitest/ui": "2.1.1",
"happy-dom": "*",
"jsdom": "*"
},
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
index 55032bd364bee..2576a2c5c945f 100644
--- a/e2e/playwright.config.ts
+++ b/e2e/playwright.config.ts
@@ -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,
},
});
diff --git a/e2e/src/api/specs/map.e2e-spec.ts b/e2e/src/api/specs/map.e2e-spec.ts
index 343a7c91d03e6..da5f779cffaad 100644
--- a/e2e/src/api/specs/map.e2e-spec.ts
+++ b/e2e/src/api/specs/map.e2e-spec.ts
@@ -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');
diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts
index 571d98cda744e..1ef8d8602ad24 100644
--- a/e2e/src/api/specs/server-info.e2e-spec.ts
+++ b/e2e/src/api/specs/server-info.e2e-spec.ts
@@ -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',
});
});
});
diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/api/specs/server.e2e-spec.ts
index b19e6d85c4ad0..3133460adaf2a 100644
--- a/e2e/src/api/specs/server.e2e-spec.ts
+++ b/e2e/src/api/specs/server.e2e-spec.ts
@@ -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',
});
});
});
diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts
index 0c28a72825beb..17bb568c61cef 100644
--- a/e2e/src/api/specs/trash.e2e-spec.ts
+++ b/e2e/src/api/specs/trash.e2e-spec.ts
@@ -34,8 +34,11 @@ describe('/trash', () => {
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
- const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
- expect(status).toBe(204);
+ const { status, body } = await request(app)
+ .post('/trash/empty')
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 1 });
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
@@ -51,8 +54,11 @@ describe('/trash', () => {
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
- const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
- expect(status).toBe(204);
+ const { status, body } = await request(app)
+ .post('/trash/empty')
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 1 });
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
@@ -76,8 +82,11 @@ describe('/trash', () => {
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
- const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
- expect(status).toBe(204);
+ const { status, body } = await request(app)
+ .post('/trash/restore')
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 1 });
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
@@ -99,11 +108,12 @@ describe('/trash', () => {
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true);
- const { status } = await request(app)
+ const { status, body } = await request(app)
.post('/trash/restore/assets')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [assetId] });
- expect(status).toBe(204);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 1 });
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false);
diff --git a/e2e/src/setup/docker-compose.ts b/e2e/src/setup/docker-compose.ts
index 3ae87417a2f94..49a702e776c85 100644
--- a/e2e/src/setup/docker-compose.ts
+++ b/e2e/src/setup/docker-compose.ts
@@ -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();
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index c67e5696975a9..3c9d4284ce49c 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -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;`);
}
diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock
index 659c4c2f4d4f7..84c9ae5d31151 100644
--- a/machine-learning/poetry.lock
+++ b/machine-learning/poetry.lock
@@ -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]]
diff --git a/mobile/.fvmrc b/mobile/.fvmrc
index 971587f297946..ee6eaac06fefc 100644
--- a/mobile/.fvmrc
+++ b/mobile/.fvmrc
@@ -1,3 +1,3 @@
{
- "flutter": "3.24.0"
+ "flutter": "3.24.3"
}
diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json
index aa43dab3fb008..ceaf9a6ab88dc 100644
--- a/mobile/.vscode/settings.json
+++ b/mobile/.vscode/settings.json
@@ -1,5 +1,5 @@
{
- "dart.flutterSdkPath": ".fvm/versions/3.24.0",
+ "dart.flutterSdkPath": ".fvm/versions/3.24.3",
"search.exclude": {
"**/.fvm": true
},
diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index fe5729fc60fb2..e996a54372b6e 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -36,8 +36,75 @@ analyzer:
- openapi/**
- lib/generated_plugin_registrant.dart
-plugins:
- - custom_lint
+ plugins:
+ - custom_lint
+
+custom_lint:
+ debug: true
+ rules:
+ - avoid_build_context_in_providers: false
+ - avoid_public_notifier_properties: false
+ - avoid_manual_providers_as_generated_provider_dependency: false
+ - unsupported_provider_value: false
+ - import_rule_photo_manager:
+ message: photo_manager must only be used in MediaRepositories
+ restrict: package:photo_manager
+ allowed:
+ # required / wanted
+ - 'lib/repositories/{album,asset,file}_media.repository.dart'
+ # acceptable exceptions for the time being
+ - lib/entities/asset.entity.dart # to provide local AssetEntity for now
+ - lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
+ # refactor to make the providers and services testable
+ - lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
+ - lib/services/{background,backup}.service.dart # uses only PMProgressHandler
+ - import_rule_isar:
+ message: isar must only be used in entities and repositories
+ restrict: package:isar
+ allowed:
+ # required / wanted
+ - lib/entities/*.entity.dart
+ - lib/repositories/{album,asset,backup,exif_info,user}.repository.dart
+ # acceptable exceptions for the time being
+ - integration_test/test_utils/general_helper.dart
+ - lib/main.dart
+ - lib/routing/router.dart
+ - lib/utils/{db,image_url_builder,migration,renderlist_generator}.dart
+ - test/**.dart
+ # refactor to make the providers and services testable
+ - lib/pages/common/{album_asset_selection,gallery_viewer}.page.dart
+ - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart
+ - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart
+ - lib/services/{asset,background,backup,hash,immich_logger,memory,partner,person,search,stack,sync,user}.service.dart
+ - lib/widgets/asset_grid/{asset_grid_data_structure,thumbnail_image}.dart
+
+ - import_rule_openapi:
+ message: openapi must only be used through ApiRepositories
+ restrict: package:openapi
+ allowed:
+ # requried / wanted
+ - lib/repositories/*_api.repository.dart
+ # 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/search/search_filter.model.dart
+ - lib/models/server_info/server_{config,disk_info,features,version}.model.dart
+ - lib/models/shared_link/shared_link.model.dart
+ - lib/pages/search/search_input.page.dart
+ - lib/providers/asset_viewer/asset_people.provider.dart
+ - lib/providers/authentication.provider.dart
+ - lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
+ - lib/providers/map/map_state.provider.dart
+ - lib/providers/search/{people,search,search_filter}.provider.dart
+ - lib/providers/websocket.provider.dart
+ - lib/routing/auth_guard.dart
+ - lib/services/{api,asset,backup,memory,oauth,partner,person,search,shared_link,stack,trash,user}.service.dart
+ - lib/widgets/album/album_thumbnail_listtile.dart
+ - lib/widgets/forms/login/login_form.dart
+ - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
dart_code_metrics:
metrics:
diff --git a/mobile/immich_lint/analysis_options.yaml b/mobile/immich_lint/analysis_options.yaml
new file mode 100644
index 0000000000000..572dd239d0976
--- /dev/null
+++ b/mobile/immich_lint/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:lints/recommended.yaml
diff --git a/mobile/immich_lint/lib/immich_mobile_immich_lint.dart b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
new file mode 100644
index 0000000000000..65f3fc18f30ea
--- /dev/null
+++ b/mobile/immich_lint/lib/immich_mobile_immich_lint.dart
@@ -0,0 +1,86 @@
+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 getLintRules(CustomLintConfigs configs) {
+ final List rules = [];
+ for (final entry in configs.rules.entries) {
+ if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
+ final code = makeCode(entry.key, entry.value);
+ final allowedPaths = getStrings(entry.value, "allowed");
+ final forbiddenPaths = getStrings(entry.value, "forbidden");
+ final restrict = getStrings(entry.value, "restrict");
+ rules.add(ImportRule(code, buildGlob(allowedPaths),
+ buildGlob(forbiddenPaths), restrict));
+ }
+ }
+ return rules;
+ }
+
+ static makeCode(String name, LintOptions options) => LintCode(
+ name: name,
+ problemMessage: options.json["message"] as String,
+ errorSeverity: ErrorSeverity.WARNING,
+ );
+
+ static List getStrings(LintOptions options, String field) {
+ final List 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 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 _restrict;
+ int _rootOffset = -1;
+
+ @override
+ void run(
+ CustomLintResolver resolver,
+ ErrorReporter reporter,
+ CustomLintContext context,
+ ) {
+ if (_rootOffset == -1) {
+ const project = "/immich/mobile/";
+ _rootOffset = resolver.path.indexOf(project) + project.length;
+ }
+ final path = resolver.path.substring(_rootOffset);
+
+ if ((_allowed != null && _allowed!.matches(path)) &&
+ (_forbidden == null || !_forbidden!.matches(path))) return;
+
+ context.registry.addImportDirective((node) {
+ final uri = node.uri.stringValue;
+ if (uri == null) return;
+ for (final restricted in _restrict) {
+ if (uri.startsWith(restricted) == true) {
+ reporter.atNode(node, code);
+ return;
+ }
+ }
+ });
+ }
+}
diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock
new file mode 100644
index 0000000000000..6b7a4c99c5ae3
--- /dev/null
+++ b/mobile/immich_lint/pubspec.lock
@@ -0,0 +1,370 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ _fe_analyzer_shared:
+ dependency: transitive
+ description:
+ name: _fe_analyzer_shared
+ sha256: "45cfa8471b89fb6643fe9bf51bd7931a76b8f5ec2d65de4fb176dba8d4f22c77"
+ url: "https://pub.dev"
+ source: hosted
+ version: "73.0.0"
+ _macros:
+ dependency: transitive
+ description: dart
+ source: sdk
+ version: "0.3.2"
+ analyzer:
+ dependency: "direct main"
+ description:
+ name: analyzer
+ sha256: "4959fec185fe70cce007c57e9ab6983101dbe593d2bf8bbfb4453aaec0cf470a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.8.0"
+ analyzer_plugin:
+ dependency: "direct main"
+ description:
+ name: analyzer_plugin
+ sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.3"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.5.0"
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.11.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.1"
+ checked_yaml:
+ dependency: transitive
+ description:
+ name: checked_yaml
+ sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.3"
+ ci:
+ dependency: transitive
+ description:
+ name: ci
+ sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.0"
+ cli_util:
+ dependency: transitive
+ description:
+ name: cli_util
+ sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.4.1"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.19.0"
+ convert:
+ dependency: transitive
+ description:
+ name: convert
+ sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
+ crypto:
+ dependency: transitive
+ description:
+ name: crypto
+ sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.5"
+ custom_lint:
+ dependency: transitive
+ description:
+ name: custom_lint
+ sha256: "6e1ec47427ca968f22bce734d00028ae7084361999b41673291138945c5baca0"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.7"
+ custom_lint_builder:
+ dependency: "direct main"
+ description:
+ name: custom_lint_builder
+ sha256: ba2f90fff4eff71d202d097eb14b14f87087eaaef742e956208c0eb9d3a40a21
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.7"
+ custom_lint_core:
+ dependency: transitive
+ description:
+ name: custom_lint_core
+ sha256: "4ddbbdaa774265de44c97054dcec058a83d9081d071785ece601e348c18c267d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.6.5"
+ dart_style:
+ dependency: transitive
+ description:
+ name: dart_style
+ sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.3.7"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ fixnum:
+ dependency: transitive
+ description:
+ name: fixnum
+ sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ freezed_annotation:
+ dependency: transitive
+ description:
+ name: freezed_annotation
+ sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.4.4"
+ glob:
+ dependency: "direct main"
+ description:
+ name: glob
+ sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ hotreloader:
+ dependency: transitive
+ description:
+ name: hotreloader
+ sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.0"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.0"
+ lints:
+ dependency: "direct dev"
+ description:
+ name: lints
+ sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.0.0"
+ logging:
+ dependency: transitive
+ description:
+ name: logging
+ sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.0"
+ macros:
+ dependency: transitive
+ description:
+ name: macros
+ sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.2-main.4"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.16+1"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.15.0"
+ package_config:
+ dependency: transitive
+ description:
+ name: package_config
+ sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.0"
+ pub_semver:
+ dependency: transitive
+ description:
+ name: pub_semver
+ sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ pubspec_parse:
+ dependency: transitive
+ description:
+ name: pubspec_parse
+ sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ rxdart:
+ dependency: transitive
+ description:
+ name: rxdart
+ sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.28.0"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.0"
+ sprintf:
+ dependency: transitive
+ description:
+ name: sprintf
+ sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.11.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ stream_transform:
+ dependency: transitive
+ description:
+ name: stream_transform
+ sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.0"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.1"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.3"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.2"
+ uuid:
+ dependency: transitive
+ description:
+ name: uuid
+ sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.5.0"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "14.2.5"
+ watcher:
+ dependency: transitive
+ description:
+ name: watcher
+ sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
+ yaml:
+ dependency: transitive
+ description:
+ name: yaml
+ sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.2"
+sdks:
+ dart: ">=3.4.0 <4.0.0"
diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml
new file mode 100644
index 0000000000000..78298f451e7ca
--- /dev/null
+++ b/mobile/immich_lint/pubspec.yaml
@@ -0,0 +1,14 @@
+name: immich_mobile_immich_lint
+publish_to: none
+
+environment:
+ sdk: '>=3.0.0 <4.0.0'
+
+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
diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart
index b20cec97c33a5..6331c4b9f06d0 100644
--- a/mobile/lib/entities/album.entity.dart
+++ b/mobile/lib/entities/album.entity.dart
@@ -1,11 +1,11 @@
import 'package:flutter/foundation.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/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';
-import 'package:photo_manager/photo_manager.dart';
part 'album.entity.g.dart';
@@ -25,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;
@@ -43,6 +44,17 @@ class Album {
final IsarLinks sharedUsers = IsarLinks();
final IsarLinks assets = IsarLinks();
+ // transient fields
+ @ignore
+ bool isAll = false;
+
+ @ignore
+ String? remoteThumbnailAssetId;
+
+ @ignore
+ int remoteAssetCount = 0;
+
+ // getters
@ignore
bool get isRemote => remoteId != null;
@@ -70,6 +82,21 @@ class Album {
return name.join(' ');
}
+ @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 get remoteUsers => sharedUsers.isEmpty
+ ? (sharedUsers as IsarLinksCommon).addedObjects
+ : sharedUsers;
+
+ @ignore
+ Iterable get remoteAssets =>
+ assets.isEmpty ? (assets as IsarLinksCommon).addedObjects : assets;
+
@override
bool operator ==(other) {
if (other is! Album) return false;
@@ -112,19 +139,6 @@ class Album {
sharedUsers.length.hashCode ^
assets.length.hashCode;
- static Album local(AssetPathEntity ape) {
- final Album a = Album(
- name: ape.name,
- createdAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
- modifiedAt: ape.lastModified?.toUtc() ?? DateTime.now().toUtc(),
- shared: false,
- activityEnabled: false,
- );
- a.owner.value = Store.get(StoreKey.currentUser);
- a.localId = ape.id;
- return a;
- }
-
static Future remote(AlbumResponseDto dto) async {
final Isar db = Isar.getInstance()!;
final Album a = Album(
@@ -138,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
@@ -173,11 +188,3 @@ extension AssetsHelper on IsarCollection {
return a;
}
}
-
-extension AlbumResponseDtoHelper on AlbumResponseDto {
- List getAssets() => assets.map(Asset.remote).toList();
-}
-
-extension AssetPathEntityHelper on AssetPathEntity {
- String get eTagKeyAssetCount => "device-album-$id-asset-count";
-}
diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart
index 97e10b3d200fe..df902ca995e9b 100644
--- a/mobile/lib/entities/asset.entity.dart
+++ b/mobile/lib/entities/asset.entity.dart
@@ -1,11 +1,10 @@
import 'dart:convert';
import 'package:immich_mobile/entities/exif_info.entity.dart';
-import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show AssetEntity;
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:path/path.dart' as p;
@@ -42,33 +41,6 @@ class Asset {
stackId = remote.stack?.id,
thumbhash = remote.thumbhash;
- Asset.local(AssetEntity local, List hash)
- : localId = local.id,
- checksum = base64.encode(hash),
- durationInSeconds = local.duration,
- type = AssetType.values[local.typeInt],
- height = local.height,
- width = local.width,
- fileName = local.title!,
- ownerId = Store.get(StoreKey.currentUser).isarId,
- fileModifiedAt = local.modifiedDateTime,
- updatedAt = local.modifiedDateTime,
- isFavorite = local.isFavorite,
- isArchived = false,
- isTrashed = false,
- isOffline = false,
- stackCount = 0,
- fileCreatedAt = local.createDateTime {
- if (fileCreatedAt.year == 1970) {
- fileCreatedAt = fileModifiedAt;
- }
- if (local.latitude != null) {
- exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
- }
- _local = local;
- assert(hash.length == 20, "invalid SHA1 hash");
- }
-
Asset({
this.id = Isar.autoIncrement,
required this.checksum,
@@ -115,6 +87,8 @@ class Asset {
return _local;
}
+ set local(AssetEntity? assetEntity) => _local = assetEntity;
+
Id id = Isar.autoIncrement;
/// stores the raw SHA1 bytes as a base64 String
@@ -210,6 +184,10 @@ class Asset {
@ignore
Duration get duration => Duration(seconds: durationInSeconds);
+ // ignore: invalid_annotation_target
+ @ignore
+ set byteHash(List hash) => checksum = base64.encode(hash);
+
@override
bool operator ==(other) {
if (other is! Asset) return false;
diff --git a/mobile/lib/interfaces/activity_api.interface.dart b/mobile/lib/interfaces/activity_api.interface.dart
new file mode 100644
index 0000000000000..99aef6f4d4668
--- /dev/null
+++ b/mobile/lib/interfaces/activity_api.interface.dart
@@ -0,0 +1,16 @@
+import 'package:immich_mobile/models/activities/activity.model.dart';
+
+abstract interface class IActivityApiRepository {
+ Future> getAll(
+ String albumId, {
+ String? assetId,
+ });
+ Future create(
+ String albumId,
+ ActivityType type, {
+ String? assetId,
+ String? comment,
+ });
+ Future delete(String id);
+ Future getStats(String albumId, {String? assetId});
+}
diff --git a/mobile/lib/interfaces/album_api.interface.dart b/mobile/lib/interfaces/album_api.interface.dart
new file mode 100644
index 0000000000000..33b589841fdc3
--- /dev/null
+++ b/mobile/lib/interfaces/album_api.interface.dart
@@ -0,0 +1,40 @@
+import 'package:immich_mobile/entities/album.entity.dart';
+
+abstract interface class IAlbumApiRepository {
+ Future get(String id);
+
+ Future> getAll({bool? shared});
+
+ Future create(
+ String name, {
+ required Iterable assetIds,
+ Iterable sharedUserIds = const [],
+ });
+
+ Future update(
+ String albumId, {
+ String? name,
+ String? thumbnailAssetId,
+ String? description,
+ bool? activityEnabled,
+ });
+
+ Future delete(String albumId);
+
+ Future<({List added, List duplicates})> addAssets(
+ String albumId,
+ Iterable assetIds,
+ );
+
+ Future<({List removed, List failed})> removeAssets(
+ String albumId,
+ Iterable assetIds,
+ );
+
+ Future addUsers(
+ String albumId,
+ Iterable userIds,
+ );
+
+ Future removeUser(String albumId, {required String userId});
+}
diff --git a/mobile/lib/interfaces/album_media.interface.dart b/mobile/lib/interfaces/album_media.interface.dart
new file mode 100644
index 0000000000000..fd5f3c8af1063
--- /dev/null
+++ b/mobile/lib/interfaces/album_media.interface.dart
@@ -0,0 +1,21 @@
+import 'package:immich_mobile/entities/album.entity.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAlbumMediaRepository {
+ Future> getAll();
+
+ Future> getAssetIds(String albumId);
+
+ Future getAssetCount(String albumId);
+
+ Future> getAssets(
+ String albumId, {
+ int start = 0,
+ int end = 0x7fffffffffffffff,
+ DateTime? modifiedFrom,
+ DateTime? modifiedUntil,
+ bool orderByModificationDate = false,
+ });
+
+ Future get(String id);
+}
diff --git a/mobile/lib/interfaces/asset.interface.dart b/mobile/lib/interfaces/asset.interface.dart
index 46425ba617cda..98f4c7687cdfe 100644
--- a/mobile/lib/interfaces/asset.interface.dart
+++ b/mobile/lib/interfaces/asset.interface.dart
@@ -3,6 +3,20 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IAssetRepository {
+ Future getByRemoteId(String id);
+ Future> getAllByRemoteId(Iterable ids);
Future> getByAlbum(Album album, {User? notOwnedBy});
Future deleteById(List ids);
+ Future> getAll({
+ required int ownerId,
+ bool? remote,
+ int limit = 100,
+ });
+
+ Future> getMatches({
+ required List assets,
+ required int ownerId,
+ bool? remote,
+ int limit = 100,
+ });
}
diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart
new file mode 100644
index 0000000000000..201c85cea7324
--- /dev/null
+++ b/mobile/lib/interfaces/asset_api.interface.dart
@@ -0,0 +1,16 @@
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAssetApiRepository {
+ // Future get(String id);
+
+ // Future> getAll();
+
+ // Future create(Asset asset);
+
+ Future update(
+ String id, {
+ String? description,
+ });
+
+ // Future delete(String id);
+}
diff --git a/mobile/lib/interfaces/asset_media.interface.dart b/mobile/lib/interfaces/asset_media.interface.dart
new file mode 100644
index 0000000000000..f89a238dd47ee
--- /dev/null
+++ b/mobile/lib/interfaces/asset_media.interface.dart
@@ -0,0 +1,7 @@
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IAssetMediaRepository {
+ Future> deleteAll(List ids);
+
+ Future get(String id);
+}
diff --git a/mobile/lib/interfaces/exif_info.interface.dart b/mobile/lib/interfaces/exif_info.interface.dart
new file mode 100644
index 0000000000000..fa8ca08f9d55f
--- /dev/null
+++ b/mobile/lib/interfaces/exif_info.interface.dart
@@ -0,0 +1,9 @@
+import 'package:immich_mobile/entities/exif_info.entity.dart';
+
+abstract interface class IExifInfoRepository {
+ Future get(int id);
+
+ Future update(ExifInfo exifInfo);
+
+ Future delete(int id);
+}
diff --git a/mobile/lib/interfaces/file_media.interface.dart b/mobile/lib/interfaces/file_media.interface.dart
new file mode 100644
index 0000000000000..c898183d79229
--- /dev/null
+++ b/mobile/lib/interfaces/file_media.interface.dart
@@ -0,0 +1,30 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:immich_mobile/entities/asset.entity.dart';
+
+abstract interface class IFileMediaRepository {
+ Future saveImage(
+ Uint8List data, {
+ required String title,
+ String? relativePath,
+ });
+
+ Future saveVideo(
+ File file, {
+ required String title,
+ String? relativePath,
+ });
+
+ Future saveLivePhoto({
+ required File image,
+ required File video,
+ required String title,
+ });
+
+ Future clearFileCache();
+
+ Future enableBackgroundAccess();
+
+ Future requestExtendedPermissions();
+}
diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart
index d9841a1187595..4e847ea0229e7 100644
--- a/mobile/lib/interfaces/user.interface.dart
+++ b/mobile/lib/interfaces/user.interface.dart
@@ -2,4 +2,5 @@ import 'package:immich_mobile/entities/user.entity.dart';
abstract interface class IUserRepository {
Future> getByIds(List ids);
+ Future get(String id);
}
diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart
index 6adb80dca9233..4702753f41cdd 100644
--- a/mobile/lib/models/activities/activity.model.dart
+++ b/mobile/lib/models/activities/activity.model.dart
@@ -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});
+}
diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart
index 0b428eea0fea6..59c57582ce430 100644
--- a/mobile/lib/models/backup/available_album.model.dart
+++ b/mobile/lib/models/backup/available_album.model.dart
@@ -1,45 +1,47 @@
import 'dart:typed_data';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/album.entity.dart';
class AvailableAlbum {
- final AssetPathEntity albumEntity;
+ final Album album;
+ final int assetCount;
final DateTime? lastBackup;
AvailableAlbum({
- required this.albumEntity,
+ required this.album,
+ required this.assetCount,
this.lastBackup,
});
AvailableAlbum copyWith({
- AssetPathEntity? albumEntity,
+ Album? album,
+ int? assetCount,
DateTime? lastBackup,
Uint8List? thumbnailData,
}) {
return AvailableAlbum(
- albumEntity: albumEntity ?? this.albumEntity,
+ album: album ?? this.album,
+ assetCount: assetCount ?? this.assetCount,
lastBackup: lastBackup ?? this.lastBackup,
);
}
- String get name => albumEntity.name;
+ String get name => album.name;
- Future get assetCount => albumEntity.assetCountAsync;
+ String get id => album.localId!;
- String get id => albumEntity.id;
-
- bool get isAll => albumEntity.isAll;
+ bool get isAll => album.isAll;
@override
String toString() =>
- 'AvailableAlbum(albumEntity: $albumEntity, lastBackup: $lastBackup)';
+ 'AvailableAlbum(albumEntity: $album, lastBackup: $lastBackup)';
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
- return other is AvailableAlbum && other.albumEntity == albumEntity;
+ return other is AvailableAlbum && other.album == album;
}
@override
- int get hashCode => albumEntity.hashCode;
+ int get hashCode => album.hashCode;
}
diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart
index 5ef15167455df..01c257dc052c0 100644
--- a/mobile/lib/models/backup/backup_candidate.model.dart
+++ b/mobile/lib/models/backup/backup_candidate.model.dart
@@ -1,9 +1,9 @@
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
class BackupCandidate {
BackupCandidate({required this.asset, required this.albumNames});
- AssetEntity asset;
+ Asset asset;
List albumNames;
@override
diff --git a/mobile/lib/models/backup/error_upload_asset.model.dart b/mobile/lib/models/backup/error_upload_asset.model.dart
index b63592eda86f3..38f241e748566 100644
--- a/mobile/lib/models/backup/error_upload_asset.model.dart
+++ b/mobile/lib/models/backup/error_upload_asset.model.dart
@@ -1,11 +1,11 @@
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
class ErrorUploadAsset {
final String id;
final DateTime fileCreatedAt;
final String fileName;
final String fileType;
- final AssetEntity asset;
+ final Asset asset;
final String errorMessage;
const ErrorUploadAsset({
@@ -22,7 +22,7 @@ class ErrorUploadAsset {
DateTime? fileCreatedAt,
String? fileName,
String? fileType,
- AssetEntity? asset,
+ Asset? asset,
String? errorMessage,
}) {
return ErrorUploadAsset(
diff --git a/mobile/lib/models/server_info/server_config.model.dart b/mobile/lib/models/server_info/server_config.model.dart
index 8936939135d26..f07ffde522f14 100644
--- a/mobile/lib/models/server_info/server_config.model.dart
+++ b/mobile/lib/models/server_info/server_config.model.dart
@@ -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) {
diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart
index 5cb5d418a024a..b9fed41305dcc 100644
--- a/mobile/lib/pages/backup/album_preview.page.dart
+++ b/mobile/lib/pages/backup/album_preview.page.dart
@@ -1,28 +1,27 @@
-import 'dart:typed_data';
-
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
-import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
@RoutePage()
class AlbumPreviewPage extends HookConsumerWidget {
- final AssetPathEntity album;
+ final Album album;
const AlbumPreviewPage({super.key, required this.album});
@override
Widget build(BuildContext context, WidgetRef ref) {
- final assets = useState>([]);
+ final assets = useState>([]);
getAssetsInAlbum() async {
- assets.value = await album.getAssetListRange(
- start: 0,
- end: await album.assetCountAsync,
- );
+ assets.value = await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssets(album.localId!);
}
useEffect(
@@ -68,30 +67,10 @@ class AlbumPreviewPage extends HookConsumerWidget {
),
itemCount: assets.value.length,
itemBuilder: (context, index) {
- Future thumbData =
- assets.value[index].thumbnailDataWithSize(
- const ThumbnailSize(200, 200),
- quality: 50,
- );
-
- return FutureBuilder(
- future: thumbData,
- builder: ((context, snapshot) {
- if (snapshot.hasData && snapshot.data != null) {
- return Image.memory(
- snapshot.data!,
- width: 100,
- height: 100,
- fit: BoxFit.cover,
- );
- }
-
- return const SizedBox(
- width: 100,
- height: 100,
- child: ImmichLoadingIndicator(),
- );
- }),
+ return ImmichThumbnail(
+ asset: assets.value[index],
+ width: 100,
+ height: 100,
);
},
),
diff --git a/mobile/lib/pages/backup/failed_backup_status.page.dart b/mobile/lib/pages/backup/failed_backup_status.page.dart
index 1c6d3a7aadef7..551555d75eaa3 100644
--- a/mobile/lib/pages/backup/failed_backup_status.page.dart
+++ b/mobile/lib/pages/backup/failed_backup_status.page.dart
@@ -3,9 +3,8 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
import 'package:intl/intl.dart';
-import 'package:photo_manager/photo_manager.dart';
-import 'package:photo_manager_image_provider/photo_manager_image_provider.dart';
@RoutePage()
class FailedBackupStatusPage extends HookConsumerWidget {
@@ -70,11 +69,10 @@ class FailedBackupStatusPage extends HookConsumerWidget {
clipBehavior: Clip.hardEdge,
child: Image(
fit: BoxFit.cover,
- image: AssetEntityImageProvider(
- errorAsset.asset,
- isOriginal: false,
- thumbnailSize: const ThumbnailSize.square(512),
- thumbnailFormat: ThumbnailFormat.jpeg,
+ image: ImmichLocalThumbnailProvider(
+ asset: errorAsset.asset,
+ height: 512,
+ width: 512,
),
),
),
diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart
index c81e84877b208..5c0c185dbce0a 100644
--- a/mobile/lib/pages/editing/edit.page.dart
+++ b/mobile/lib/pages/editing/edit.page.dart
@@ -8,11 +8,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/routing/router.dart';
-import 'package:photo_manager/photo_manager.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:path/path.dart' as p;
@@ -67,10 +67,10 @@ class EditImagePage extends ConsumerWidget {
) async {
try {
final Uint8List imageData = await _imageToUint8List(image);
- await PhotoManager.editor.saveImage(
- imageData,
- title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
- );
+ await ref.read(fileMediaRepositoryProvider).saveImage(
+ imageData,
+ title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
+ );
await ref.read(albumProvider.notifier).getDeviceAlbums();
Navigator.of(context).popUntil((route) => route.isFirst);
ImmichToast.show(
diff --git a/mobile/lib/pages/library/favorite.page.dart b/mobile/lib/pages/library/favorite.page.dart
index 7462dc8f21519..cc422f88c7fbf 100644
--- a/mobile/lib/pages/library/favorite.page.dart
+++ b/mobile/lib/pages/library/favorite.page.dart
@@ -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';
diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart
index d226ea55a36da..3be7e9b3e5374 100644
--- a/mobile/lib/pages/search/map/map.page.dart
+++ b/mobile/lib/pages/search/map/map.page.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(
diff --git a/mobile/lib/providers/activity_service.provider.dart b/mobile/lib/providers/activity_service.provider.dart
index dcfaac883fd7c..6bd139c56504e 100644
--- a/mobile/lib/providers/activity_service.provider.dart
+++ b/mobile/lib/providers/activity_service.provider.dart
@@ -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));
diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart
index afb43e8cba3d3..b1d2b4b9871f4 100644
--- a/mobile/lib/providers/activity_statistics.provider.dart
+++ b/mobile/lib/providers/activity_statistics.provider.dart
@@ -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;
}
diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart
index 3c1a5ecc0119b..a2c3987aa8165 100644
--- a/mobile/lib/providers/asset.provider.dart
+++ b/mobile/lib/providers/asset.provider.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/memory.provider.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@@ -15,7 +16,6 @@ import 'package:immich_mobile/utils/db.dart';
import 'package:immich_mobile/utils/renderlist_generator.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
-import 'package:photo_manager/photo_manager.dart';
class AssetNotifier extends StateNotifier {
final AssetService _assetService;
@@ -257,7 +257,7 @@ class AssetNotifier extends StateNotifier {
// Delete asset from device
if (local.isNotEmpty) {
try {
- return await PhotoManager.editor.deleteWithIds(local);
+ return await _ref.read(assetMediaRepositoryProvider).deleteAll(local);
} catch (e, stack) {
log.severe("Failed to delete asset from device", e, stack);
}
diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart
index 02f1f07904f97..0885f35f77998 100644
--- a/mobile/lib/providers/backup/backup.provider.dart
+++ b/mobile/lib/providers/backup/backup.provider.dart
@@ -5,6 +5,9 @@ import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.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/file_media.interface.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
@@ -13,6 +16,8 @@ import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/models/authentication/authentication_state.model.dart';
@@ -28,7 +33,7 @@ import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
class BackupNotifier extends StateNotifier {
BackupNotifier(
@@ -38,6 +43,8 @@ class BackupNotifier extends StateNotifier {
this._backgroundService,
this._galleryPermissionNotifier,
this._db,
+ this._albumMediaRepository,
+ this._fileMediaRepository,
this.ref,
) : super(
BackUpState(
@@ -86,6 +93,8 @@ class BackupNotifier extends StateNotifier {
final BackgroundService _backgroundService;
final GalleryPermissionNotifier _galleryPermissionNotifier;
final Isar _db;
+ final IAlbumMediaRepository _albumMediaRepository;
+ final IFileMediaRepository _fileMediaRepository;
final Ref ref;
///
@@ -224,22 +233,24 @@ class BackupNotifier extends StateNotifier {
Stopwatch stopwatch = Stopwatch()..start();
// Get all albums on the device
List availableAlbums = [];
- List albums = await PhotoManager.getAssetPathList(
- hasAll: true,
- type: RequestType.common,
- );
+ List albums = await _albumMediaRepository.getAll();
// Map of id -> album for quick album lookup later on.
- Map albumMap = {};
+ Map albumMap = {};
log.info('Found ${albums.length} local albums');
- for (AssetPathEntity album in albums) {
- AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
+ for (Album album in albums) {
+ AvailableAlbum availableAlbum = AvailableAlbum(
+ album: album,
+ assetCount: await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssetCount(album.localId!),
+ );
availableAlbums.add(availableAlbum);
- albumMap[album.id] = album;
+ albumMap[album.localId!] = album;
}
state = state.copyWith(availableAlbums: availableAlbums);
@@ -248,14 +259,18 @@ class BackupNotifier extends StateNotifier {
final List selectedBackupAlbums =
await _backupService.selectedAlbumsQuery().findAll();
- // Generate AssetPathEntity from id to add to local state
final Set selectedAlbums = {};
for (final BackupAlbum ba in selectedBackupAlbums) {
final albumAsset = albumMap[ba.id];
if (albumAsset != null) {
selectedAlbums.add(
- AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+ AvailableAlbum(
+ album: albumAsset,
+ assetCount:
+ await _albumMediaRepository.getAssetCount(albumAsset.localId!),
+ lastBackup: ba.lastBackup,
+ ),
);
} else {
log.severe('Selected album not found');
@@ -268,7 +283,13 @@ class BackupNotifier extends StateNotifier {
if (albumAsset != null) {
excludedAlbums.add(
- AvailableAlbum(albumEntity: albumAsset, lastBackup: ba.lastBackup),
+ AvailableAlbum(
+ album: albumAsset,
+ assetCount: await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssetCount(albumAsset.localId!),
+ lastBackup: ba.lastBackup,
+ ),
);
} else {
log.severe('Excluded album not found');
@@ -292,28 +313,32 @@ class BackupNotifier extends StateNotifier {
/// Those assets are unique and are used as the total assets
///
Future _updateBackupAssetCount() async {
+ // Save to persistent storage
+ await _updatePersistentAlbumsSelection();
+
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
final Set assetsFromSelectedAlbums = {};
final Set assetsFromExcludedAlbums = {};
for (final album in state.selectedBackupAlbums) {
- final assetCount = await album.albumEntity.assetCountAsync;
+ final assetCount = await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssetCount(album.album.localId!);
if (assetCount == 0) {
continue;
}
- final assets = await album.albumEntity.getAssetListRange(
- start: 0,
- end: assetCount,
- );
+ final assets = await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssets(album.album.localId!);
// Add album's name to the asset info
for (final asset in assets) {
List albumNames = [album.name];
final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
- (a) => a.asset.id == asset.id,
+ (a) => a.asset.localId == asset.localId,
);
if (existingAsset != null) {
@@ -331,16 +356,17 @@ class BackupNotifier extends StateNotifier {
}
for (final album in state.excludedBackupAlbums) {
- final assetCount = await album.albumEntity.assetCountAsync;
+ final assetCount = await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssetCount(album.album.localId!);
if (assetCount == 0) {
continue;
}
- final assets = await album.albumEntity.getAssetListRange(
- start: 0,
- end: assetCount,
- );
+ final assets = await ref
+ .read(albumMediaRepositoryProvider)
+ .getAssets(album.album.localId!);
for (final asset in assets) {
assetsFromExcludedAlbums.add(
@@ -360,14 +386,14 @@ class BackupNotifier extends StateNotifier {
// Find asset that were backup from selected albums
final Set selectedAlbumsBackupAssets =
- Set.from(allUniqueAssets.map((e) => e.asset.id));
+ Set.from(allUniqueAssets.map((e) => e.asset.localId));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
// Remove duplicated asset from all unique assets
allUniqueAssets.removeWhere(
- (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+ (candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
);
if (allUniqueAssets.isEmpty) {
@@ -385,9 +411,6 @@ class BackupNotifier extends StateNotifier {
selectedAlbumsBackupAssetsIds: selectedAlbumsBackupAssets,
);
}
-
- // Save to persistent storage
- await _updatePersistentAlbumsSelection();
}
/// Get all necessary information for calculating the available albums,
@@ -454,7 +477,7 @@ class BackupNotifier extends StateNotifier {
final hasPermission = _galleryPermissionNotifier.hasPermission;
if (hasPermission) {
- await PhotoManager.clearFileCache();
+ await _fileMediaRepository.clearFileCache();
if (state.allUniqueAssets.isEmpty) {
log.info("No Asset On Device - Abort Backup Process");
@@ -465,7 +488,7 @@ class BackupNotifier extends StateNotifier {
Set assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up
for (final assetId in state.allAssetsInDatabase) {
- assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
+ assetsWillBeBackup.removeWhere((e) => e.asset.localId == assetId);
}
if (assetsWillBeBackup.isEmpty) {
@@ -531,7 +554,8 @@ class BackupNotifier extends StateNotifier {
state = state.copyWith(
allUniqueAssets: state.allUniqueAssets
.where(
- (candidate) => candidate.asset.id != result.candidate.asset.id,
+ (candidate) =>
+ candidate.asset.localId != result.candidate.asset.localId,
)
.toSet(),
);
@@ -539,11 +563,11 @@ class BackupNotifier extends StateNotifier {
state = state.copyWith(
selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds,
- result.candidate.asset.id,
+ result.candidate.asset.localId!,
},
allAssetsInDatabase: [
...state.allAssetsInDatabase,
- result.candidate.asset.id,
+ result.candidate.asset.localId!,
],
);
}
@@ -552,7 +576,7 @@ class BackupNotifier extends StateNotifier {
state.selectedAlbumsBackupAssetsIds.length ==
0) {
final latestAssetBackup = state.allUniqueAssets
- .map((candidate) => candidate.asset.modifiedDateTime)
+ .map((candidate) => candidate.asset.fileModifiedAt)
.reduce(
(v, e) => e.isAfter(v) ? e : v,
);
@@ -741,6 +765,8 @@ final backupProvider =
ref.watch(backgroundServiceProvider),
ref.watch(galleryPermissionNotifier.notifier),
ref.watch(dbProvider),
+ ref.watch(albumMediaRepositoryProvider),
+ ref.watch(fileMediaRepositoryProvider),
ref,
);
});
diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart
index a76b56fea7f8a..0cf159bfddb1c 100644
--- a/mobile/lib/providers/backup/manual_upload.provider.dart
+++ b/mobile/lib/providers/backup/manual_upload.provider.dart
@@ -8,6 +8,7 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/file_media.repository.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -27,7 +28,7 @@ import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
final manualUploadProvider =
StateNotifierProvider((ref) {
@@ -193,17 +194,10 @@ class ManualUploadNotifier extends StateNotifier {
_backupProvider.updateBackupProgress(BackUpProgressEnum.manualInProgress);
if (ref.read(galleryPermissionNotifier.notifier).hasPermission) {
- await PhotoManager.clearFileCache();
+ await ref.read(fileMediaRepositoryProvider).clearFileCache();
- // We do not have 1:1 mapping of all AssetEntity fields to Asset. This results in cases
- // where platform specific fields such as `subtype` used to detect platform specific assets such as
- // LivePhoto in iOS is lost when we directly fetch the local asset from Asset using Asset.local
- List allAssetsFromDevice = await Future.wait(
- allManualUploads
- // Filter local only assets
- .where((e) => e.isLocal && !e.isRemote)
- .map((e) => e.local!.obtainForNewProperties()),
- );
+ final allAssetsFromDevice =
+ allManualUploads.where((e) => e.isLocal && !e.isRemote).toList();
if (allAssetsFromDevice.length != allManualUploads.length) {
_log.warning(
@@ -221,11 +215,17 @@ class ManualUploadNotifier extends StateNotifier {
await _backupService.buildUploadCandidates(
selectedBackupAlbums,
excludedBackupAlbums,
+ useTimeFilter: false,
);
- // Extrack candidate from allAssetsFromDevice.nonNulls
- final uploadAssets = candidates
- .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset));
+ // Extrack candidate from allAssetsFromDevice
+ final uploadAssets = candidates.where(
+ (candidate) =>
+ allAssetsFromDevice.firstWhereOrNull(
+ (asset) => asset.localId == candidate.asset.localId,
+ ) !=
+ null,
+ );
if (uploadAssets.isEmpty) {
debugPrint("[_startUpload] No Assets to upload - Abort Process");
diff --git a/mobile/lib/providers/favorite_provider.dart b/mobile/lib/providers/favorite.provider.dart
similarity index 100%
rename from mobile/lib/providers/favorite_provider.dart
rename to mobile/lib/providers/favorite.provider.dart
diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart
index dc1b8a98456aa..c1bafa6c5a0ba 100644
--- a/mobile/lib/providers/image/immich_local_image_provider.dart
+++ b/mobile/lib/providers/image/immich_local_image_provider.dart
@@ -9,7 +9,7 @@ import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
/// The local image provider for an asset
class ImmichLocalImageProvider extends ImageProvider {
diff --git a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
index 28e78ae762197..69cdb105c0f72 100644
--- a/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
+++ b/mobile/lib/providers/image/immich_local_thumbnail_provider.dart
@@ -6,7 +6,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show ThumbnailSize;
/// The local image provider for an asset
/// Only viable
diff --git a/mobile/lib/providers/map/map_state.provider.dart b/mobile/lib/providers/map/map_state.provider.dart
index 6d1630bba2e18..189a23cd0aad1 100644
--- a/mobile/lib/providers/map/map_state.provider.dart
+++ b/mobile/lib/providers/map/map_state.provider.dart
@@ -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(AppSettingsEnum.mapThemeMode)],
@@ -34,65 +29,11 @@ class MapStateNotifier extends _$MapStateNotifier {
appSettingsProvider.getSetting(AppSettingsEnum.mapwithPartners),
relativeTime:
appSettingsProvider.getSetting(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,
diff --git a/mobile/lib/providers/server_info.provider.dart b/mobile/lib/providers/server_info.provider.dart
index 6327f992f5cd0..14521b06f64ca 100644
--- a/mobile/lib/providers/server_info.provider.dart
+++ b/mobile/lib/providers/server_info.provider.dart
@@ -34,6 +34,9 @@ class ServerInfoNotifier extends StateNotifier {
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",
diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart
new file mode 100644
index 0000000000000..0b1b4d99f36df
--- /dev/null
+++ b/mobile/lib/repositories/activity_api.repository.dart
@@ -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> getAll(String albumId, {String? assetId}) async {
+ final response =
+ await checkNull(_api.getActivities(albumId, assetId: assetId));
+ return response.map(_toActivity).toList();
+ }
+
+ @override
+ Future 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 delete(String id) {
+ return checkNull(_api.deleteActivity(id));
+ }
+
+ @override
+ Future 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,
+ );
+}
diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart
new file mode 100644
index 0000000000000..0e27e44684a67
--- /dev/null
+++ b/mobile/lib/repositories/album_api.repository.dart
@@ -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 get(String id) async {
+ final dto = await checkNull(_api.getAlbumInfo(id));
+ return _toAlbum(dto);
+ }
+
+ @override
+ Future> getAll({bool? shared}) async {
+ final dtos = await checkNull(_api.getAllAlbums(shared: shared));
+ return dtos.map(_toAlbum).toList().cast();
+ }
+
+ @override
+ Future create(
+ String name, {
+ required Iterable assetIds,
+ Iterable 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 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 delete(String albumId) {
+ return _api.deleteAlbum(albumId);
+ }
+
+ @override
+ Future<({List added, List duplicates})> addAssets(
+ String albumId,
+ Iterable assetIds,
+ ) async {
+ final response = await checkNull(
+ _api.addAssetsToAlbum(
+ albumId,
+ BulkIdsDto(ids: assetIds.toList()),
+ ),
+ );
+
+ final List added = [];
+ final List 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 removed, List failed})> removeAssets(
+ String albumId,
+ Iterable assetIds,
+ ) async {
+ final response = await checkNull(
+ _api.removeAssetFromAlbum(
+ albumId,
+ BulkIdsDto(ids: assetIds.toList()),
+ ),
+ );
+ final List 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 addUsers(String albumId, Iterable 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 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;
+ }
+}
diff --git a/mobile/lib/repositories/album_media.repository.dart b/mobile/lib/repositories/album_media.repository.dart
new file mode 100644
index 0000000000000..c3795f75df4e1
--- /dev/null
+++ b/mobile/lib/repositories/album_media.repository.dart
@@ -0,0 +1,93 @@
+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/store.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final albumMediaRepositoryProvider = Provider((ref) => AlbumMediaRepository());
+
+class AlbumMediaRepository implements IAlbumMediaRepository {
+ @override
+ Future> getAll() async {
+ final List assetPathEntities =
+ await PhotoManager.getAssetPathList(
+ hasAll: true,
+ filterOption: FilterOptionGroup(containsPathModified: true),
+ );
+ return assetPathEntities.map(_toAlbum).toList();
+ }
+
+ @override
+ Future> getAssetIds(String albumId) async {
+ final album = await AssetPathEntity.fromId(albumId);
+ final List assets =
+ await album.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
+ return assets.map((e) => e.id).toList();
+ }
+
+ @override
+ Future getAssetCount(String albumId) async {
+ final album = await AssetPathEntity.fromId(albumId);
+ return album.assetCountAsync;
+ }
+
+ @override
+ Future> getAssets(
+ String albumId, {
+ int start = 0,
+ int end = 0x7fffffffffffffff,
+ DateTime? modifiedFrom,
+ DateTime? modifiedUntil,
+ bool orderByModificationDate = false,
+ }) async {
+ final onDevice = await AssetPathEntity.fromId(
+ albumId,
+ filterOption: FilterOptionGroup(
+ containsPathModified: true,
+ orders: orderByModificationDate
+ ? [const OrderOption(type: OrderOptionType.updateDate)]
+ : [],
+ imageOption: const FilterOption(needTitle: true),
+ videoOption: const FilterOption(needTitle: true),
+ updateTimeCond: modifiedFrom == null && modifiedUntil == null
+ ? null
+ : DateTimeCond(
+ min: modifiedFrom ?? DateTime.utc(-271820),
+ max: modifiedUntil ?? DateTime.utc(275760),
+ ),
+ ),
+ );
+
+ final List assets =
+ await onDevice.getAssetListRange(start: start, end: end);
+ return assets.map(AssetMediaRepository.toAsset).toList().cast();
+ }
+
+ @override
+ Future get(
+ String id, {
+ DateTime? modifiedFrom,
+ DateTime? modifiedUntil,
+ }) async {
+ final assetPathEntity = await AssetPathEntity.fromId(id);
+ return _toAlbum(assetPathEntity);
+ }
+
+ static Album _toAlbum(AssetPathEntity assetPathEntity) {
+ final Album album = Album(
+ name: assetPathEntity.name,
+ createdAt:
+ assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
+ modifiedAt:
+ assetPathEntity.lastModified?.toUtc() ?? DateTime.now().toUtc(),
+ shared: false,
+ activityEnabled: false,
+ );
+ album.owner.value = Store.get(StoreKey.currentUser);
+ album.localId = assetPathEntity.id;
+ album.isAll = assetPathEntity.isAll;
+ return album;
+ }
+}
diff --git a/mobile/lib/repositories/asset.repository.dart b/mobile/lib/repositories/asset.repository.dart
index ea05feab38f68..c6012af3717eb 100644
--- a/mobile/lib/repositories/asset.repository.dart
+++ b/mobile/lib/repositories/asset.repository.dart
@@ -28,4 +28,91 @@ class AssetRepository implements IAssetRepository {
@override
Future deleteById(List ids) =>
_db.writeTxn(() => _db.assets.deleteAll(ids));
+
+ @override
+ Future getByRemoteId(String id) => _db.assets.getByRemoteId(id);
+
+ @override
+ Future> getAllByRemoteId(Iterable ids) =>
+ _db.assets.getAllByRemoteId(ids);
+
+ @override
+ Future> getAll({
+ required int ownerId,
+ bool? remote,
+ int limit = 100,
+ }) {
+ if (remote == null) {
+ return _db.assets
+ .where()
+ .ownerIdEqualToAnyChecksum(ownerId)
+ .limit(limit)
+ .findAll();
+ }
+ final QueryBuilder 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> getMatches({
+ required List assets,
+ required int ownerId,
+ bool? remote,
+ int limit = 100,
+ }) {
+ final QueryBuilder 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);
+ }
}
+
+Future> _getMatchesImpl(
+ QueryBuilder query,
+ int ownerId,
+ List 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();
diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart
new file mode 100644
index 0000000000000..3ad0e1cba0d19
--- /dev/null
+++ b/mobile/lib/repositories/asset_api.repository.dart
@@ -0,0 +1,25 @@
+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),
+);
+
+class AssetApiRepository extends BaseApiRepository
+ implements IAssetApiRepository {
+ final AssetsApi _api;
+
+ AssetApiRepository(this._api);
+
+ @override
+ Future update(String id, {String? description}) async {
+ final response = await checkNull(
+ _api.updateAsset(id, UpdateAssetDto(description: description)),
+ );
+ return Asset.remote(response);
+ }
+}
diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart
new file mode 100644
index 0000000000000..20cf680339e53
--- /dev/null
+++ b/mobile/lib/repositories/asset_media.repository.dart
@@ -0,0 +1,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_media.interface.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository());
+
+class AssetMediaRepository implements IAssetMediaRepository {
+ @override
+ Future> deleteAll(List ids) =>
+ PhotoManager.editor.deleteWithIds(ids);
+
+ @override
+ Future get(String id) async {
+ final entity = await AssetEntity.fromId(id);
+ return toAsset(entity);
+ }
+
+ static Asset? toAsset(AssetEntity? local) {
+ if (local == null) return null;
+ final Asset asset = Asset(
+ checksum: "",
+ localId: local.id,
+ ownerId: Store.get(StoreKey.currentUser).isarId,
+ fileCreatedAt: local.createDateTime,
+ fileModifiedAt: local.modifiedDateTime,
+ updatedAt: local.modifiedDateTime,
+ durationInSeconds: local.duration,
+ type: AssetType.values[local.typeInt],
+ fileName: local.title!,
+ width: local.width,
+ height: local.height,
+ isFavorite: local.isFavorite,
+ );
+ if (asset.fileCreatedAt.year == 1970) {
+ asset.fileCreatedAt = asset.fileModifiedAt;
+ }
+ if (local.latitude != null) {
+ asset.exifInfo = ExifInfo(lat: local.latitude, long: local.longitude);
+ }
+ asset.local = local;
+ return asset;
+ }
+}
diff --git a/mobile/lib/repositories/base_api.repository.dart b/mobile/lib/repositories/base_api.repository.dart
new file mode 100644
index 0000000000000..418cba84f886c
--- /dev/null
+++ b/mobile/lib/repositories/base_api.repository.dart
@@ -0,0 +1,11 @@
+import 'package:flutter/foundation.dart';
+import 'package:immich_mobile/constants/errors.dart';
+
+abstract class BaseApiRepository {
+ @protected
+ Future checkNull(Future future) async {
+ final response = await future;
+ if (response == null) throw NoResponseDtoError();
+ return response;
+ }
+}
diff --git a/mobile/lib/repositories/exif_info.repository.dart b/mobile/lib/repositories/exif_info.repository.dart
new file mode 100644
index 0000000000000..a165e98bdbfe3
--- /dev/null
+++ b/mobile/lib/repositories/exif_info.repository.dart
@@ -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 delete(int id) => _db.exifInfos.delete(id);
+
+ @override
+ Future get(int id) => _db.exifInfos.get(id);
+
+ @override
+ Future update(ExifInfo exifInfo) async {
+ await _db.writeTxn(() => _db.exifInfos.put(exifInfo));
+ return exifInfo;
+ }
+}
diff --git a/mobile/lib/repositories/file_media.repository.dart b/mobile/lib/repositories/file_media.repository.dart
new file mode 100644
index 0000000000000..e115868ba0d60
--- /dev/null
+++ b/mobile/lib/repositories/file_media.repository.dart
@@ -0,0 +1,62 @@
+import 'dart:io';
+
+import 'package:flutter/foundation.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/repositories/asset_media.repository.dart';
+import 'package:photo_manager/photo_manager.dart' hide AssetType;
+
+final fileMediaRepositoryProvider = Provider((ref) => FileMediaRepository());
+
+class FileMediaRepository implements IFileMediaRepository {
+ @override
+ Future saveImage(
+ Uint8List data, {
+ required String title,
+ String? relativePath,
+ }) async {
+ final entity = await PhotoManager.editor
+ .saveImage(data, title: title, relativePath: relativePath);
+ return AssetMediaRepository.toAsset(entity);
+ }
+
+ @override
+ Future saveLivePhoto({
+ required File image,
+ required File video,
+ required String title,
+ }) async {
+ final entity = await PhotoManager.editor.darwin.saveLivePhoto(
+ imageFile: image,
+ videoFile: video,
+ title: title,
+ );
+ return AssetMediaRepository.toAsset(entity);
+ }
+
+ @override
+ Future saveVideo(
+ File file, {
+ required String title,
+ String? relativePath,
+ }) async {
+ final entity = await PhotoManager.editor.saveVideo(
+ file,
+ title: title,
+ relativePath: relativePath,
+ );
+ return AssetMediaRepository.toAsset(entity);
+ }
+
+ @override
+ Future clearFileCache() => PhotoManager.clearFileCache();
+
+ @override
+ Future enableBackgroundAccess() =>
+ PhotoManager.setIgnorePermissionCheck(true);
+
+ @override
+ Future requestExtendedPermissions() =>
+ PhotoManager.requestPermissionExtend();
+}
diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart
index cd87eb17ecb24..b05af9a57f89c 100644
--- a/mobile/lib/repositories/user.repository.dart
+++ b/mobile/lib/repositories/user.repository.dart
@@ -17,4 +17,7 @@ class UserRepository implements IUserRepository {
@override
Future> getByIds(List ids) async =>
(await _db.users.getAllById(ids)).cast();
+
+ @override
+ Future get(String id) => _db.users.getById(id);
}
diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart
index 211c847726095..6869e7b7047e9 100644
--- a/mobile/lib/routing/router.dart
+++ b/mobile/lib/routing/router.dart
@@ -63,7 +63,6 @@ import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
import 'package:isar/isar.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
-import 'package:photo_manager/photo_manager.dart' hide LatLng;
part 'router.gr.dart';
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 90fc4cb0fe96c..df4c29fba1c70 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -185,7 +185,7 @@ class AlbumOptionsRouteArgs {
class AlbumPreviewRoute extends PageRouteInfo {
AlbumPreviewRoute({
Key? key,
- required AssetPathEntity album,
+ required Album album,
List? children,
}) : super(
AlbumPreviewRoute.name,
@@ -218,7 +218,7 @@ class AlbumPreviewRouteArgs {
final Key? key;
- final AssetPathEntity album;
+ final Album album;
@override
String toString() {
diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart
index 58af26e204663..5496041416558 100644
--- a/mobile/lib/services/activity.service.dart
+++ b/mobile/lib/services/activity.service.dart
@@ -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> 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 getStatistics(String albumId, {String? assetId}) async {
+ Future 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 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",
);
}
diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart
index 92302a0d88f29..dd021e698e094 100644
--- a/mobile/lib/services/album.service.dart
+++ b/mobile/lib/services/album.service.dart
@@ -6,59 +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/user.repository.dart';
-import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/repositories/album_media.repository.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';
-import 'package:photo_manager/photo_manager.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 _localCompleter = Completer()..complete(false);
Completer _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
@@ -84,11 +86,7 @@ class AlbumService {
}
return false;
}
- final List onDevice =
- await PhotoManager.getAssetPathList(
- hasAll: true,
- filterOption: FilterOptionGroup(containsPathModified: true),
- );
+ final List onDevice = await _albumMediaRepository.getAll();
_log.info("Found ${onDevice.length} device albums");
Set? excludedAssets;
if (excludedIds.isNotEmpty) {
@@ -104,13 +102,15 @@ class AlbumService {
_log.info("Found ${excludedAssets.length} assets to exclude");
}
// remove all excluded albums
- onDevice.removeWhere((e) => excludedIds.contains(e.id));
+ onDevice.removeWhere((e) => excludedIds.contains(e.localId));
_log.info(
"Ignoring ${excludedIds.length} excluded albums resulting in ${onDevice.length} device albums",
);
}
final hasAll = selectedIds
- .map((id) => onDevice.firstWhereOrNull((a) => a.id == id))
+ .map(
+ (id) => onDevice.firstWhereOrNull((album) => album.localId == id),
+ )
.whereNotNull()
.any((a) => a.isAll);
if (hasAll) {
@@ -122,7 +122,7 @@ class AlbumService {
}
} else {
// keep only the explicitly selected albums
- onDevice.removeWhere((e) => !selectedIds.contains(e.id));
+ onDevice.removeWhere((e) => !selectedIds.contains(e.localId));
_log.info("'Recents' is not selected, keeping only selected albums");
}
changes =
@@ -136,15 +136,15 @@ class AlbumService {
}
Future> _loadExcludedAssetIds(
- List albums,
+ List albums,
List excludedAlbumIds,
) async {
final Set result = HashSet();
- for (AssetPathEntity a in albums) {
- if (excludedAlbumIds.contains(a.id)) {
- final List assets =
- await a.getAssetListRange(start: 0, end: 0x7fffffffffffffff);
- result.addAll(assets.map((e) => e.id));
+ for (Album album in albums) {
+ if (excludedAlbumIds.contains(album.localId)) {
+ final assetIds =
+ await _albumMediaRepository.getAssetIds(album.localId!);
+ result.addAll(assetIds);
}
}
return result;
@@ -162,17 +162,11 @@ class AlbumService {
bool changes = false;
try {
await _userService.refreshUsers();
- final List? serverAlbums = await _apiService.albumsApi
- .getAllAlbums(shared: isShared ? true : null);
- if (serverAlbums == null) {
- return false;
- }
+ final List 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);
@@ -186,30 +180,13 @@ class AlbumService {
Iterable assets, [
Iterable 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);
}
/*
@@ -241,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 successAssets = [];
- List duplicatedAssets = [];
+ final List 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()}");
}
@@ -291,20 +257,11 @@ class AlbumService {
Album album,
) async {
try {
- final List 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()}");
}
@@ -313,15 +270,13 @@ class AlbumService {
Future 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()}");
}
@@ -332,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 =
@@ -363,7 +318,7 @@ class AlbumService {
Future 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()}");
@@ -376,21 +331,14 @@ class AlbumService {
Iterable 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()}");
}
@@ -402,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);
@@ -425,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()}");
@@ -454,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);
}
}
}
diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart
index c4f258e259129..262040026e6a2 100644
--- a/mobile/lib/services/asset.service.dart
+++ b/mobile/lib/services/asset.service.dart
@@ -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,
@@ -321,7 +331,7 @@ class AssetService {
for (BackupCandidate candidate in candidates) {
final asset = remoteAssets.firstWhereOrNull(
- (a) => a.localId == candidate.asset.id,
+ (a) => a.localId == candidate.asset.localId,
);
if (asset != null) {
@@ -342,4 +352,46 @@ class AssetService {
log.severe("Error while syncing uploaded asset to albums", error, stack);
}
}
+
+ Future 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 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 ?? "";
+ }
}
diff --git a/mobile/lib/services/asset_description.service.dart b/mobile/lib/services/asset_description.service.dart
deleted file mode 100644
index 196e29dc6a97d..0000000000000
--- a/mobile/lib/services/asset_description.service.dart
+++ /dev/null
@@ -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 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),
- ),
-);
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index 0d4d547434034..09030a621bc9d 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -13,10 +13,14 @@ 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/user.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';
@@ -34,7 +38,7 @@ import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
import 'package:isar/isar.dart';
import 'package:path_provider_ios/path_provider_ios.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
final backgroundServiceProvider = Provider(
(ref) => BackgroundService(),
@@ -361,23 +365,42 @@ class BackgroundService {
PartnerService partnerService = PartnerService(apiService, db);
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
- UserRepository userRepository = UserRepository(db);
BackupRepository backupAlbumRepository = BackupRepository(db);
- HashService hashService = HashService(db, this);
- SyncService syncSerive = SyncService(db, hashService);
+ AlbumMediaRepository albumMediaRepository = AlbumMediaRepository();
+ FileMediaRepository fileMediaRepository = FileMediaRepository();
+ UserRepository userRepository = UserRepository(db);
+ AlbumApiRepository albumApiRepository =
+ AlbumApiRepository(apiService.albumsApi);
+ HashService hashService = HashService(db, this, albumMediaRepository);
+ EntityService entityService =
+ EntityService(assetRepository, userRepository);
+ SyncService syncSerive = SyncService(
+ db,
+ hashService,
+ entityService,
+ albumMediaRepository,
+ albumApiRepository,
+ );
UserService userService =
UserService(apiService, db, syncSerive, partnerService);
AlbumService albumService = AlbumService(
- apiService,
userService,
syncSerive,
+ entityService,
albumRepository,
assetRepository,
- userRepository,
backupAlbumRepository,
+ albumMediaRepository,
+ albumApiRepository,
+ );
+ BackupService backupService = BackupService(
+ apiService,
+ db,
+ settingService,
+ albumService,
+ albumMediaRepository,
+ fileMediaRepository,
);
- BackupService backupService =
- BackupService(apiService, db, settingService, albumService);
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
@@ -385,7 +408,7 @@ class BackgroundService {
return true;
}
- await PhotoManager.setIgnorePermissionCheck(true);
+ await fileMediaRepository.enableBackgroundAccess();
do {
final bool backupOk = await _runBackup(
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index 858499443ee1d..19d731d773d75 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -6,9 +6,13 @@ import 'package:cancellation_token_http/http.dart' as http;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
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/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/interfaces/album_media.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
@@ -16,6 +20,8 @@ import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/repositories/album_media.repository.dart';
+import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
@@ -24,7 +30,7 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart' as pm;
-import 'package:photo_manager/photo_manager.dart';
+import 'package:photo_manager/photo_manager.dart' show PMProgressHandler;
final backupServiceProvider = Provider(
(ref) => BackupService(
@@ -32,6 +38,8 @@ final backupServiceProvider = Provider(
ref.watch(dbProvider),
ref.watch(appSettingsServiceProvider),
ref.watch(albumServiceProvider),
+ ref.watch(albumMediaRepositoryProvider),
+ ref.watch(fileMediaRepositoryProvider),
),
);
@@ -42,12 +50,16 @@ class BackupService {
final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting;
final AlbumService _albumService;
+ final IAlbumMediaRepository _albumMediaRepository;
+ final IFileMediaRepository _fileMediaRepository;
BackupService(
this._apiService,
this._db,
this._appSetting,
this._albumService,
+ this._albumMediaRepository,
+ this._fileMediaRepository,
);
Future?> getDeviceBackupAsset() async {
@@ -86,44 +98,17 @@ class BackupService {
List excludedBackupAlbums, {
bool useTimeFilter = true,
}) async {
- final filter = FilterOptionGroup(
- containsPathModified: true,
- orders: [const OrderOption(type: OrderOptionType.updateDate)],
- // title is needed to create Assets
- imageOption: const FilterOption(needTitle: true),
- videoOption: const FilterOption(needTitle: true),
- );
final now = DateTime.now();
- final List selectedAlbums =
- await _loadAlbumsWithTimeFilter(
- selectedBackupAlbums,
- filter,
- now,
- useTimeFilter: useTimeFilter,
- );
-
- if (selectedAlbums.every((e) => e == null)) {
- return {};
- }
-
- final List excludedAlbums =
- await _loadAlbumsWithTimeFilter(
- excludedBackupAlbums,
- filter,
- now,
- useTimeFilter: useTimeFilter,
- );
-
final Set toAdd = await _fetchAssetsAndUpdateLastBackup(
- selectedAlbums,
selectedBackupAlbums,
now,
useTimeFilter: useTimeFilter,
);
+ if (toAdd.isEmpty) return {};
+
final Set toRemove = await _fetchAssetsAndUpdateLastBackup(
- excludedAlbums,
excludedBackupAlbums,
now,
useTimeFilter: useTimeFilter,
@@ -132,92 +117,62 @@ class BackupService {
return toAdd.difference(toRemove);
}
- Future> _loadAlbumsWithTimeFilter(
- List albums,
- FilterOptionGroup filter,
- DateTime now, {
- bool useTimeFilter = true,
- }) async {
- List result = [];
- for (BackupAlbum backupAlbum in albums) {
- try {
- final optionGroup = useTimeFilter
- ? filter.copyWith(
- updateTimeCond: DateTimeCond(
- // subtract 2 seconds to prevent missing assets due to rounding issues
- min: backupAlbum.lastBackup
- .subtract(const Duration(seconds: 2)),
- max: now,
- ),
- )
- : filter;
-
- final AssetPathEntity album =
- await AssetPathEntity.obtainPathFromProperties(
- id: backupAlbum.id,
- optionGroup: optionGroup,
- maxDateTimeToNow: false,
- );
-
- result.add(album);
- } on StateError {
- // either there are no assets matching the filter criteria OR the album no longer exists
- }
- }
-
- return result;
- }
-
Future> _fetchAssetsAndUpdateLastBackup(
- List localAlbums,
List backupAlbums,
DateTime now, {
bool useTimeFilter = true,
}) async {
- Set candidate = {};
+ Set candidates = {};
- for (int i = 0; i < localAlbums.length; i++) {
- final localAlbum = localAlbums[i];
- if (localAlbum == null) {
+ for (final BackupAlbum backupAlbum in backupAlbums) {
+ final Album localAlbum;
+ try {
+ localAlbum = await _albumMediaRepository.get(backupAlbum.id);
+ } on StateError {
+ // the album no longer exists
continue;
}
if (useTimeFilter &&
- localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) ==
- true) {
+ localAlbum.modifiedAt.isBefore(backupAlbum.lastBackup)) {
+ continue;
+ }
+ final List assets;
+ try {
+ assets = await _albumMediaRepository.getAssets(
+ backupAlbum.id,
+ modifiedFrom: useTimeFilter
+ ?
+ // subtract 2 seconds to prevent missing assets due to rounding issues
+ backupAlbum.lastBackup.subtract(const Duration(seconds: 2))
+ : null,
+ modifiedUntil: useTimeFilter ? now : null,
+ );
+ } on StateError {
+ // either there are no assets matching the filter criteria OR the album no longer exists
continue;
}
-
- final assets = await localAlbum.getAssetListRange(
- start: 0,
- end: await localAlbum.assetCountAsync,
- );
// Add album's name to the asset info
for (final asset in assets) {
List albumNames = [localAlbum.name];
- final existingAsset = candidate.firstWhereOrNull(
- (a) => a.asset.id == asset.id,
+ final existingAsset = candidates.firstWhereOrNull(
+ (candidate) => candidate.asset.localId == asset.localId,
);
if (existingAsset != null) {
albumNames.addAll(existingAsset.albumNames);
- candidate.remove(existingAsset);
+ candidates.remove(existingAsset);
}
- candidate.add(
- BackupCandidate(
- asset: asset,
- albumNames: albumNames,
- ),
- );
+ candidates.add(BackupCandidate(asset: asset, albumNames: albumNames));
}
- backupAlbums[i].lastBackup = now;
+ backupAlbum.lastBackup = now;
}
- return candidate;
+ return candidates;
}
/// Returns a new list of assets not yet uploaded
@@ -230,7 +185,7 @@ class BackupService {
final Set duplicatedAssetIds = await getDuplicatedAssetIds();
candidates.removeWhere(
- (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+ (candidate) => duplicatedAssetIds.contains(candidate.asset.localId),
);
if (candidates.isEmpty) {
@@ -243,7 +198,7 @@ class BackupService {
final CheckExistingAssetsResponseDto? duplicates =
await _apiService.assetsApi.checkExistingAssets(
CheckExistingAssetsDto(
- deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
+ deviceAssetIds: candidates.map((c) => c.asset.localId!).toList(),
deviceId: deviceId,
),
);
@@ -259,7 +214,7 @@ class BackupService {
}
if (existing.isNotEmpty) {
- candidates.removeWhere((c) => existing.contains(c.asset.id));
+ candidates.removeWhere((c) => existing.contains(c.asset.localId));
}
return candidates;
@@ -278,7 +233,7 @@ class BackupService {
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
if (Platform.isIOS) {
- await PhotoManager.requestPermissionExtend();
+ await _fileMediaRepository.requestExtendedPermissions();
}
return true;
@@ -289,9 +244,9 @@ class BackupService {
List _sortPhotosFirst(List candidates) {
return candidates.sorted(
(a, b) {
- final cmp = a.asset.typeInt - b.asset.typeInt;
+ final cmp = a.asset.type.index - b.asset.type.index;
if (cmp != 0) return cmp;
- return a.asset.createDateTime.compareTo(b.asset.createDateTime);
+ return a.asset.fileCreatedAt.compareTo(b.asset.fileCreatedAt);
},
);
}
@@ -325,13 +280,13 @@ class BackupService {
}
for (final candidate in candidates) {
- final AssetEntity entity = candidate.asset;
+ final Asset asset = candidate.asset;
File? file;
File? livePhotoFile;
try {
final isAvailableLocally =
- await entity.isLocallyAvailable(isOrigin: true);
+ await asset.local!.isLocallyAvailable(isOrigin: true);
// Handle getting files from iCloud
if (!isAvailableLocally && Platform.isIOS) {
@@ -342,39 +297,41 @@ class BackupService {
onCurrentAsset(
CurrentUploadAsset(
- id: entity.id,
- fileCreatedAt: entity.createDateTime.year == 1970
- ? entity.modifiedDateTime
- : entity.createDateTime,
- fileName: await entity.titleAsync,
- fileType: _getAssetType(entity.type),
+ id: asset.localId!,
+ fileCreatedAt: asset.fileCreatedAt.year == 1970
+ ? asset.fileModifiedAt
+ : asset.fileCreatedAt,
+ fileName: asset.fileName,
+ fileType: _getAssetType(asset.type),
iCloudAsset: true,
),
);
- file = await entity.loadFile(progressHandler: pmProgressHandler);
- if (entity.isLivePhoto) {
- livePhotoFile = await entity.loadFile(
+ file =
+ await asset.local!.loadFile(progressHandler: pmProgressHandler);
+ if (asset.local!.isLivePhoto) {
+ livePhotoFile = await asset.local!.loadFile(
withSubtype: true,
progressHandler: pmProgressHandler,
);
}
} else {
- if (entity.type == AssetType.video) {
- file = await entity.originFile;
+ if (asset.type == AssetType.video) {
+ file = await asset.local!.originFile;
} else {
- file = await entity.originFile.timeout(const Duration(seconds: 5));
- if (entity.isLivePhoto) {
- livePhotoFile = await entity.originFileWithSubtype
+ file = await asset.local!.originFile
+ .timeout(const Duration(seconds: 5));
+ if (asset.local!.isLivePhoto) {
+ livePhotoFile = await asset.local!.originFileWithSubtype
.timeout(const Duration(seconds: 5));
}
}
}
if (file != null) {
- String originalFileName = await entity.titleAsync;
+ String originalFileName = asset.fileName;
- if (entity.isLivePhoto) {
+ if (asset.local!.isLivePhoto) {
if (livePhotoFile == null) {
_log.warning(
"Failed to obtain motion part of the livePhoto - $originalFileName",
@@ -398,31 +355,31 @@ class BackupService {
baseRequest.headers.addAll(ApiService.getRequestHeaders());
baseRequest.headers["Transfer-Encoding"] = "chunked";
- baseRequest.fields['deviceAssetId'] = entity.id;
+ baseRequest.fields['deviceAssetId'] = asset.localId!;
baseRequest.fields['deviceId'] = deviceId;
baseRequest.fields['fileCreatedAt'] =
- entity.createDateTime.toUtc().toIso8601String();
+ asset.fileCreatedAt.toUtc().toIso8601String();
baseRequest.fields['fileModifiedAt'] =
- entity.modifiedDateTime.toUtc().toIso8601String();
- baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
- baseRequest.fields['duration'] = entity.videoDuration.toString();
+ asset.fileModifiedAt.toUtc().toIso8601String();
+ baseRequest.fields['isFavorite'] = asset.isFavorite.toString();
+ baseRequest.fields['duration'] = asset.duration.toString();
baseRequest.files.add(assetRawUploadData);
onCurrentAsset(
CurrentUploadAsset(
- id: entity.id,
- fileCreatedAt: entity.createDateTime.year == 1970
- ? entity.modifiedDateTime
- : entity.createDateTime,
+ id: asset.localId!,
+ fileCreatedAt: asset.fileCreatedAt.year == 1970
+ ? asset.fileModifiedAt
+ : asset.fileCreatedAt,
fileName: originalFileName,
- fileType: _getAssetType(entity.type),
+ fileType: _getAssetType(asset.type),
fileSize: file.lengthSync(),
iCloudAsset: false,
),
);
String? livePhotoVideoId;
- if (entity.isLivePhoto && livePhotoFile != null) {
+ if (asset.local!.isLivePhoto && livePhotoFile != null) {
livePhotoVideoId = await uploadLivePhotoVideo(
originalFileName,
livePhotoFile,
@@ -448,16 +405,16 @@ class BackupService {
final errorMessage = error['message'] ?? error['error'];
debugPrint(
- "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
+ "Error(${error['statusCode']}) uploading ${asset.localId} | $originalFileName | Created on ${asset.fileCreatedAt} | ${error['error']}",
);
onError(
ErrorUploadAsset(
- asset: entity,
- id: entity.id,
- fileCreatedAt: entity.createDateTime,
+ asset: asset,
+ id: asset.localId!,
+ fileCreatedAt: asset.fileCreatedAt,
fileName: originalFileName,
- fileType: _getAssetType(entity.type),
+ fileType: _getAssetType(candidate.asset.type),
errorMessage: errorMessage,
),
);
@@ -473,7 +430,7 @@ class BackupService {
bool isDuplicate = false;
if (response.statusCode == 200) {
isDuplicate = true;
- duplicatedAssetIds.add(entity.id);
+ duplicatedAssetIds.add(asset.localId!);
}
onSuccess(
diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart
index c7cd134cb1a1e..da9d8da1649e4 100644
--- a/mobile/lib/services/backup_verification.service.dart
+++ b/mobile/lib/services/backup_verification.service.dart
@@ -8,39 +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/providers/db.provider.dart';
+import 'package:immich_mobile/interfaces/asset.interface.dart';
+import 'package:immich_mobile/interfaces/exif_info.interface.dart';
+import 'package:immich_mobile/interfaces/file_media.interface.dart';
+import 'package:immich_mobile/repositories/asset.repository.dart';
+import 'package:immich_mobile/repositories/exif_info.repository.dart';
+import 'package:immich_mobile/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';
-import 'package:photo_manager/photo_manager.dart' show PhotoManager;
/// Finds duplicates originating from missing EXIF information
class BackupVerificationService {
- final Isar _db;
+ final IFileMediaRepository _fileMediaRepository;
+ final IAssetRepository _assetRepository;
+ final IExifInfoRepository _exifInfoRepository;
- BackupVerificationService(this._db);
+ BackupVerificationService(
+ this._fileMediaRepository,
+ this._assetRepository,
+ this._exifInfoRepository,
+ );
/// Returns at most [limit] assets that were backed up without exif
Future> findWronglyBackedUpAssets({int limit = 100}) async {
final owner = Store.get(StoreKey.currentUser).isarId;
- final List onlyLocal = await _db.assets
- .where()
- .remoteIdIsNull()
- .filter()
- .ownerIdEqualTo(owner)
- .localIdIsNotNull()
- .findAll();
- final List remoteMatches = await _getMatches(
- _db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(),
- owner,
- onlyLocal,
- limit,
+ final List onlyLocal = await _assetRepository.getAll(
+ ownerId: owner,
+ remote: false,
+ limit: limit,
);
- final List localMatches = await _getMatches(
- _db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(),
- owner,
- remoteMatches,
- limit,
+ final List remoteMatches = await _assetRepository.getMatches(
+ assets: onlyLocal,
+ ownerId: owner,
+ remote: true,
+ limit: limit,
+ );
+ final List localMatches = await _assetRepository.getMatches(
+ assets: remoteMatches,
+ ownerId: owner,
+ remote: false,
+ limit: limit,
);
final List deleteCandidates = [], originals = [];
@@ -50,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;
@@ -71,6 +78,7 @@ class BackupVerificationService {
auth: Store.get(StoreKey.accessToken),
endpoint: Store.get(StoreKey.serverEndpoint),
rootIsolateToken: isolateToken,
+ fileMediaRepository: _fileMediaRepository,
),
);
final upper = compute(
@@ -81,6 +89,7 @@ class BackupVerificationService {
auth: Store.get(StoreKey.accessToken),
endpoint: Store.get(StoreKey.serverEndpoint),
rootIsolateToken: isolateToken,
+ fileMediaRepository: _fileMediaRepository,
),
);
toDelete = await lower + await upper;
@@ -93,6 +102,7 @@ class BackupVerificationService {
auth: Store.get(StoreKey.accessToken),
endpoint: Store.get(StoreKey.serverEndpoint),
rootIsolateToken: isolateToken,
+ fileMediaRepository: _fileMediaRepository,
),
);
}
@@ -106,12 +116,13 @@ class BackupVerificationService {
String auth,
String endpoint,
RootIsolateToken rootIsolateToken,
+ IFileMediaRepository fileMediaRepository,
}) tuple,
) async {
assert(tuple.deleteCandidates.length == tuple.originals.length);
final List result = [];
BackgroundIsolateBinaryMessenger.ensureInitialized(tuple.rootIsolateToken);
- await PhotoManager.setIgnorePermissionCheck(true);
+ await tuple.fileMediaRepository.enableBackgroundAccess();
final ApiService apiService = ApiService();
apiService.setEndpoint(tuple.endpoint);
apiService.setAccessToken(tuple.auth);
@@ -186,35 +197,6 @@ class BackupVerificationService {
return bytes.buffer.asUint64List(start);
}
- static Future> _getMatches(
- QueryBuilder query,
- int ownerId,
- List 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
@@ -227,6 +209,8 @@ class BackupVerificationService {
final backupVerificationServiceProvider = Provider(
(ref) => BackupVerificationService(
- ref.watch(dbProvider),
+ ref.watch(fileMediaRepositoryProvider),
+ ref.watch(assetRepositoryProvider),
+ ref.watch(exifInfoRepositoryProvider),
),
);
diff --git a/mobile/lib/services/entity.service.dart b/mobile/lib/services/entity.service.dart
new file mode 100644
index 0000000000000..8297620bc70e3
--- /dev/null
+++ b/mobile/lib/services/entity.service.dart
@@ -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 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),
+ ),
+);
diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart
index ffc81a3445acf..94d680972fa1a 100644
--- a/mobile/lib/services/hash.service.dart
+++ b/mobile/lib/services/hash.service.dart
@@ -2,6 +2,9 @@ import 'dart:io';
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/repositories/album_media.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';
@@ -11,38 +14,46 @@ 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';
-import 'package:photo_manager/photo_manager.dart';
class HashService {
- HashService(this._db, this._backgroundService);
+ HashService(this._db, this._backgroundService, this._albumMediaRepository);
final Isar _db;
final BackgroundService _backgroundService;
+ final IAlbumMediaRepository _albumMediaRepository;
final _log = Logger('HashService');
/// Returns all assets that were successfully hashed
Future> getHashedAssets(
- AssetPathEntity album, {
+ Album album, {
int start = 0,
int end = 0x7fffffffffffffff,
+ DateTime? modifiedFrom,
+ DateTime? modifiedUntil,
Set? excludedAssets,
}) async {
- final entities = await album.getAssetListRange(start: start, end: end);
+ final entities = await _albumMediaRepository.getAssets(
+ album.localId!,
+ start: start,
+ end: end,
+ modifiedFrom: modifiedFrom,
+ modifiedUntil: modifiedUntil,
+ );
final filtered = excludedAssets == null
? entities
- : entities.where((e) => !excludedAssets.contains(e.id)).toList();
+ : entities.where((e) => !excludedAssets.contains(e.localId!)).toList();
return _hashAssets(filtered);
}
- /// Converts a list of [AssetEntity]s to [Asset]s including only those
+ /// Processes a list of local [Asset]s, storing their hash and returning only those
/// that were successfully hashed. Hashes are looked up in a DB table
/// [AndroidDeviceAsset] / [IOSDeviceAsset] by local id. Only missing
/// entries are newly hashed and added to the DB table.
- Future> _hashAssets(List assetEntities) async {
+ Future> _hashAssets(List assets) async {
const int batchFileCount = 128;
const int batchDataSize = 1024 * 1024 * 1024; // 1GB
- final ids = assetEntities
- .map(Platform.isAndroid ? (a) => a.id.toInt() : (a) => a.id)
+ final ids = assets
+ .map(Platform.isAndroid ? (a) => a.localId!.toInt() : (a) => a.localId!)
.toList();
final List hashes = await _lookupHashes(ids);
final List toAdd = [];
@@ -50,22 +61,28 @@ class HashService {
int bytes = 0;
- for (int i = 0; i < assetEntities.length; i++) {
+ for (int i = 0; i < assets.length; i++) {
if (hashes[i] != null) {
continue;
}
- final file = await assetEntities[i].originFile;
- if (file == null) {
- final fileName = await assetEntities[i].titleAsync.catchError((error) {
- _log.warning(
- "Failed to get title for asset ${assetEntities[i].id}",
- );
- return "";
- });
+ 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;
_log.warning(
- "Failed to get file for asset ${assetEntities[i].id}, name: $fileName, created on: ${assetEntities[i].createDateTime}, skipping",
+ "Failed to get file for asset ${assets[i].localId}, name: $fileName, created on: ${assets[i].fileCreatedAt}, skipping",
);
continue;
}
@@ -86,7 +103,7 @@ class HashService {
if (toHash.isNotEmpty) {
await _processBatch(toHash, toAdd);
}
- return _mapAllHashedAssets(assetEntities, hashes);
+ return _getHashedAssets(assets, hashes);
}
/// Lookup hashes of assets by their local ID
@@ -133,15 +150,16 @@ class HashService {
return hashes;
}
- /// Converts [AssetEntity]s that were successfully hashed to [Asset]s
- List _mapAllHashedAssets(
- List