diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ae0368861abfe..0b0cfbafd918f 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,14 @@ blank_issues_enabled: false contact_links: - - name: I have a question or need support + - name: ✋ I have a question or need support url: https://discord.immich.app about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support. - - name: Feature Request + - name: 📷 My photo or video has a date, time, or timezone problem + url: https://github.com/immich-app/immich/discussions/12650 + about: Upload a sample file to this discussion and we will take a look + - name: 🌟 Feature request url: https://github.com/immich-app/immich/discussions/new?category=feature-request about: Please use our GitHub Discussion for making feature requests. - - name: I'm unsure where to go + - name: 🫣 I'm unsure where to go url: https://discord.immich.app about: If you are unsure where to go, then joining our Discord is recommended; Just ask! 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/.vscode/launch.json b/.vscode/launch.json index 7a88b7f3e12d4..ed3da9f667d01 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,8 +5,8 @@ "type": "node", "request": "attach", "restart": true, - "port": 9230, - "name": "Immich Server", + "port": 9231, + "name": "Immich API Server", "remoteRoot": "/usr/src/app", "localRoot": "${workspaceFolder}/server" }, @@ -14,8 +14,8 @@ "type": "node", "request": "attach", "restart": true, - "port": 9231, - "name": "Immich Microservices", + "port": 9230, + "name": "Immich Workers", "remoteRoot": "/usr/src/app", "localRoot": "${workspaceFolder}/server" } diff --git a/README.md b/README.md index 44c38e6d14813..5c4b9c39edd1d 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Português Brasileiro Svenska العربية +Tiếng Việt

diff --git a/cli/Dockerfile b/cli/Dockerfile index e3cce6d448249..b08aba9d3c2b5 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS core +FROM node:20.17.0-alpine3.20@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/package-lock.json b/cli/package-lock.json index f443c141b9e06..e508fe843f60d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.18", + "version": "2.2.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.18", + "version": "2.2.22", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -52,14 +52,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.114.0", + "version": "1.116.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "typescript": "^5.3.3" } }, @@ -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", @@ -1324,9 +1337,9 @@ } }, "node_modules/@types/node": { - "version": "20.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", - "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dev": true, "license": "MIT", "dependencies": { @@ -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,21 +3342,23 @@ "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/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "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" }, @@ -3436,9 +3383,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -3457,8 +3404,8 @@ "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" @@ -3827,10 +3774,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" } @@ -3933,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", @@ -4031,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", @@ -4055,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" } @@ -4140,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": { @@ -4210,14 +4155,14 @@ } }, "node_modules/vite": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", - "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.41", + "postcss": "^8.4.43", "rollup": "^4.20.0" }, "bin": { @@ -4270,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": { @@ -4312,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": { @@ -4349,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": "*" }, @@ -4535,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/cli/package.json b/cli/package.json index 0d560c8456585..522a8e593e9e7 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.18", + "version": "2.2.22", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl index afa00e60677c1..6419c16dad324 100644 --- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.41.0" - constraints = "4.41.0" + version = "4.43.0" + constraints = "4.43.0" hashes = [ - "h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=", - "h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=", - "h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=", - "h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=", - "h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=", - "h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=", - "h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=", - "h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=", - "h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=", - "h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=", - "h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=", - "h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=", - "h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=", - "h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=", - "zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8", - "zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562", - "zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf", - "zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d", - "zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6", - "zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6", + "h1:2kDVLD36BOVgBzI9p0WIQ+xjFfMmjaItA0l8SyZWEPo=", + "h1:2sGJDAwFEgO8+3y+2suYO+yrjNOzSsihad0hbM3+jPg=", + "h1:A1WPQFcdD+7FrFBFrKcx4CiSr75xSmsO93C0e5NBAeQ=", + "h1:BuXs/1ohmF4fWyOErY6vNbm7DaEIfbLSepSiZ2ol9I8=", + "h1:QPh+X19oyo808sqdeJaVqahZcQgcG1jCi3DA5zpjz6U=", + "h1:RI7c7dhSJoIkfou5b8ITRpM5MqsQD3FULj1h/rI4rJk=", + "h1:gdI5JTCPjewdGq1bhGAs+V5qCcmJ73N2gtMfuFybJp4=", + "h1:h4lnJpCIYZ7dsN9IO2mmwNdWNiQYEPoAEUjLF2sZ5kc=", + "h1:jTaExrX/eR7vGT5wayGqH8ZtXS2zyk0WmD3zbAKFIQU=", + "h1:l5NKJUOQJ1mHl1eekeXaxUZ+g+8Yv4aGcIN9vuK6GL4=", + "h1:sNbvm66/2vc8B/khyioOO8eNaU8nb89x693AN7fQheU=", + "h1:tXS4g1yE420AU4mvZ7RrYI+yYTutkRID3l+W0gBH4BM=", + "h1:vA+kES7uqmKA9K0U45IXR94jaTQZCHZLCHqMUeGxKMI=", + "h1:zV131k79+ob9p4jrLDgztDNvZvt8fvrrzpn0nPikBw8=", + "zh:006d111d6eafe6eeb5df2f91bd0ca320f979bd71f8cd8c475f10b2bd94acba55", + "zh:031fbb5cac23a841dc18e270cbfcd3ce9f4ba504edbd3c78931f7ed9827220a8", + "zh:07a72fe8b55afee99529bf4169ab6abfac5eabcd10968c29101925bcd358b09f", + "zh:0d14727d011c2d9df4c3058f527d2409223449ab48b46cbc86922eb553ef77c1", + "zh:155ce1333672d26cd18a5866b0761489d91682beffee58e45c3a1b68e8491d3d", + "zh:35a2a1939a965335b29ebdbfd759d93a97c0f589d9cd218f537dee6f600e3fb9", + "zh:52912fe421e7d911431f77788db2ea13836efd65a2e82385adb52c6a84d4ee90", + "zh:57374318d9194ea1db08884b0541a9055823d5970ad48f9a57547ac231163007", + "zh:5fb942b9e2553c058fe09fe12fb39dd175cd6715bb41c059c1a70df2bfc64dc1", + "zh:63cabd2bda201b09b35a3279d1f813ab71394b9b90fc5cf8962a5eba207803bc", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7", - "zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e", - "zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d", - "zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba", - "zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d", - "zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0", - "zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54", - "zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49", + "zh:978ee67d3d53970a5c474ab40b00adee97f4153b16804a2b6b7ee205ae69d18a", + "zh:bbafdbef631b5c80570087817b42b16b1a76d556d692853a71c47fb48663cf00", + "zh:be91b3f2a697cbbb41f65aad2600972d0ede1e962a7d8a00bb3177cb77d86666", + "zh:efe168ad4aaa6156ce5a31d4e50e9d54d38ee5a5888412f9e690c0de5d619683", ] } diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf index 18d8ff1eb4665..74ea6d5816f8e 100644 --- a/deployment/modules/cloudflare/docs-release/config.tf +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.41.0" + version = "4.43.0" } } } diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl index afa00e60677c1..6419c16dad324 100644 --- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.41.0" - constraints = "4.41.0" + version = "4.43.0" + constraints = "4.43.0" hashes = [ - "h1:0mc+YrjQrcctGrGYDmzlcqcgSv9MYB74rvMaZylIKC8=", - "h1:0zUx4vk4jOORQqn6xHBF7dO6N6bielFHdJ0mgF4Obn8=", - "h1:AsIZW3uLFNOZO7kL/K7/Y/S0IYxUV9Hz85NNk/3TTsA=", - "h1:FSgYM4+LHMbX/a4Y1kx7FPPWmXqS3/MQYzvjMJHHHWM=", - "h1:Tx6Nh3BWP1x9L3KK/Eyi+ET0T26g3+jf1jyiuqpNIis=", - "h1:VRI9wu8P43xxfpeTndRwsisLnqncfnmEYMOEH5zH4pQ=", - "h1:YxQqmiES/Yanq/VfGqBEqg+VIO7FGhO88aKoWFHyGIg=", - "h1:ZWHiaesjgDLKWlfdNj0oKyj/DWdxcfsO6NINu39zfpY=", - "h1:a2aCgDDBz3ccrr8YstIMl7VFnKo1xZAp+rOv59PPJ7U=", - "h1:aRyv8tB6wBAF9lKsLEdiHyCqnK5LfZq0FqMXCcUB4UU=", - "h1:lXpuO7zv2uD2GzPE1ARxznreRAh+QHTc2lAJ7iOoFgY=", - "h1:sA1xq0QNQ4fH8SHXouYNq50xirVD18SamKQwPsBQrrY=", - "h1:v7sHvKq7oqMYPn47ULHFyIQsKD9o+6Xg/uHbxQUixEw=", - "h1:wo/x4atWyXuWGlfR6h5nH0YwBAmBwTRY27HtWP8ycLo=", - "zh:339d26e06dc6fb299ea8aad9476a60fd65bb1d40631ae8eeb81cddf2dd2bebc8", - "zh:3dec2ad96ac2c283fd34ce65781b55c4edbb4d5c5cb53da8e31537176c0ed562", - "zh:5f63a5f8080319a2fff09d4d49944829fa708723436520787cfb60725ced80cf", - "zh:67162c28ccea71cb8141ed15c0637e35621354ebe14878e0b75a8f160fc5505d", - "zh:6ac1e07f5347b6395aca690ed22101bb25e957d25f986f760ff673a7adfd5ef6", - "zh:70282a723c7b52fcabde2baad41c864ed3a8d69f0c4d27a6b6933cac434cffc6", + "h1:2kDVLD36BOVgBzI9p0WIQ+xjFfMmjaItA0l8SyZWEPo=", + "h1:2sGJDAwFEgO8+3y+2suYO+yrjNOzSsihad0hbM3+jPg=", + "h1:A1WPQFcdD+7FrFBFrKcx4CiSr75xSmsO93C0e5NBAeQ=", + "h1:BuXs/1ohmF4fWyOErY6vNbm7DaEIfbLSepSiZ2ol9I8=", + "h1:QPh+X19oyo808sqdeJaVqahZcQgcG1jCi3DA5zpjz6U=", + "h1:RI7c7dhSJoIkfou5b8ITRpM5MqsQD3FULj1h/rI4rJk=", + "h1:gdI5JTCPjewdGq1bhGAs+V5qCcmJ73N2gtMfuFybJp4=", + "h1:h4lnJpCIYZ7dsN9IO2mmwNdWNiQYEPoAEUjLF2sZ5kc=", + "h1:jTaExrX/eR7vGT5wayGqH8ZtXS2zyk0WmD3zbAKFIQU=", + "h1:l5NKJUOQJ1mHl1eekeXaxUZ+g+8Yv4aGcIN9vuK6GL4=", + "h1:sNbvm66/2vc8B/khyioOO8eNaU8nb89x693AN7fQheU=", + "h1:tXS4g1yE420AU4mvZ7RrYI+yYTutkRID3l+W0gBH4BM=", + "h1:vA+kES7uqmKA9K0U45IXR94jaTQZCHZLCHqMUeGxKMI=", + "h1:zV131k79+ob9p4jrLDgztDNvZvt8fvrrzpn0nPikBw8=", + "zh:006d111d6eafe6eeb5df2f91bd0ca320f979bd71f8cd8c475f10b2bd94acba55", + "zh:031fbb5cac23a841dc18e270cbfcd3ce9f4ba504edbd3c78931f7ed9827220a8", + "zh:07a72fe8b55afee99529bf4169ab6abfac5eabcd10968c29101925bcd358b09f", + "zh:0d14727d011c2d9df4c3058f527d2409223449ab48b46cbc86922eb553ef77c1", + "zh:155ce1333672d26cd18a5866b0761489d91682beffee58e45c3a1b68e8491d3d", + "zh:35a2a1939a965335b29ebdbfd759d93a97c0f589d9cd218f537dee6f600e3fb9", + "zh:52912fe421e7d911431f77788db2ea13836efd65a2e82385adb52c6a84d4ee90", + "zh:57374318d9194ea1db08884b0541a9055823d5970ad48f9a57547ac231163007", + "zh:5fb942b9e2553c058fe09fe12fb39dd175cd6715bb41c059c1a70df2bfc64dc1", + "zh:63cabd2bda201b09b35a3279d1f813ab71394b9b90fc5cf8962a5eba207803bc", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:924cd23abc326c6b3914e2cd9c94c7832c2552e1e9ae258fb9fd9aedaa5f7ce7", - "zh:a4b75e4c239879296259e7d54f1befbc7fdc16da2d62d1294e9f73add4cae61e", - "zh:a6ceb08feb63b00c7141783b31e45a154c76fd8cdebbdf371074805f0053572d", - "zh:afae1843f9ba85f2f6d94108c65cf43a457e83531a632d44d863e935160cb2ba", - "zh:bd6628ce60c778960a5755f7010b7e2cc5c6ff0341a21c175341b28058ec843d", - "zh:cd30866a1ff99d72b5fa1699db582fa4f25562e6ab21dcc6870324f3056108e0", - "zh:df5924cca691a8220aaaebb5cb55c3d6c32ff0a881f198695eff28155eb12b54", - "zh:e78d0696c941aba58df1cb36b8a0d25cd5f3963f01d9338fdbda74db58afdd49", + "zh:978ee67d3d53970a5c474ab40b00adee97f4153b16804a2b6b7ee205ae69d18a", + "zh:bbafdbef631b5c80570087817b42b16b1a76d556d692853a71c47fb48663cf00", + "zh:be91b3f2a697cbbb41f65aad2600972d0ede1e962a7d8a00bb3177cb77d86666", + "zh:efe168ad4aaa6156ce5a31d4e50e9d54d38ee5a5888412f9e690c0de5d619683", ] } diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf index 18d8ff1eb4665..74ea6d5816f8e 100644 --- a/deployment/modules/cloudflare/docs/config.tf +++ b/deployment/modules/cloudflare/docs/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.41.0" + version = "4.43.0" } } } diff --git a/deployment/modules/cloudflare/docs/domain.tf b/deployment/modules/cloudflare/docs/domain.tf index 80997c2e87176..a28fb4c0f80a9 100644 --- a/deployment/modules/cloudflare/docs/domain.tf +++ b/deployment/modules/cloudflare/docs/domain.tf @@ -18,7 +18,7 @@ output "immich_app_branch_subdomain" { } output "immich_app_branch_pages_hostname" { - value = cloudflare_record.immich_app_branch_subdomain.value + value = cloudflare_record.immich_app_branch_subdomain.content } output "pages_project_name" { diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index f42bcc0ab0a73..60685d84d6d63 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -43,6 +43,7 @@ services: ports: - 3001:3001 - 9230:9230 + - 9231:9231 depends_on: - redis - database 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/FAQ.mdx b/docs/docs/FAQ.mdx index b1a24e1788a2f..b328d3a047099 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -187,7 +187,7 @@ However, when the trash is emptied, the files will re-appear in the main timelin ### How does smart search work? -Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip). +Immich uses CLIP models. An ML model converts each image to an "embedding", which is essentially a string of numbers that semantically encodes what is in the image. The same is done for the text that you enter when you do a search, and that text embedding is then compared with those of the images to find similar ones. As such, there are no "tags", "labels", or "descriptions" generated that you can look at. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip). ### How does facial recognition work? @@ -333,7 +333,11 @@ You may need to add mount points or docker volumes for the following internal co - `immich-machine-learning:/.cache` - `redis:/data` -The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION`. +The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning. + +:::note Docker Compose Volumes +The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts. +::: For a further hardened system, you can add the following block to every container except for `immich_postgres`. diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 3d226dd0615df..860b1e1ce7426 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -21,6 +21,8 @@ The recommended way to backup and restore the Immich database is to use the `pg_ It is not recommended to directly backup the `DB_DATA_LOCATION` folder. Doing so while the database is running can lead to a corrupted backup that cannot be restored. ::: +### Manual Backup and Restore + @@ -29,10 +31,11 @@ docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgre ``` ```bash title='Restore' -docker compose down -v # CAUTION! Deletes all Immich data to start from scratch. -# rm -rf DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch. +docker compose down -v # CAUTION! Deletes all Immich data to start from scratch +## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database +# rm -rf DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch docker compose pull # Update to latest version of Immich (if desired) -docker compose create # Create Docker containers for Immich apps without running them. +docker compose create # Create Docker containers for Immich apps without running them docker start immich_postgres # Start Postgres server sleep 10 # Wait for Postgres server to start up gunzip < "/path/to/backup/dump.sql.gz" \ @@ -49,10 +52,11 @@ docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgre ``` ```powershell title='Restore' -docker compose down -v # CAUTION! Deletes all Immich data to start from scratch. -# Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch. +docker compose down -v # CAUTION! Deletes all Immich data to start from scratch +## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database +# Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch docker compose pull # Update to latest version of Immich (if desired) -docker compose create # Create Docker containers for Immich apps without running them. +docker compose create # Create Docker containers for Immich apps without running them docker start immich_postgres # Start Postgres server sleep 10 # Wait for Postgres server to start up gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup @@ -68,6 +72,8 @@ Note that for the database restore to proceed properly, it requires a completely Some deployment methods make it difficult to start the database without also starting the server or microservices. In these cases, you may set the environmental variable `DB_SKIP_MIGRATIONS=true` before starting the services. This will prevent the server from running migrations that interfere with the restore process. Note that both the server and microservices must have this variable set to prevent the migrations from running. Be sure to remove this variable and restart the services after the database is restored. ::: +### Automatic Database Backups + The database dumps can also be automated (using [this image](https://github.com/prodrigestivill/docker-postgres-backup-local)) by editing the docker compose file to match the following: ```yaml @@ -157,7 +163,7 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele - The Immich database containing all the information to allow the system to function properly. **Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version. - - Stored in `UPLOAD_LOCATION/postgres`. + - Stored in `DB_DATA_LOCATION`. :::danger A backup of this folder does not constitute a backup of your database! @@ -203,7 +209,7 @@ When you turn off the storage template engine, it will leave the assets in `UPLO - The Immich database containing all the information to allow the system to function properly. **Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version. - - Stored in `UPLOAD_LOCATION/postgres`. + - Stored in `DB_DATA_LOCATION`. :::danger A backup of this folder does not constitute a backup of your database! diff --git a/docs/docs/administration/email-notification.mdx b/docs/docs/administration/email-notification.mdx index 4a2a0b5a837f7..93b1051053069 100644 --- a/docs/docs/administration/email-notification.mdx +++ b/docs/docs/administration/email-notification.mdx @@ -8,13 +8,11 @@ Immich supports the option to send notifications via Email for the following eve ## SMTP settings -You can access the settings panel from the web at `Administration -> Settings -> Notification settings` +You can access the settings panel from the web at `Administration -> Settings -> Notification settings`. -Under Email, enter the following details to connect with SMTP servers. +Under Email, enter the required details to connect with an SMTP server. -You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server. - - +You can use [this guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server. ## User's notifications settings diff --git a/docs/docs/administration/img/email-settings.png b/docs/docs/administration/img/email-settings.png deleted file mode 100644 index a0d71354267fb..0000000000000 Binary files a/docs/docs/administration/img/email-settings.png and /dev/null differ 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 @@ + + +
Mobile App
Mobile App
Services
Services
Repositories
Repositories
Providers
Providers
Pages
Pages
Widgets
Widgets
User
User
platform
system
platform...
on-device
database
on-device...
server
server
OpenAPI
OpenAPI
UI part
UI part
non-UI part
non-UI part
Models
Models
Entities
Entities
\ No newline at end of file diff --git a/docs/docs/developer/setup.md b/docs/docs/developer/setup.md index 5f8d442aa85e8..32e79849efdcf 100644 --- a/docs/docs/developer/setup.md +++ b/docs/docs/developer/setup.md @@ -106,7 +106,7 @@ in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JS "editor.suggest.snippetsPreventQuickSuggestions": false, "editor.suggestSelection": "first", "editor.tabCompletion": "onlySnippets", - "editor.wordBasedSuggestions": false, + "editor.wordBasedSuggestions": "off", "editor.defaultFormatter": "Dart-Code.dart-code" } } diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index cdea1a11a51e1..17555469543c8 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -1,18 +1,14 @@ -# Libraries +# External Libraries -## Overview +External libraries track assets stored in the filesystem outside of Immich. When the external library is scanned, Immich will load videos and photos from disk and create the corresponding assets. These assets will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. Later, if a file is modified outside of Immich, you need to scan the library for the changes to show up. -Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries. +If an external asset is deleted from disk, Immich will move it to trash on rescan. To restore the asset, you need to restore the original file. After 30 days the file will be removed from trash, and any changes to metadata within Immich will be lost. -## External Libraries +:::caution -External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. +If you add metadata to an external asset in any way (i.e. add it to an album or edit the description), that metadata is only stored inside Immich and will not be persisted to the external asset file. If you move an asset to another location within the library all such metadata will be lost upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release. -If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case: - -- Scan Library Files: This is the default scan method and also the quickest. It will scan all files in the library and add new files to the library. It will notice if any files are missing (see below) but not check existing assets -- Scan All Library Files: Same as above, but will check each existing asset to see if the modification time has changed. If it has, the asset will be updated. Since it has to check each asset, this is slower than Scan Library Files. -- Force Scan All Library Files: Same as above, but will read each asset from disk no matter the modification time. This is useful in some cases where an asset has been modified externally but the modification time has not changed. This is the slowest way to scan because it reads each asset from disk. +::: :::caution @@ -20,22 +16,6 @@ Due to aggressive caching it can take some time for a refreshed asset to appear ::: -In external libraries, the file path is used for duplicate detection. This means that if a file is moved to a different location, it will be added as a new asset. If the file is moved back to its original location, it will be added as a new asset. In contrast to upload libraries, two identical files can be uploaded if they are in different locations. This is a deliberate design choice to make Immich reflect the file system as closely as possible. Remember that duplication detection is only done within the same library, so if you have multiple external libraries, the same file can be added to multiple libraries. - -:::caution - -If you add assets from an external library to an album and then move the asset to another location within the library, the asset will be removed from the album upon rescan. This is because the asset is considered a new asset after the move. This is a known issue and will be fixed in a future release. - -::: - -### Deleted External Assets - -Note: Either a manual or scheduled library scan must have been performed to identify offline assets before this process will work. - -In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored. - -Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich. - ### Import Paths External libraries use import paths to determine which files to scan. Each library can have multiple import paths so that files from different locations can be added to the same library. Import paths are scanned recursively, and if a file is in multiple import paths, it will only be added once. Each import file must be a readable directory that exists on the filesystem; the import path dialog will alert you of any paths that are not accessible. @@ -66,9 +46,13 @@ Some basic examples: - `**/Raw/**` will exclude all files in any directory named `Raw` - `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg` +Special characters such as @ should be escaped, for instance: + +- `**/\@eadir/**` will exclude all files in any directory named `@eadir` + ### Automatic watching (EXPERIMENTAL) -This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button. +This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes. @@ -84,7 +68,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up. ### Nightly job -There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion. +There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. ## Usage @@ -120,7 +104,7 @@ This will disallow the images from being deleted in the web UI, or adding metada _Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._ ::: -### Create External Libraries +### Create A New Library These actions must be performed by the Immich administrator. @@ -144,7 +128,7 @@ Next, we'll add an exclusion pattern to filter out raw files. - Enter `**/Raw/**` and click save. - Click save - Click the drop-down menu on the newly created library -- Click on Scan Library Files +- Click on Scan The christmas trip library will now be scanned in the background. In the meantime, let's add the videos and old photos to another library. @@ -161,7 +145,7 @@ If you get an error here, please rename the other external library to something - Click on Add Path - Enter `/mnt/media/videos` then click Add - Click Save -- Click on Scan Library Files +- Click on Scan Within seconds, the assets from the old-pics and videos folders should show up in the main timeline. diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md index b20c3fc2b6315..9f2d33cc35d7c 100644 --- a/docs/docs/features/ml-hardware-acceleration.md +++ b/docs/docs/features/ml-hardware-acceleration.md @@ -38,7 +38,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele - The GPU must have compute capability 5.2 or greater. - The server must have the official NVIDIA driver installed. -- The installed driver must be >= 545 (it must support CUDA 12.3.2). +- The installed driver must be >= 535 (it must support CUDA 12.2). - On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed. #### OpenVINO diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index 1ea068c3a0a79..6f401dfc5a5c3 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -27,7 +27,7 @@ You may use a VPN service to open an encrypted connection to your Immich instanc If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation). -:::tip Video toturial +:::tip Video tutorial You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created. ::: diff --git a/docs/docs/guides/scaling-immich.md b/docs/docs/guides/scaling-immich.md new file mode 100644 index 0000000000000..a8d916ae2a807 --- /dev/null +++ b/docs/docs/guides/scaling-immich.md @@ -0,0 +1,19 @@ +# Scaling Immich + +Immich is built with modern deployment practices in mind, and the backend is designed to be able to run multiple instances in parallel. When doing this, the only requirement you need to be aware of is that every instance needs to be connected to the shared infrastructure. That means they should all have access to the same Postgres and Redis instances, and have the same files mounted into the containers. + +Scaling can be useful for many reasons. Maybe you have a gaming PC that you want to use for transcoding and thumbnail generation, or perhaps you run a Kubernetes cluster across a handful of powerful servers that you want to make use of. + +:::info +If you only have a single machine to run Immich on, scaling to multiple containers is unlikely to provide any benefit. An Immich container will run multiple background tasks at once, and you can increase their number from the admin panel. +::: + +The details of how to scale across multiple machines will vary widely between different environments and require some knowledge to set up, and as such this guide gives no specific instructions. In some cases scaling up can be as easy as incrementing the amount of replicas on a Kubernetes deployment, in others it might need you to configure network tunnels or NFS mounts. The details are left as an exercise for the reader ;) + +## Workers + +By default, each running `immich-server` container comes with multiple internal workers. If you're scaling up only to handle more background tasks, you can choose to disable the worker responsible for the API. See [workers](../administration/jobs-workers.md) for more detail. + +## Scaling down + +In the same way you can scale up to multiple containers, you can also choose to scale down. All state is stored in Postgres, Redis, and the filesystem so there is no risk in stopping a running immich-server container, for example if you want to use your GPU to play some games. As long as there is an API worker running you will still be able to browse Immich, and jobs will wait to be processed until there is a worker available for them. diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index abbba8c6b39b5..b789d8653f168 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -20,6 +20,7 @@ The default configuration looks like this: "acceptedVideoCodecs": ["h264"], "targetAudioCodec": "aac", "acceptedAudioCodecs": ["aac", "mp3", "libopus"], + "acceptedContainers": ["mov", "ogg", "webm"], "targetResolution": "720", "maxBitrate": "0", "bframes": -1, @@ -32,7 +33,8 @@ The default configuration looks like this: "preferredHwDevice": "auto", "transcode": "required", "tonemap": "hable", - "accel": "disabled" + "accel": "disabled", + "accelDecode": false }, "job": { "backgroundTask": { @@ -60,10 +62,13 @@ The default configuration looks like this: "concurrency": 5 }, "thumbnailGeneration": { - "concurrency": 5 + "concurrency": 3 }, "videoConversion": { "concurrency": 1 + }, + "notifications": { + "concurrency": 5 } }, "logging": { @@ -78,40 +83,46 @@ The default configuration looks like this: "modelName": "ViT-B-32__openai" }, "duplicateDetection": { - "enabled": false, - "maxDistance": 0.03 + "enabled": true, + "maxDistance": 0.01 }, "facialRecognition": { "enabled": true, "modelName": "buffalo_l", "minScore": 0.7, - "maxDistance": 0.6, + "maxDistance": 0.5, "minFaces": 3 } }, "map": { "enabled": true, - "lightStyle": "", - "darkStyle": "" + "lightStyle": "https://tiles.immich.cloud/v1/style/light.json", + "darkStyle": "https://tiles.immich.cloud/v1/style/dark.json" }, "reverseGeocoding": { "enabled": true }, + "metadata": { + "faces": { + "import": false + } + }, "oauth": { - "enabled": false, - "issuerUrl": "", + "autoLaunch": false, + "autoRegister": true, + "buttonText": "Login with OAuth", "clientId": "", "clientSecret": "", + "defaultStorageQuota": 0, + "enabled": false, + "issuerUrl": "", + "mobileOverrideEnabled": false, + "mobileRedirectUri": "", "scope": "openid email profile", "signingAlgorithm": "RS256", + "profileSigningAlgorithm": "none", "storageLabelClaim": "preferred_username", - "storageQuotaClaim": "immich_quota", - "defaultStorageQuota": 0, - "buttonText": "Login with OAuth", - "autoRegister": true, - "autoLaunch": false, - "mobileOverrideEnabled": false, - "mobileRedirectUri": "" + "storageQuotaClaim": "immich_quota" }, "passwordLogin": { "enabled": true @@ -122,11 +133,16 @@ The default configuration looks like this: "template": "{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}" }, "image": { - "thumbnailFormat": "webp", - "thumbnailSize": 250, - "previewFormat": "jpeg", - "previewSize": 1440, - "quality": 80, + "thumbnail": { + "format": "webp", + "size": 250, + "quality": 80 + }, + "preview": { + "format": "jpeg", + "size": 1440, + "quality": 80 + }, "colorspace": "p3", "extractEmbedded": false }, @@ -140,23 +156,35 @@ The default configuration looks like this: "theme": { "customCss": "" }, - "user": { - "deleteDelay": 7 - }, "library": { "scan": { "enabled": true, "cronExpression": "0 0 * * *" }, "watch": { - "enabled": false, - "usePolling": false, - "interval": 10000 + "enabled": false } }, "server": { "externalDomain": "", "loginPageMessage": "" + }, + "notifications": { + "smtp": { + "enabled": false, + "from": "", + "replyTo": "", + "transport": { + "ignoreCert": false, + "host": "", + "port": 587, + "username": "", + "password": "" + } + } + }, + "user": { + "deleteDelay": 7 } } ``` diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 9ef63523a05ec..b73d51b4d240a 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -58,6 +58,7 @@ Optionally, you can enable hardware acceleration for machine learning and transc - Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. - Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication. To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. +- Set your timezone by uncommenting the `TZ=` line. ### Step 3 - Start the containers @@ -80,6 +81,10 @@ The Compose file './docker-compose.yml' is invalid because: See the previous paragraph about installing from the official docker repository. ::: +:::info Health check start interval +If you get an error `can't set healthcheck.start_interval as feature require Docker Engine v25 or later`, it helps to comment out the line for `start_interval` in the `database` section of the `docker-compose.yml` file. +::: + :::tip For more information on how to use the application, please refer to the [Post Installation](/docs/install/post-install.mdx) guide. ::: diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index a0cf71e044724..3944f6755b65a 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -27,23 +27,14 @@ If this should not work, try running `docker compose up -d --force-recreate`. These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly. ::: -### Supported filesystems - -The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group -ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32. -It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`). -If this is an issue, you can change the bind mount to a Docker volume instead. - -Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues. - ## General | Variable | Description | Default | Containers | Workers | | :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- | -| `TZ` | Timezone | | server | microservices | +| `TZ` | Timezone | \*1 | server | microservices | | `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices | | `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | -| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**\*1⚠️ | `./upload`\*2 | server | api, microservices | +| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `./upload`\*3 | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | | @@ -52,16 +43,13 @@ Regardless of filesystem, it is not recommended to use a network share for your | `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices | | `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api | -\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. - -\*2: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. -It only need to be set if the Immich deployment method is changing. - -:::tip -`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. - +\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`. `TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution. -::: + +\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead. + +\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`. +It only need to be set if the Immich deployment method is changing. ## Workers diff --git a/docs/docs/install/post-install.mdx b/docs/docs/install/post-install.mdx index ba8aa2e9a35a6..bc1ee80b47d11 100644 --- a/docs/docs/install/post-install.mdx +++ b/docs/docs/install/post-install.mdx @@ -8,6 +8,7 @@ import StorageTemplate from '/docs/partials/_storage-template.md'; import MobileAppDownload from '/docs/partials/_mobile-app-download.md'; import MobileAppLogin from '/docs/partials/_mobile-app-login.md'; import MobileAppBackup from '/docs/partials/_mobile-app-backup.md'; +import ServerBackup from '/docs/partials/_server-backup.md'; # Post Install Steps @@ -33,6 +34,10 @@ A list of common steps to take after installing Immich include: -## Step 6 - Backup Your Library +## Step 6 - Upload Your Library + +## Step 7 - Setup Server Backups + + diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 88d85c7bee8cc..b96705203aa8f 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -23,7 +23,33 @@ Immich requires the command `docker compose` - the similarly named `docker-compo - **RAM**: Minimum 4GB, recommended 6GB. - **CPU**: Minimum 2 cores, recommended 4 cores. - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions. - - This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems) - for more details and alternatives. + - This can present an issue for Windows users. See below for details and an alternative setup. - The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average. - - Network shares are supported for the storage of image and video assets only. + - Network shares are supported for the storage of image and video assets only. It is not recommended to use a network share for your database location due to performance and possible data loss issues. + +### Special requirements for Windows users + +
+Database storage on Windows systems + +The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group +ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32. +It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`). +If this is an issue, you can change the bind mount to a Docker volume instead as follows: + +Make the following change to `.env`: + +```diff +- DB_DATA_LOCATION=./postgres ++ DB_DATA_LOCATION=pgdata +``` + +Add the following line to the bottom of `docker-compose.yml`: + +```diff +volumes: + model-cache: ++ pgdata: +``` + +
diff --git a/docs/docs/partials/_server-backup.md b/docs/docs/partials/_server-backup.md new file mode 100644 index 0000000000000..7aab90a753c24 --- /dev/null +++ b/docs/docs/partials/_server-backup.md @@ -0,0 +1,2 @@ +Now that you have imported some pictures, you should setup server backups to preserve your memories. +You can do so by following our [backup guide](/docs/administration/backup-and-restore.md). 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/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index 55bb3d4cee193..1e5c724d16678 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -6,6 +6,7 @@ import { mdiLeadPencil, mdiLockOff, mdiLockOutline, + mdiMicrosoftWindows, mdiSecurity, mdiSpeedometerSlow, mdiTrashCan, @@ -21,6 +22,18 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri type Item = Omit & { date: Date }; const items: Item[] = [ + { + icon: mdiMicrosoftWindows, + iconColor: '#357EC7', + title: 'Hidden files in Windows are cursed', + description: + 'Hidden files in Windows cannot be opened with the "w" flag. That, combined with SMB option "hide dot files" leads to a lot of confusion.', + link: { + url: 'https://github.com/immich-app/immich/pull/12812', + text: '#12812', + }, + date: new Date(2024, 8, 20), + }, { icon: mdiWrap, iconColor: 'gray', diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index c16413f4c5f49..36a8fed81df1e 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,20 @@ [ + { + "label": "v1.116.2", + "url": "https://v1.116.2.archive.immich.app" + }, + { + "label": "v1.116.1", + "url": "https://v1.116.1.archive.immich.app" + }, + { + "label": "v1.116.0", + "url": "https://v1.116.0.archive.immich.app" + }, + { + "label": "v1.115.0", + "url": "https://v1.115.0.archive.immich.app" + }, { "label": "v1.114.0", "url": "https://v1.114.0.archive.immich.app" 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 97e396c09f1b0..e7b463b0b2696 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.114.0", + "version": "1.116.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.114.0", + "version": "1.116.2", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.18", + "version": "2.2.22", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -92,14 +92,14 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.114.0", + "version": "1.116.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "typescript": "^5.3.3" } }, @@ -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", @@ -1516,9 +1569,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", - "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dev": true, "license": "MIT", "dependencies": { @@ -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/package.json b/e2e/package.json index 3577bc4510a9e..7c0025902dd3a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.114.0", + "version": "1.116.2", "description": "", "main": "index.js", "type": "module", @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", 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/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index e065e60c993d8..e0281085cf1f0 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -577,6 +577,16 @@ describe('/asset', () => { expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: asset.id }); }); + it('should unlink a motion photo', async () => { + const { status, body } = await request(app) + .put(`/assets/${user1Assets[0].id}`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ livePhotoVideoId: null }); + + expect(status).toEqual(200); + expect(body).toMatchObject({ id: user1Assets[0].id, livePhotoVideoId: null }); + }); + it('should update date time original when sidecar file contains DateTimeOriginal', async () => { const sidecarData = ` diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 2f6274d1fca4d..20bd230159c28 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -1,11 +1,4 @@ -import { - LibraryResponseDto, - LoginResponseDto, - ScanLibraryDto, - getAllLibraries, - removeOfflineFiles, - scanLibrary, -} from '@immich/sdk'; +import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk'; import { cpSync, existsSync } from 'node:fs'; import { Socket } from 'socket.io-client'; import { userDto, uuidDto } from 'src/fixtures'; @@ -15,8 +8,7 @@ import request from 'supertest'; import { utimes } from 'utimes'; import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; -const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) => - scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) }); +const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) }); describe('/libraries', () => { let admin: LoginResponseDto; @@ -83,7 +75,7 @@ describe('/libraries', () => { refreshedAt: null, assetCount: 0, importPaths: [], - exclusionPatterns: [], + exclusionPatterns: expect.any(Array), }), ); }); @@ -270,7 +262,7 @@ describe('/libraries', () => { refreshedAt: null, assetCount: 0, importPaths: [], - exclusionPatterns: [], + exclusionPatterns: expect.any(Array), }), ); }); @@ -293,14 +285,19 @@ describe('/libraries', () => { expect(body).toEqual(errorDto.unauthorized); }); - it('should scan external library', async () => { + it('should import new asset when scanning external library', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp/directoryA`], }); - await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 }); + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); const { assets } = await utils.metadataSearch(admin.accessToken, { originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`, @@ -315,8 +312,13 @@ describe('/libraries', () => { exclusionPatterns: ['**/directoryA'], }); - await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 }); + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); @@ -330,8 +332,13 @@ describe('/libraries', () => { importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`], }); - await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); @@ -340,95 +347,144 @@ describe('/libraries', () => { expect(assets.items.find((asset) => asset.originalPath.includes('directoryB'))).toBeDefined(); }); - it('should pick up new files', async () => { + it('should reimport a modified file', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], }); - await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); - - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - - expect(assets.count).toBe(2); - - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); + await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 }); + await utils.waitForQueueFinish(admin.accessToken, 'library'); - const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); + await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001); - expect(newAssets.count).toBe(3); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ refreshModifiedFiles: true }); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); + + const { assets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + model: 'NIKON D750', + }); + expect(assets.count).toBe(1); }); - it('should offline a file missing from disk', async () => { - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + it('should not reimport unmodified files', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], }); + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); + await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); + await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); + + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ refreshModifiedFiles: true }); + expect(status).toBe(204); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); + + const { assets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + model: 'NIKON D750', + }); + expect(assets.count).toBe(0); + }); + + it('should set an asset offline if its file is missing', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/offline`], + }); + + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - expect(assets.count).toBe(3); + expect(assets.count).toBe(1); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); - await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); + const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); + expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`); + expect(trashedAsset.isOffline).toEqual(true); + const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - expect(newAssets.count).toBe(3); - - expect(newAssets.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - isOffline: true, - originalFileName: 'assetC.png', - }), - ]), - ); + expect(newAssets.items).toEqual([]); }); - it('should offline a file outside of import paths', async () => { + it('should set an asset offline its file is not in any import path', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], + importPaths: [`${testAssetDirInternal}/temp/offline`], }); + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(1); + + utils.createDirectory(`${testAssetDir}/temp/another-path/`); + await request(app) .put(`/libraries/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] }); + .send({ importPaths: [`${testAssetDirInternal}/temp/another-path/`] }); + + const { status } = await request(app) + .post(`/libraries/${library.id}/scan`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); - await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); + expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`); + expect(trashedAsset.isOffline).toBe(true); - expect(assets.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - isOffline: false, - originalFileName: 'assetA.png', - }), - expect.objectContaining({ - isOffline: true, - originalFileName: 'assetB.png', - }), - ]), - ); + const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(newAssets.items).toEqual([]); + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + utils.removeDirectory(`${testAssetDir}/temp/another-path/`); }); - it('should offline a file covered by an exclusion pattern', async () => { + it('should set an asset offline if its file is covered by an exclusion pattern', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], @@ -437,6 +493,12 @@ describe('/libraries', () => { await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); + const { assets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + originalFileName: 'assetB.png', + }); + expect(assets.count).toBe(1); + await request(app) .put(`/libraries/${library.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) @@ -445,282 +507,21 @@ describe('/libraries', () => { await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id); + expect(trashedAsset.isTrashed).toBe(true); + expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/directoryB/assetB.png`); + expect(trashedAsset.isOffline).toBe(true); - expect(assets.count).toBe(2); + const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - expect(assets.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - isOffline: false, - originalFileName: 'assetA.png', - }), - expect.objectContaining({ - isOffline: true, - originalFileName: 'assetB.png', - }), - ]), - ); + expect(newAssets.items).toEqual([ + expect.objectContaining({ + originalFileName: 'assetA.png', + }), + ]); }); - it('should not try to delete offline files', async () => { - utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`); - - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp/offline1`], - }); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - expect(initialAssets).toEqual({ - count: 1, - total: 1, - facets: [], - items: [expect.objectContaining({ originalFileName: 'assetA.png' })], - nextPage: null, - }); - - utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - isOffline: true, - }); - expect(offlineAssets).toEqual({ - count: 1, - total: 1, - facets: [], - items: [expect.objectContaining({ originalFileName: 'assetA.png' })], - nextPage: null, - }); - - utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`); - await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) }); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 }); - - expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true); - - utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`); - }); - - it('should scan new files', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], - }); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - - expect(assets.count).toBe(3); - expect(assets.items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - originalFileName: 'assetC.png', - }), - ]), - ); - - utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`); - }); - - describe('with refreshModifiedFiles=true', () => { - it('should reimport modified files', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], - }); - - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001); - - await scan(admin.accessToken, library.id, { refreshModifiedFiles: true }); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - - const { assets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - model: 'NIKON D750', - }); - expect(assets.count).toBe(1); - }); - - it('should not reimport unmodified files', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], - }); - - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); - - await scan(admin.accessToken, library.id, { refreshModifiedFiles: true }); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - - const { assets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - model: 'NIKON D750', - }); - expect(assets.count).toBe(0); - }); - }); - - describe('with refreshAllFiles=true', () => { - it('should reimport all files', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp`], - }); - - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`); - await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000); - - await scan(admin.accessToken, library.id, { refreshAllFiles: true }); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`); - - const { assets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - model: 'NIKON D750', - }); - expect(assets.count).toBe(1); - }); - }); - }); - - describe('POST /libraries/:id/removeOffline', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({}); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should remove offline files', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp/offline`], - }); - - utils.createImageFile(`${testAssetDir}/temp/offline/online.png`); - utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - }); - expect(initialAssets.count).toBe(2); - - utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - isOffline: true, - }); - expect(offlineAssets.count).toBe(1); - - const { status } = await request(app) - .post(`/libraries/${library.id}/removeOffline`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send(); - expect(status).toBe(204); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask'); - - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - - expect(assets.count).toBe(1); - - utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`); - }); - - it('should remove offline files from trash', async () => { - const library = await utils.createLibrary(admin.accessToken, { - ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp/offline`], - }); - - utils.createImageFile(`${testAssetDir}/temp/offline/online.png`); - utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - }); - - expect(initialAssets.count).toBe(2); - utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); - - await scan(admin.accessToken, library.id); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - - const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, { - libraryId: library.id, - isOffline: true, - }); - expect(offlineAssets.count).toBe(1); - - const { status } = await request(app) - .post(`/libraries/${library.id}/removeOffline`) - .set('Authorization', `Bearer ${admin.accessToken}`) - .send(); - expect(status).toBe(204); - await utils.waitForQueueFinish(admin.accessToken, 'library'); - await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask'); - - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - - expect(assets.count).toBe(1); - expect(assets.items[0].isOffline).toBe(false); - expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`); - - utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`); - }); - - it('should not remove online files', async () => { + it('should not trash an online asset', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], @@ -733,10 +534,11 @@ describe('/libraries', () => { expect(assetsBefore.count).toBeGreaterThan(1); const { status } = await request(app) - .post(`/libraries/${library.id}/removeOffline`) + .post(`/libraries/${library.id}/scan`) .set('Authorization', `Bearer ${admin.accessToken}`) .send(); expect(status).toBe(204); + await utils.waitForQueueFinish(admin.accessToken, 'library'); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); @@ -828,7 +630,7 @@ describe('/libraries', () => { }); await scan(admin.accessToken, library.id); - await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); + await utils.waitForQueueFinish(admin.accessToken, 'library'); const { status, body } = await request(app) .delete(`/libraries/${library.id}`) 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/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index beeaf1cc01ca5..0e5d882f80e50 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -181,7 +181,7 @@ describe('/search', () => { dto: { size: -1.5 }, expected: ['size must not be less than 1', 'size must be an integer number'], }, - ...['isArchived', 'isFavorite', 'isEncoded', 'isMotion', 'isOffline', 'isVisible'].map((value) => ({ + ...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({ should: `should reject ${value} not a boolean`, dto: { [value]: 'immich' }, expected: [`${value} must be a boolean value`], 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..0bfc0ec19be21 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -1,10 +1,13 @@ -import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk'; +import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk'; +import { existsSync } from 'node:fs'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; -import { app, asBearerAuth, utils } from 'src/utils'; +import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) }); + describe('/trash', () => { let admin: LoginResponseDto; let ws: Socket; @@ -34,13 +37,18 @@ 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 }); const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); expect(after.total).toBe(0); + + expect(existsSync(before.originalPath)).toBe(false); }); it('should empty the trash with archived assets', async () => { @@ -51,13 +59,56 @@ 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 }); const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) }); expect(after.total).toBe(0); + + expect(existsSync(before.originalPath)).toBe(false); + }); + + it('should not delete offline-trashed assets from disk', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/offline`], + }); + + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.items.length).toBe(1); + const asset = assets.items[0]; + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id); + expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true }); + + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + + const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + + await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask'); + + const assetAfter = await utils.getAssetInfo(admin.accessToken, asset.id); + expect(assetAfter).toMatchObject({ isTrashed: true, isOffline: true }); + + expect(existsSync(`${testAssetDir}/temp/offline/offline.png`)).toBe(true); + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); }); }); @@ -76,12 +127,46 @@ 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 })); }); + + it('should not restore offline-trashed assets', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/offline`], + }); + + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(1); + const assetId = assets.items[0].id; + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); + expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true })); + + const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + + const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) }); + expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true })); + }); }); describe('POST /trash/restore/assets', () => { @@ -99,14 +184,48 @@ 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); }); + + it('should not restore an offline-trashed asset', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/offline`], + }); + + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(1); + const assetId = assets.items[0].id; + + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const before = await utils.getAssetInfo(admin.accessToken, assetId); + expect(before.isTrashed).toBe(true); + + const { status } = await request(app) + .post('/trash/restore/assets') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ ids: [assetId] }); + expect(status).toBe(200); + + const after = await utils.getAssetInfo(admin.accessToken, assetId); + expect(after.isTrashed).toBe(true); + }); }); }); diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 6ca2225180de6..0148f2e1e938c 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -94,6 +94,7 @@ export const signupResponseDto = { quotaSizeInBytes: null, status: 'active', license: null, + profileChangedAt: expect.any(String), }, }; 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..e21b3bfd14934 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;`); } @@ -373,6 +372,12 @@ export const utils = { writeFileSync(path, makeRandomImage()); }, + createDirectory: (path: string) => { + if (!existsSync(dirname(path))) { + mkdirSync(dirname(path), { recursive: true }); + } + }, + removeImageFile: (path: string) => { if (!existsSync(path)) { return; @@ -381,6 +386,14 @@ export const utils = { rmSync(path); }, + removeDirectory: (path: string) => { + if (!existsSync(path)) { + return; + } + + rmSync(path); + }, + getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) => diff --git a/localizely.yml b/localizely.yml index 343464284a876..5d119fe9d8e49 100644 --- a/localizely.yml +++ b/localizely.yml @@ -52,7 +52,7 @@ download: locale_code: nb-NO - file: mobile/assets/i18n/sv-SE.json locale_code: sv-SE - - file: mobile/assets/i18n/mn.json + - file: mobile/assets/i18n/mn-MN.json locale_code: mn - file: mobile/assets/i18n/ko-KR.json locale_code: ko-KR diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index baeefbf0d8064..d982962fbcdc6 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:3cd9b520be95c671135ea1318f32be6912876024ee16d0f472669d3878801651 AS builder-cpu +FROM python:3.11-bookworm@sha256:e456ff58048f52f121025159e68bf16248c4122c8b96fadffd89331df50c9994 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:50ec89bdac0a845ec1751f91cb6187a3d8adb2b919d6e82d17acf48d1a9743fc AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:585cf0799407efc267fe1cce318322ec26e015ac1b3d77f2517d50bc3acfc232 AS prod-cpu FROM prod-cpu AS prod-openvino @@ -49,7 +49,12 @@ RUN apt-get update && \ apt-get remove wget -yqq && \ rm -rf /var/lib/apt/lists/* -FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 AS prod-cuda +FROM nvidia/cuda:12.2.2-runtime-ubuntu22.04@sha256:94c1577b2cd9dd6c0312dc04dff9cb2fdce2b268018abc3d7c2dbcacf1155000 AS prod-cuda + +RUN apt-get update && \ + apt-get install --no-install-recommends -yqq libcudnn9-cuda-12 && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3 COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11 diff --git a/machine-learning/app/models/base.py b/machine-learning/app/models/base.py index 1c019969b4790..3bbd1a02892d4 100644 --- a/machine-learning/app/models/base.py +++ b/machine-learning/app/models/base.py @@ -71,7 +71,6 @@ class InferenceModel(ABC): f"immich-app/{clean_name(self.model_name)}", cache_dir=self.cache_dir, local_dir=self.cache_dir, - local_dir_use_symlinks=False, ignore_patterns=ignore_patterns, ) diff --git a/machine-learning/app/models/facial_recognition/recognition.py b/machine-learning/app/models/facial_recognition/recognition.py index d9ceb12b6d590..c060bdd61634f 100644 --- a/machine-learning/app/models/facial_recognition/recognition.py +++ b/machine-learning/app/models/facial_recognition/recognition.py @@ -13,7 +13,6 @@ from app.config import log from app.models.base import InferenceModel from app.models.transforms import decode_cv2 from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType -from app.sessions import has_batch_axis class FaceRecognizer(InferenceModel): @@ -27,7 +26,7 @@ class FaceRecognizer(InferenceModel): def _load(self) -> ModelSession: session = self._make_session(self.model_path) - if self.batch and not has_batch_axis(session): + if self.batch and str(session.get_inputs()[0].shape[0]) != "batch": self._add_batch_axis(self.model_path) session = self._make_session(self.model_path) self.model = ArcFaceONNX( diff --git a/machine-learning/app/sessions/__init__.py b/machine-learning/app/sessions/__init__.py index e0c00ea4a0472..e69de29bb2d1d 100644 --- a/machine-learning/app/sessions/__init__.py +++ b/machine-learning/app/sessions/__init__.py @@ -1,5 +0,0 @@ -from app.schemas import ModelSession - - -def has_batch_axis(session: ModelSession) -> bool: - return not isinstance(session.get_inputs()[0].shape[0], int) or session.get_inputs()[0].shape[0] < 0 diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index 17fdb5b1fadd2..5f8e5b9e9c0f9 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -124,7 +124,6 @@ class TestBase: "immich-app/ViT-B-32__openai", cache_dir=encoder.cache_dir, local_dir=encoder.cache_dir, - local_dir_use_symlinks=False, ignore_patterns=["*.armnn"], ) @@ -136,7 +135,6 @@ class TestBase: "immich-app/ViT-B-32__openai", cache_dir=encoder.cache_dir, local_dir=encoder.cache_dir, - local_dir_use_symlinks=False, ignore_patterns=[], ) diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 85be083c3cebd..195e64ab35ad6 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:b10f75974a30a6889b03519ac48d3e1510fd13d0689468c2c443033a15d84f1b AS builder +FROM mambaorg/micromamba:bookworm-slim@sha256:e3797091302382ea841498bc93a7b0a50f7c1448333d5e946d2d1608d0c5f43d AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 43e3e0ae362f1..5bb1726378050 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -680,13 +680,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi-slim" -version = "0.112.2" +version = "0.115.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_slim-0.112.2-py3-none-any.whl", hash = "sha256:c023f74768f187af142c2fe5ff9e4ca3c4c1940bbde7df008cb283532422a23f"}, - {file = "fastapi_slim-0.112.2.tar.gz", hash = "sha256:75b8eb0c6ee05a20270da7a527ac7ad53b83414602f42b68f7027484dab3aedb"}, + {file = "fastapi_slim-0.115.0-py3-none-any.whl", hash = "sha256:27ab44da95b622e68be7a19f06df1960a320b9d94e689b0adfc055bb26ee9be7"}, + {file = "fastapi_slim-0.115.0.tar.gz", hash = "sha256:b4b962ca2aa0a31010dafdad3d4da99d368a5591223304c6fb385712fad7feb6"}, ] [package.dependencies] @@ -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] @@ -2037,22 +2037,22 @@ reference = "cuda12" [[package]] name = "onnxruntime-openvino" -version = "1.18.0" +version = "1.19.0" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime_openvino-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:565b874d21bcd48126da7d62f57db019f5ec0e1f82ae9b0740afa2ad91f8d331"}, - {file = "onnxruntime_openvino-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:7f1931060f710a6c8e32121bb73044c4772ef5925802fc8776d3fe1e87ab3f75"}, - {file = "onnxruntime_openvino-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eb1723d386f70a8e26398d983ebe35d2c25ba56e9cdb382670ebbf1f5139f8ba"}, - {file = "onnxruntime_openvino-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:874a1e263dd86674593e5a879257650b06a8609c4d5768c3d8ed8dc4ae874b9c"}, - {file = "onnxruntime_openvino-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:597eb18f3de7ead69b08a242d74c4573b28bbfba40ca2a1a40f75bf7a834808e"}, + {file = "onnxruntime_openvino-1.19.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:8c5658da819b26d9f35f95204e1bdfb74a100a7533e74edab3af6316c1e316e8"}, + {file = "onnxruntime_openvino-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:fb8de2a60cf78db6e201b0a489479995d166938e9c53b01ff342dc7f5f8251ff"}, + {file = "onnxruntime_openvino-1.19.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f3a0b954026286421b3a769c746c403e8f141f3887d1dd601beb7c4dbf77488a"}, + {file = "onnxruntime_openvino-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:12330922ecdb694ea28dbdcf08c172e47a5a84fee603040691341336ee3e42bc"}, + {file = "onnxruntime_openvino-1.19.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:be00502b1a46ba1891cbe49049033745f71c0b99df6d24b979f5b4084b9567d0"}, ] [package.dependencies] coloredlogs = "*" flatbuffers = "*" -numpy = ">=1.26.4" +numpy = ">=1.21.6" packaging = "*" protobuf = "*" sympy = "*" @@ -2473,13 +2473,13 @@ files = [ [[package]] name = "pytest" -version = "8.3.2" +version = "8.3.3" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, ] [package.dependencies] @@ -2576,18 +2576,15 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.9" +version = "0.0.10" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.9-py3-none-any.whl", hash = "sha256:97ca7b8ea7b05f977dc3849c3ba99d51689822fab725c3703af7c866a0c2b215"}, - {file = "python_multipart-0.0.9.tar.gz", hash = "sha256:03f54688c663f1b7977105f021043b0793151e4cb1c1a9d4a11fc13d622c4026"}, + {file = "python_multipart-0.0.10-py3-none-any.whl", hash = "sha256:2b06ad9e8d50c7a8db80e3b56dab590137b323410605af2be20d62a5f1ba1dc8"}, + {file = "python_multipart-0.0.10.tar.gz", hash = "sha256:46eb3c6ce6fdda5fb1a03c7e11d490e407c6930a2703fe7aef4da71c374688fa"}, ] -[package.extras] -dev = ["atomicwrites (==1.4.1)", "attrs (==23.2.0)", "coverage (==7.4.1)", "hatch", "invoke (==2.2.0)", "more-itertools (==10.2.0)", "pbr (==6.0.0)", "pluggy (==1.4.0)", "py (==1.11.0)", "pytest (==8.0.0)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.2.0)", "pyyaml (==6.0.1)", "ruff (==0.2.1)"] - [[package]] name = "pywin32" version = "306" @@ -2816,13 +2813,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.8.0" +version = "13.8.1" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, - {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, + {file = "rich-13.8.1-py3-none-any.whl", hash = "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06"}, + {file = "rich-13.8.1.tar.gz", hash = "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a"}, ] [package.dependencies] @@ -2834,29 +2831,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.3" +version = "0.6.8" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, - {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, - {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, - {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, - {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, - {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, - {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, - {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, - {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, + {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"}, + {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"}, + {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"}, + {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"}, + {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"}, + {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"}, + {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"}, + {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"}, + {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, ] [[package]] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index a69fb33a8d50e..840aa93c06453 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.114.0" +version = "1.116.2" description = "" authors = ["Hau Tran "] readme = "README.md" 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/.isar-cargo.lock b/mobile/.isar-cargo.lock new file mode 100644 index 0000000000000..a7b1dd37b9fbe --- /dev/null +++ b/mobile/.isar-cargo.lock @@ -0,0 +1,859 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "bindgen" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 1.0.109", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "cc" +version = "1.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" +dependencies = [ + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "enum_dispatch" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "float_next_after" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632" +dependencies = [ + "num-traits", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "intmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee87fd093563344074bacf24faa0bb0227fb6969fb223e922db798516de924d6" + +[[package]] +name = "isar" +version = "0.0.0" +dependencies = [ + "dirs", + "intmap", + "isar-core", + "itertools", + "jni", + "ndk-context", + "objc", + "objc-foundation", + "once_cell", + "paste", + "serde_json", + "threadpool", + "unicode-segmentation", +] + +[[package]] +name = "isar-core" +version = "0.0.0" +dependencies = [ + "byteorder", + "cfg-if", + "crossbeam-channel", + "enum_dispatch", + "float_next_after", + "intmap", + "itertools", + "libc", + "mdbx-sys", + "once_cell", + "paste", + "rand", + "serde", + "serde_json", + "snafu", + "widestring", + "xxhash-rust", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "mdbx-sys" +version = "0.0.0" +dependencies = [ + "bindgen", + "cc", + "cmake", + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "serde_json" +version = "1.0.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "widestring" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "xxhash-rust" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984" + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] 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..6a7d7a6b4df89 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -36,8 +36,73 @@ 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,migration,renderlist_generator}.dart + - test/**.dart + # refactor to make the providers and services testable + - lib/pages/common/album_asset_selection.page.dart + - lib/providers/{archive,asset,authentication,db,favorite,partner,trash,user}.provider.dart + - lib/providers/{album/album,album/shared_album,asset_viewer/asset_stack,asset_viewer/render_list,backup/backup,backup/manual_upload,search/all_motion_photos,search/recently_added_asset}.provider.dart + - lib/services/{asset,background,backup,immich_logger,sync}.service.dart + - lib/widgets/asset_grid/asset_grid_data_structure.dart + + - import_rule_openapi: + message: openapi must only be used through ApiRepositories + restrict: package:openapi + allowed: + # requried / wanted + - lib/repositories/*_api.repository.dart + # acceptable exceptions for the time being + - lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities + - lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine + - test/modules/utils/openapi_patching_test.dart # filename is self-explanatory... + # refactor + - lib/models/map/map_marker.model.dart + - lib/models/server_info/server_{config,disk_info,features,version}.model.dart + - lib/models/shared_link/shared_link.model.dart + - lib/providers/asset_viewer/asset_people.provider.dart + - lib/providers/authentication.provider.dart + - lib/providers/image/immich_remote_{image,thumbnail}_provider.dart + - lib/providers/map/map_state.provider.dart + - lib/providers/search/{search,search_filter}.provider.dart + - lib/providers/websocket.provider.dart + - lib/routing/auth_guard.dart + - lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart + - lib/widgets/album/album_thumbnail_listtile.dart + - lib/widgets/forms/login/login_form.dart + - lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart dart_code_metrics: metrics: diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index c127032b19ee2..d1f09a011f4fe 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 158, - "android.injected.version.name" => "1.114.0", + "android.injected.version.code" => 161, + "android.injected.version.name" => "1.116.2", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 324c9069fdf46..bb4f3efd267c6 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -588,5 +588,16 @@ "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" -} \ No newline at end of file + "viewer_unstack": "Un-Stack", + "downloading_media": "Downloading media", + "download_finished": "Download finished", + "download_filename": "file: {}", + "downloading": "Downloading...", + "download_complete": "Download complete", + "download_failed": "Download failed", + "download_canceled": "Download canceled", + "download_paused": "Download paused", + "download_enqueue": "Download enqueued", + "download_notfound": "Download not found", + "download_waiting_to_retry": "Waiting to retry" +} diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn-MN.json similarity index 99% rename from mobile/assets/i18n/mn.json rename to mobile/assets/i18n/mn-MN.json index cf951cea0b50b..54697af5da324 100644 --- a/mobile/assets/i18n/mn.json +++ b/mobile/assets/i18n/mn-MN.json @@ -589,4 +589,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} \ No newline at end of file +} 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/ios/Podfile.lock b/mobile/ios/Podfile.lock index 54fdc74abc54c..c1d158a5398df 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -1,4 +1,6 @@ PODS: + - background_downloader (0.0.1): + - Flutter - connectivity_plus (0.0.1): - Flutter - ReachabilitySwift @@ -101,6 +103,7 @@ PODS: - Flutter DEPENDENCIES: + - background_downloader (from `.symlinks/plugins/background_downloader/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -140,6 +143,8 @@ SPEC REPOS: - Toast EXTERNAL SOURCES: + background_downloader: + :path: ".symlinks/plugins/background_downloader/ios" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: @@ -194,6 +199,7 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: + background_downloader: 9f788ffc5de45acf87d6380e91ca0841066c18cf connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 2d8439e36a4f3..70bddbf10b997 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -401,7 +401,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 173; + CURRENT_PROJECT_VERSION = 177; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 173; + CURRENT_PROJECT_VERSION = 177; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -571,7 +571,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 173; + CURRENT_PROJECT_VERSION = 177; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index b33be9a370df7..b684804037010 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.114.0 + 1.116.1 CFBundleSignature ???? CFBundleVersion - 173 + 177 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index c1740771d98c4..8dc3676fb787a 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.114.0" + version_number: "1.116.2" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/constants/constants.dart b/mobile/lib/constants/constants.dart new file mode 100644 index 0000000000000..8b74b1a66fb2f --- /dev/null +++ b/mobile/lib/constants/constants.dart @@ -0,0 +1 @@ +const int noDbId = -9223372036854775808; // from Isar diff --git a/mobile/lib/entities/album.entity.dart b/mobile/lib/entities/album.entity.dart index c05b849dcd26d..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 @@ -164,19 +179,12 @@ class Album { } extension AssetsHelper on IsarCollection { - Future store(Album a) async { + Future store(Album a) async { await put(a); await a.owner.save(); await a.thumbnail.save(); await a.sharedUsers.save(); await a.assets.save(); + 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/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart index 23bf23604635d..8be636efb659b 100644 --- a/mobile/lib/entities/asset.entity.g.dart +++ b/mobile/lib/entities/asset.entity.g.dart @@ -57,69 +57,64 @@ const AssetSchema = CollectionSchema( name: r'isFavorite', type: IsarType.bool, ), - r'isOffline': PropertySchema( - id: 8, - name: r'isOffline', - type: IsarType.bool, - ), r'isTrashed': PropertySchema( - id: 9, + id: 8, name: r'isTrashed', type: IsarType.bool, ), r'livePhotoVideoId': PropertySchema( - id: 10, + id: 9, name: r'livePhotoVideoId', type: IsarType.string, ), r'localId': PropertySchema( - id: 11, + id: 10, name: r'localId', type: IsarType.string, ), r'ownerId': PropertySchema( - id: 12, + id: 11, name: r'ownerId', type: IsarType.long, ), r'remoteId': PropertySchema( - id: 13, + id: 12, name: r'remoteId', type: IsarType.string, ), r'stackCount': PropertySchema( - id: 14, + id: 13, name: r'stackCount', type: IsarType.long, ), r'stackId': PropertySchema( - id: 15, + id: 14, name: r'stackId', type: IsarType.string, ), r'stackPrimaryAssetId': PropertySchema( - id: 16, + id: 15, name: r'stackPrimaryAssetId', type: IsarType.string, ), r'thumbhash': PropertySchema( - id: 17, + id: 16, name: r'thumbhash', type: IsarType.string, ), r'type': PropertySchema( - id: 18, + id: 17, name: r'type', type: IsarType.byte, enumMap: _AssettypeEnumValueMap, ), r'updatedAt': PropertySchema( - id: 19, + id: 18, name: r'updatedAt', type: IsarType.dateTime, ), r'width': PropertySchema( - id: 20, + id: 19, name: r'width', type: IsarType.int, ) @@ -244,19 +239,18 @@ void _assetSerialize( writer.writeInt(offsets[5], object.height); writer.writeBool(offsets[6], object.isArchived); writer.writeBool(offsets[7], object.isFavorite); - writer.writeBool(offsets[8], object.isOffline); - writer.writeBool(offsets[9], object.isTrashed); - writer.writeString(offsets[10], object.livePhotoVideoId); - writer.writeString(offsets[11], object.localId); - writer.writeLong(offsets[12], object.ownerId); - writer.writeString(offsets[13], object.remoteId); - writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackId); - writer.writeString(offsets[16], object.stackPrimaryAssetId); - writer.writeString(offsets[17], object.thumbhash); - writer.writeByte(offsets[18], object.type.index); - writer.writeDateTime(offsets[19], object.updatedAt); - writer.writeInt(offsets[20], object.width); + writer.writeBool(offsets[8], object.isTrashed); + writer.writeString(offsets[9], object.livePhotoVideoId); + writer.writeString(offsets[10], object.localId); + writer.writeLong(offsets[11], object.ownerId); + writer.writeString(offsets[12], object.remoteId); + writer.writeLong(offsets[13], object.stackCount); + writer.writeString(offsets[14], object.stackId); + writer.writeString(offsets[15], object.stackPrimaryAssetId); + writer.writeString(offsets[16], object.thumbhash); + writer.writeByte(offsets[17], object.type.index); + writer.writeDateTime(offsets[18], object.updatedAt); + writer.writeInt(offsets[19], object.width); } Asset _assetDeserialize( @@ -275,20 +269,19 @@ Asset _assetDeserialize( id: id, isArchived: reader.readBoolOrNull(offsets[6]) ?? false, isFavorite: reader.readBoolOrNull(offsets[7]) ?? false, - isOffline: reader.readBoolOrNull(offsets[8]) ?? false, - isTrashed: reader.readBoolOrNull(offsets[9]) ?? false, - livePhotoVideoId: reader.readStringOrNull(offsets[10]), - localId: reader.readStringOrNull(offsets[11]), - ownerId: reader.readLong(offsets[12]), - remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]) ?? 0, - stackId: reader.readStringOrNull(offsets[15]), - stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), - thumbhash: reader.readStringOrNull(offsets[17]), - type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? + isTrashed: reader.readBoolOrNull(offsets[8]) ?? false, + livePhotoVideoId: reader.readStringOrNull(offsets[9]), + localId: reader.readStringOrNull(offsets[10]), + ownerId: reader.readLong(offsets[11]), + remoteId: reader.readStringOrNull(offsets[12]), + stackCount: reader.readLongOrNull(offsets[13]) ?? 0, + stackId: reader.readStringOrNull(offsets[14]), + stackPrimaryAssetId: reader.readStringOrNull(offsets[15]), + thumbhash: reader.readStringOrNull(offsets[16]), + type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? AssetType.other, - updatedAt: reader.readDateTime(offsets[19]), - width: reader.readIntOrNull(offsets[20]), + updatedAt: reader.readDateTime(offsets[18]), + width: reader.readIntOrNull(offsets[19]), ); return object; } @@ -319,29 +312,27 @@ P _assetDeserializeProp

( case 8: return (reader.readBoolOrNull(offset) ?? false) as P; case 9: - return (reader.readBoolOrNull(offset) ?? false) as P; + return (reader.readStringOrNull(offset)) as P; case 10: return (reader.readStringOrNull(offset)) as P; case 11: - return (reader.readStringOrNull(offset)) as P; - case 12: return (reader.readLong(offset)) as P; - case 13: + case 12: return (reader.readStringOrNull(offset)) as P; - case 14: + case 13: return (reader.readLongOrNull(offset) ?? 0) as P; + case 14: + return (reader.readStringOrNull(offset)) as P; case 15: return (reader.readStringOrNull(offset)) as P; case 16: return (reader.readStringOrNull(offset)) as P; case 17: - return (reader.readStringOrNull(offset)) as P; - case 18: return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? AssetType.other) as P; - case 19: + case 18: return (reader.readDateTime(offset)) as P; - case 20: + case 19: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -1362,16 +1353,6 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder isOfflineEqualTo( - bool value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'isOffline', - value: value, - )); - }); - } - QueryBuilder isTrashedEqualTo( bool value) { return QueryBuilder.apply(this, (query) { @@ -2647,18 +2628,6 @@ extension AssetQuerySortBy on QueryBuilder { }); } - QueryBuilder sortByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder sortByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - QueryBuilder sortByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isTrashed', Sort.asc); @@ -2913,18 +2882,6 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.asc); - }); - } - - QueryBuilder thenByIsOfflineDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'isOffline', Sort.desc); - }); - } - QueryBuilder thenByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'isTrashed', Sort.asc); @@ -3121,12 +3078,6 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctByIsOffline() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'isOffline'); - }); - } - QueryBuilder distinctByIsTrashed() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'isTrashed'); @@ -3263,12 +3214,6 @@ extension AssetQueryProperty on QueryBuilder { }); } - QueryBuilder isOfflineProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'isOffline'); - }); - } - QueryBuilder isTrashedProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'isTrashed'); diff --git a/mobile/lib/extensions/collection_extensions.dart b/mobile/lib/extensions/collection_extensions.dart index 769bec472b210..f71b0aacd3a7d 100644 --- a/mobile/lib/extensions/collection_extensions.dart +++ b/mobile/lib/extensions/collection_extensions.dart @@ -72,13 +72,14 @@ extension AssetListExtension on Iterable { } /// Filters out offline assets and returns those that are still accessible by the Immich server + /// TODO: isOffline is removed from Immich, so this method is not useful anymore Iterable nonOfflineOnly({ void Function()? errorCallback, }) { - final bool onlyLive = every((e) => !e.isOffline); + final bool onlyLive = every((e) => false); if (!onlyLive) { if (errorCallback != null) errorCallback(); - return where((a) => !a.isOffline); + return where((a) => false); } return this; } 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.interface.dart b/mobile/lib/interfaces/album.interface.dart new file mode 100644 index 0000000000000..c2ba650b6f407 --- /dev/null +++ b/mobile/lib/interfaces/album.interface.dart @@ -0,0 +1,21 @@ +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IAlbumRepository { + Future count({bool? local}); + Future create(Album album); + Future getById(int id); + Future getByName( + String name, { + bool? shared, + bool? remote, + }); + Future update(Album album); + Future delete(int albumId); + Future> getAll({bool? shared}); + Future removeUsers(Album album, List users); + Future addAssets(Album album, List assets); + Future removeAssets(Album album, List assets); + Future recalculateMetadata(Album album); +} 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 new file mode 100644 index 0000000000000..0d2dcfa1b5b35 --- /dev/null +++ b/mobile/lib/interfaces/asset.interface.dart @@ -0,0 +1,27 @@ +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/device_asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IAssetRepository { + Future 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> updateAll(List assets); + + Future> getMatches({ + required List assets, + required int ownerId, + bool? remote, + int limit = 100, + }); + + Future> getDeviceAssetsById(List ids); + Future upsertDeviceAssets(List deviceAssets); +} diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart new file mode 100644 index 0000000000000..fe3320c9bb101 --- /dev/null +++ b/mobile/lib/interfaces/asset_api.interface.dart @@ -0,0 +1,18 @@ +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); + + Future> search({List personIds = const []}); +} diff --git a/mobile/lib/interfaces/asset_media.interface.dart b/mobile/lib/interfaces/asset_media.interface.dart new file mode 100644 index 0000000000000..2606d5c23c518 --- /dev/null +++ b/mobile/lib/interfaces/asset_media.interface.dart @@ -0,0 +1,10 @@ +import 'package:immich_mobile/entities/asset.entity.dart'; + +abstract interface class IAssetMediaRepository { + Future> deleteAll(List ids); + + Future get(String id); + + /// Obtaining the correct original filename of the asset + Future getOriginalFilename(String id); +} diff --git a/mobile/lib/interfaces/backup.interface.dart b/mobile/lib/interfaces/backup.interface.dart new file mode 100644 index 0000000000000..e343a9d39019f --- /dev/null +++ b/mobile/lib/interfaces/backup.interface.dart @@ -0,0 +1,5 @@ +import 'package:immich_mobile/entities/backup_album.entity.dart'; + +abstract interface class IBackupRepository { + Future> getIdsBySelection(BackupSelection backup); +} diff --git a/mobile/lib/interfaces/download.interface.dart b/mobile/lib/interfaces/download.interface.dart new file mode 100644 index 0000000000000..dc4f0f57f8c44 --- /dev/null +++ b/mobile/lib/interfaces/download.interface.dart @@ -0,0 +1,14 @@ +import 'package:background_downloader/background_downloader.dart'; + +abstract interface class IDownloadRepository { + void Function(TaskStatusUpdate)? onImageDownloadStatus; + void Function(TaskStatusUpdate)? onVideoDownloadStatus; + void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus; + void Function(TaskProgressUpdate)? onTaskProgress; + + Future> getLiveVideoTasks(); + Future download(DownloadTask task); + Future cancel(String id); + Future deleteAllTrackingRecords(); + Future deleteRecordsWithIds(List 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/partner_api.interface.dart b/mobile/lib/interfaces/partner_api.interface.dart new file mode 100644 index 0000000000000..bca1baf66d251 --- /dev/null +++ b/mobile/lib/interfaces/partner_api.interface.dart @@ -0,0 +1,13 @@ +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IPartnerApiRepository { + Future> getAll(Direction direction); + Future create(String id); + Future update(String id, {required bool inTimeline}); + Future delete(String id); +} + +enum Direction { + sharedWithMe, + sharedByMe, +} diff --git a/mobile/lib/interfaces/person_api.interface.dart b/mobile/lib/interfaces/person_api.interface.dart new file mode 100644 index 0000000000000..b2fa28df8cc19 --- /dev/null +++ b/mobile/lib/interfaces/person_api.interface.dart @@ -0,0 +1,22 @@ +abstract interface class IPersonApiRepository { + Future> getAll(); + Future update(String id, {String? name}); +} + +class Person { + Person({ + required this.id, + required this.isHidden, + required this.name, + required this.thumbnailPath, + this.birthDate, + this.updatedAt, + }); + + final String id; + final DateTime? birthDate; + final bool isHidden; + final String name; + final String thumbnailPath; + final DateTime? updatedAt; +} diff --git a/mobile/lib/interfaces/user.interface.dart b/mobile/lib/interfaces/user.interface.dart new file mode 100644 index 0000000000000..828a7b2398a50 --- /dev/null +++ b/mobile/lib/interfaces/user.interface.dart @@ -0,0 +1,8 @@ +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IUserRepository { + Future> getByIds(List ids); + Future get(String id); + Future> getAll({bool self = true}); + Future update(User user); +} diff --git a/mobile/lib/interfaces/user_api.interface.dart b/mobile/lib/interfaces/user_api.interface.dart new file mode 100644 index 0000000000000..67ac3c08831be --- /dev/null +++ b/mobile/lib/interfaces/user_api.interface.dart @@ -0,0 +1,11 @@ +import 'dart:typed_data'; + +import 'package:immich_mobile/entities/user.entity.dart'; + +abstract interface class IUserApiRepository { + Future> getAll(); + Future<({String profileImagePath})> createProfileImage({ + required String name, + required Uint8List data, + }); +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index dc1df746cb964..40eda30204e01 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; @@ -9,6 +10,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/utils/download.dart'; import 'package:timezone/data/latest.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/services/background.service.dart'; @@ -72,7 +74,6 @@ Future initApp() async { var log = Logger("ImmichErrorLogger"); FlutterError.onError = (details) { - debugPrint("FlutterError - Catch all: $details"); FlutterError.presentError(details); log.severe( 'FlutterError - Catch all', @@ -82,11 +83,29 @@ Future initApp() async { }; PlatformDispatcher.instance.onError = (error, stack) { + debugPrint("FlutterError - Catch all: $error"); log.severe('PlatformDispatcher - Catch all', error, stack); return true; }; initializeTimeZones(); + + FileDownloader().configureNotification( + running: TaskNotification( + 'downloading_media'.tr(), + 'file: {filename}', + ), + complete: TaskNotification( + 'download_finished'.tr(), + 'file: {filename}', + ), + progressBar: true, + ); + + FileDownloader().trackTasksInGroup( + downloadGroupLivePhoto, + markDownloadedComplete: false, + ); } Future loadDb() async { @@ -188,8 +207,8 @@ class ImmichAppState extends ConsumerState @override Widget build(BuildContext context) { - var router = ref.watch(appRouterProvider); - var immichTheme = ref.watch(immichThemeProvider); + final router = ref.watch(appRouterProvider); + final immichTheme = ref.watch(immichThemeProvider); return MaterialApp( localizationsDelegates: context.localizationDelegates, 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/asset_viewer/asset_viewer_page_state.model.dart b/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart deleted file mode 100644 index 0a354781f81c3..0000000000000 --- a/mobile/lib/models/asset_viewer/asset_viewer_page_state.model.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:convert'; - -enum DownloadAssetStatus { idle, loading, success, error } - -class AssetViewerPageState { - // enum - final DownloadAssetStatus downloadAssetStatus; - - AssetViewerPageState({ - required this.downloadAssetStatus, - }); - - AssetViewerPageState copyWith({ - DownloadAssetStatus? downloadAssetStatus, - }) { - return AssetViewerPageState( - downloadAssetStatus: downloadAssetStatus ?? this.downloadAssetStatus, - ); - } - - Map toMap() { - final result = {}; - - result.addAll({'downloadAssetStatus': downloadAssetStatus.index}); - - return result; - } - - factory AssetViewerPageState.fromMap(Map map) { - return AssetViewerPageState( - downloadAssetStatus: - DownloadAssetStatus.values[map['downloadAssetStatus'] ?? 0], - ); - } - - String toJson() => json.encode(toMap()); - - factory AssetViewerPageState.fromJson(String source) => - AssetViewerPageState.fromMap(json.decode(source)); - - @override - String toString() => - 'ImageViewerPageState(downloadAssetStatus: $downloadAssetStatus)'; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - - return other is AssetViewerPageState && - other.downloadAssetStatus == downloadAssetStatus; - } - - @override - int get hashCode => downloadAssetStatus.hashCode; -} 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/download/download_state.model.dart b/mobile/lib/models/download/download_state.model.dart new file mode 100644 index 0000000000000..edd2fa183ec5d --- /dev/null +++ b/mobile/lib/models/download/download_state.model.dart @@ -0,0 +1,109 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:collection/collection.dart'; + +class DownloadInfo { + final String fileName; + final double progress; + // enum + final TaskStatus status; + + DownloadInfo({ + required this.fileName, + required this.progress, + required this.status, + }); + + DownloadInfo copyWith({ + String? fileName, + double? progress, + TaskStatus? status, + }) { + return DownloadInfo( + fileName: fileName ?? this.fileName, + progress: progress ?? this.progress, + status: status ?? this.status, + ); + } + + Map toMap() { + return { + 'fileName': fileName, + 'progress': progress, + 'status': status.index, + }; + } + + factory DownloadInfo.fromMap(Map map) { + return DownloadInfo( + fileName: map['fileName'] as String, + progress: map['progress'] as double, + status: TaskStatus.values[map['status'] as int], + ); + } + + String toJson() => json.encode(toMap()); + + factory DownloadInfo.fromJson(String source) => + DownloadInfo.fromMap(json.decode(source) as Map); + + @override + String toString() => + 'DownloadInfo(fileName: $fileName, progress: $progress, status: $status)'; + + @override + bool operator ==(covariant DownloadInfo other) { + if (identical(this, other)) return true; + + return other.fileName == fileName && + other.progress == progress && + other.status == status; + } + + @override + int get hashCode => fileName.hashCode ^ progress.hashCode ^ status.hashCode; +} + +class DownloadState { + // enum + final TaskStatus downloadStatus; + final Map taskProgress; + final bool showProgress; + DownloadState({ + required this.downloadStatus, + required this.taskProgress, + required this.showProgress, + }); + + DownloadState copyWith({ + TaskStatus? downloadStatus, + Map? taskProgress, + bool? showProgress, + }) { + return DownloadState( + downloadStatus: downloadStatus ?? this.downloadStatus, + taskProgress: taskProgress ?? this.taskProgress, + showProgress: showProgress ?? this.showProgress, + ); + } + + @override + String toString() => + 'DownloadState(downloadStatus: $downloadStatus, taskProgress: $taskProgress, showProgress: $showProgress)'; + + @override + bool operator ==(covariant DownloadState other) { + if (identical(this, other)) return true; + final mapEquals = const DeepCollectionEquality().equals; + + return other.downloadStatus == downloadStatus && + mapEquals(other.taskProgress, taskProgress) && + other.showProgress == showProgress; + } + + @override + int get hashCode => + downloadStatus.hashCode ^ taskProgress.hashCode ^ showProgress.hashCode; +} diff --git a/mobile/lib/models/download/livephotos_medatada.model.dart b/mobile/lib/models/download/livephotos_medatada.model.dart new file mode 100644 index 0000000000000..9c0c7ae4e95c7 --- /dev/null +++ b/mobile/lib/models/download/livephotos_medatada.model.dart @@ -0,0 +1,60 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first +import 'dart:convert'; + +enum LivePhotosPart { + video, + image, +} + +class LivePhotosMetadata { + // enum + LivePhotosPart part; + + String id; + LivePhotosMetadata({ + required this.part, + required this.id, + }); + + LivePhotosMetadata copyWith({ + LivePhotosPart? part, + String? id, + }) { + return LivePhotosMetadata( + part: part ?? this.part, + id: id ?? this.id, + ); + } + + Map toMap() { + return { + 'part': part.index, + 'id': id, + }; + } + + factory LivePhotosMetadata.fromMap(Map map) { + return LivePhotosMetadata( + part: LivePhotosPart.values[map['part'] as int], + id: map['id'] as String, + ); + } + + String toJson() => json.encode(toMap()); + + factory LivePhotosMetadata.fromJson(String source) => + LivePhotosMetadata.fromMap(json.decode(source) as Map); + + @override + String toString() => 'LivePhotosMetadata(part: $part, id: $id)'; + + @override + bool operator ==(covariant LivePhotosMetadata other) { + if (identical(this, other)) return true; + + return other.part == part && other.id == id; + } + + @override + int get hashCode => part.hashCode ^ id.hashCode; +} diff --git a/mobile/lib/models/search/search_filter.model.dart b/mobile/lib/models/search/search_filter.model.dart index 6a7c612b1563c..297a819b6a335 100644 --- a/mobile/lib/models/search/search_filter.model.dart +++ b/mobile/lib/models/search/search_filter.model.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:openapi/api.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; class SearchLocationFilter { String? country; @@ -235,7 +235,7 @@ class SearchDisplayFilters { class SearchFilter { String? context; String? filename; - Set people; + Set people; SearchLocationFilter location; SearchCameraFilter camera; SearchDateFilter date; @@ -258,7 +258,7 @@ class SearchFilter { SearchFilter copyWith({ String? context, String? filename, - Set? people, + Set? people, SearchLocationFilter? location, SearchCameraFilter? camera, SearchDateFilter? date, 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/common/download_panel.dart b/mobile/lib/pages/common/download_panel.dart new file mode 100644 index 0000000000000..95cefd742af0a --- /dev/null +++ b/mobile/lib/pages/common/download_panel.dart @@ -0,0 +1,150 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:easy_localization/easy_localization.dart'; +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/asset_viewer/download.provider.dart'; + +class DownloadPanel extends ConsumerWidget { + const DownloadPanel({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final showProgress = ref.watch( + downloadStateProvider.select((state) => state.showProgress), + ); + + final tasks = ref + .watch( + downloadStateProvider.select((state) => state.taskProgress), + ) + .entries + .toList(); + + onCancelDownload(String id) { + ref.watch(downloadStateProvider.notifier).cancelDownload(id); + } + + return Positioned( + bottom: 140, + left: 16, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: showProgress + ? ConstrainedBox( + constraints: + BoxConstraints.loose(Size(context.width - 32, 300)), + child: ListView.builder( + shrinkWrap: true, + itemCount: tasks.length, + itemBuilder: (context, index) { + final task = tasks[index]; + return DownloadTaskTile( + progress: task.value.progress, + fileName: task.value.fileName, + status: task.value.status, + onCancelDownload: () => onCancelDownload(task.key), + ); + }, + ), + ) + : const SizedBox.shrink(key: ValueKey('no_progress')), + ), + ); + } +} + +class DownloadTaskTile extends StatelessWidget { + final double progress; + final String fileName; + final TaskStatus status; + final VoidCallback onCancelDownload; + + const DownloadTaskTile({ + super.key, + required this.progress, + required this.fileName, + required this.status, + required this.onCancelDownload, + }); + + @override + Widget build(BuildContext context) { + final progressPercent = (progress * 100).round(); + + getStatusText() { + switch (status) { + case TaskStatus.running: + return 'downloading'.tr(); + case TaskStatus.complete: + return 'download_complete'.tr(); + case TaskStatus.failed: + return 'download_failed'.tr(); + case TaskStatus.canceled: + return 'download_canceled'.tr(); + case TaskStatus.paused: + return 'download_paused'.tr(); + case TaskStatus.enqueued: + return 'download_enqueue'.tr(); + case TaskStatus.notFound: + return 'download_notfound'.tr(); + case TaskStatus.waitingToRetry: + return 'download_waiting_to_retry'.tr(); + } + } + + return SizedBox( + key: const ValueKey('download_progress'), + width: MediaQuery.of(context).size.width - 32, + child: Card( + clipBehavior: Clip.antiAlias, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: ListTile( + minVerticalPadding: 18, + leading: const Icon(Icons.video_file_outlined), + title: Text( + getStatusText(), + style: context.textTheme.labelLarge, + ), + trailing: IconButton( + icon: Icon(Icons.close, color: context.colorScheme.onError), + onPressed: onCancelDownload, + style: ElevatedButton.styleFrom( + backgroundColor: context.colorScheme.error.withAlpha(200), + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + fileName, + style: context.textTheme.labelMedium, + ), + Row( + children: [ + Expanded( + child: LinearProgressIndicator( + minHeight: 8.0, + value: progress, + borderRadius: + const BorderRadius.all(Radius.circular(10.0)), + ), + ), + const SizedBox(width: 8), + Text( + '$progressPercent%', + style: context.textTheme.labelSmall, + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index fdaa4a0ad2e8d..4a4f2f2939960 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -8,9 +8,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/pages/common/native_video_viewer.page.dart'; +import 'package:immich_mobile/pages/common/download_panel.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; @@ -30,7 +32,6 @@ import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart'; import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart'; import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart'; -import 'package:isar/isar.dart'; @RoutePage() // ignore: must_be_immutable @@ -72,7 +73,7 @@ class GalleryViewerPage extends HookConsumerWidget { : []; final stackElements = showStack ? [currentAsset, ...stack] : []; // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = currentAsset.id == Isar.autoIncrement; + final isFromDto = currentAsset.id == noDbId; Asset asset = stackIndex.value == -1 ? currentAsset @@ -420,6 +421,7 @@ class GalleryViewerPage extends HookConsumerWidget { ], ), ), + const DownloadPanel(), ], ), ), diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart index 729b59ded5911..8bfb8c8bb9bf9 100644 --- a/mobile/lib/pages/editing/crop.page.dart +++ b/mobile/lib/pages/editing/crop.page.dart @@ -51,107 +51,109 @@ class CropImagePage extends HookWidget { ], ), backgroundColor: context.scaffoldBackgroundColor, - body: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Column( - children: [ - Container( - padding: const EdgeInsets.only(top: 20), - width: constraints.maxWidth * 0.9, - height: constraints.maxHeight * 0.6, - child: CropImage( - controller: cropController, - image: image, - gridColor: Colors.white, - ), - ), - Expanded( - child: Container( - width: double.infinity, - decoration: BoxDecoration( - color: context.scaffoldBackgroundColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), + body: SafeArea( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20), + width: constraints.maxWidth * 0.9, + height: constraints.maxHeight * 0.6, + child: CropImage( + controller: cropController, + image: image, + gridColor: Colors.white, ), - child: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only( - left: 20, - right: 20, - bottom: 10, + ), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: context.scaffoldBackgroundColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 20, + right: 20, + bottom: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.rotate_left, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.rotateLeft(); + }, + ), + IconButton( + icon: Icon( + Icons.rotate_right, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.rotateRight(); + }, + ), + ], + ), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - icon: Icon( - Icons.rotate_left, - color: Theme.of(context).iconTheme.color, - ), - onPressed: () { - cropController.rotateLeft(); - }, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: null, + label: 'Free', ), - IconButton( - icon: Icon( - Icons.rotate_right, - color: Theme.of(context).iconTheme.color, - ), - onPressed: () { - cropController.rotateRight(); - }, + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 1.0, + label: '1:1', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 16.0 / 9.0, + label: '16:9', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 3.0 / 2.0, + label: '3:2', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 7.0 / 5.0, + label: '7:5', ), ], ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: null, - label: 'Free', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 1.0, - label: '1:1', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 16.0 / 9.0, - label: '16:9', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 3.0 / 2.0, - label: '3:2', - ), - _AspectRatioButton( - cropController: cropController, - aspectRatio: aspectRatio, - ratio: 7.0 / 5.0, - label: '7:5', - ), - ], - ), - ], + ], + ), ), ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), ); } 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/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index acabc75aa4950..2ca2a379180dd 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; @@ -19,7 +20,6 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart'; import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart'; import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart'; -import 'package:openapi/api.dart'; @RoutePage() class SearchInputPage extends HookConsumerWidget { @@ -110,7 +110,7 @@ class SearchInputPage extends HookConsumerWidget { } showPeoplePicker() { - handleOnSelect(Set value) { + handleOnSelect(Set value) { filter.value = filter.value.copyWith( people: value, ); 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_service.provider.g.dart b/mobile/lib/providers/activity_service.provider.g.dart index 8e5ef43260119..d42b2a39e45f7 100644 --- a/mobile/lib/providers/activity_service.provider.g.dart +++ b/mobile/lib/providers/activity_service.provider.g.dart @@ -6,7 +6,7 @@ part of 'activity_service.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0'; +String _$activityServiceHash() => r'23a3ee7db71676d2719daa64217a683cc5c7eab0'; /// See also [activityService]. @ProviderFor(activityService) 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/activity_statistics.provider.g.dart b/mobile/lib/providers/activity_statistics.provider.g.dart index 79856c525b77c..16a3c0e81b374 100644 --- a/mobile/lib/providers/activity_statistics.provider.g.dart +++ b/mobile/lib/providers/activity_statistics.provider.g.dart @@ -7,7 +7,7 @@ part of 'activity_statistics.provider.dart'; // ************************************************************************** String _$activityStatisticsHash() => - r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf'; + r'1f43f0bcb11c754ca3cb586a13570db25023b9a8'; /// Copied from Dart SDK class _SystemHash { diff --git a/mobile/lib/providers/album/suggested_shared_users.provider.dart b/mobile/lib/providers/album/suggested_shared_users.provider.dart index 77518f47d0c04..fe8a1fccce861 100644 --- a/mobile/lib/providers/album/suggested_shared_users.provider.dart +++ b/mobile/lib/providers/album/suggested_shared_users.provider.dart @@ -5,5 +5,5 @@ import 'package:immich_mobile/services/user.service.dart'; final otherUsersProvider = FutureProvider.autoDispose>((ref) { UserService userService = ref.watch(userServiceProvider); - return userService.getUsersInDb(); + return userService.getUsers(); }); diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 938961efb62ac..5561d3fefd683 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -86,12 +86,16 @@ class AppLifeCycleNotifier extends StateNotifier { void handleAppPause() { state = AppLifeCycleEnum.paused; _wasPaused = true; - // Do not cancel backup if manual upload is in progress - if (_ref.read(backupProvider.notifier).backupProgress != - BackUpProgressEnum.manualInProgress) { - _ref.read(backupProvider.notifier).cancelBackup(); + + if (_ref.read(authenticationProvider).isAuthenticated) { + // Do not cancel backup if manual upload is in progress + if (_ref.read(backupProvider.notifier).backupProgress != + BackUpProgressEnum.manualInProgress) { + _ref.read(backupProvider.notifier).cancelBackup(); + } + _ref.read(websocketProvider.notifier).disconnect(); } - _ref.read(websocketProvider.notifier).disconnect(); + ImmichLogger().flush(); } 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/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart new file mode 100644 index 0000000000000..d4aa2823b5b2f --- /dev/null +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -0,0 +1,191 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/download/download_state.model.dart'; +import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; +import 'package:immich_mobile/services/download.service.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/services/share.service.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/widgets/common/share_dialog.dart'; + +class DownloadStateNotifier extends StateNotifier { + final DownloadService _downloadService; + final ShareService _shareService; + + DownloadStateNotifier( + this._downloadService, + this._shareService, + ) : super( + DownloadState( + downloadStatus: TaskStatus.complete, + showProgress: false, + taskProgress: {}, + ), + ) { + _downloadService.onImageDownloadStatus = _downloadImageCallback; + _downloadService.onVideoDownloadStatus = _downloadVideoCallback; + _downloadService.onLivePhotoDownloadStatus = _downloadLivePhotoCallback; + _downloadService.onTaskProgress = _taskProgressCallback; + } + + void _updateDownloadStatus(String taskId, TaskStatus status) { + if (status == TaskStatus.canceled) { + return; + } + + state = state.copyWith( + taskProgress: {} + ..addAll(state.taskProgress) + ..addAll({ + taskId: DownloadInfo( + progress: state.taskProgress[taskId]?.progress ?? 0, + fileName: state.taskProgress[taskId]?.fileName ?? '', + status: status, + ), + }), + ); + } + + // Download live photo callback + void _downloadLivePhotoCallback(TaskStatusUpdate update) { + _updateDownloadStatus(update.task.taskId, update.status); + + switch (update.status) { + case TaskStatus.complete: + if (update.task.metaData.isEmpty) { + return; + } + final livePhotosId = + LivePhotosMetadata.fromJson(update.task.metaData).id; + _downloadService.saveLivePhotos(update.task, livePhotosId); + _onDownloadComplete(update.task.taskId); + break; + + default: + break; + } + } + + // Download image callback + void _downloadImageCallback(TaskStatusUpdate update) { + _updateDownloadStatus(update.task.taskId, update.status); + + switch (update.status) { + case TaskStatus.complete: + _downloadService.saveImage(update.task); + _onDownloadComplete(update.task.taskId); + break; + + default: + break; + } + } + + // Download video callback + void _downloadVideoCallback(TaskStatusUpdate update) { + _updateDownloadStatus(update.task.taskId, update.status); + + switch (update.status) { + case TaskStatus.complete: + _downloadService.saveVideo(update.task); + _onDownloadComplete(update.task.taskId); + break; + + default: + break; + } + } + + void _taskProgressCallback(TaskProgressUpdate update) { + // Ignore if the task is cancled or completed + if (update.progress == -2 || update.progress == -1) { + return; + } + + state = state.copyWith( + showProgress: true, + taskProgress: {} + ..addAll(state.taskProgress) + ..addAll({ + update.task.taskId: DownloadInfo( + progress: update.progress, + fileName: update.task.filename, + status: TaskStatus.running, + ), + }), + ); + } + + void _onDownloadComplete(String id) { + Future.delayed(const Duration(seconds: 2), () { + state = state.copyWith( + taskProgress: {} + ..addAll(state.taskProgress) + ..remove(id), + ); + + if (state.taskProgress.isEmpty) { + state = state.copyWith( + showProgress: false, + ); + } + }); + } + + void downloadAsset(Asset asset, BuildContext context) async { + await _downloadService.download(asset); + } + + void cancelDownload(String id) async { + final isCanceled = await _downloadService.cancelDownload(id); + + if (isCanceled) { + state = state.copyWith( + taskProgress: {} + ..addAll(state.taskProgress) + ..remove(id), + ); + } + + if (state.taskProgress.isEmpty) { + state = state.copyWith( + showProgress: false, + ); + } + } + + void shareAsset(Asset asset, BuildContext context) async { + showDialog( + context: context, + builder: (BuildContext buildContext) { + _shareService.shareAsset(asset, context).then( + (bool status) { + if (!status) { + ImmichToast.show( + context: context, + msg: 'image_viewer_page_state_provider_share_error'.tr(), + toastType: ToastType.error, + gravity: ToastGravity.BOTTOM, + ); + } + buildContext.pop(); + }, + ); + return const ShareDialog(); + }, + barrierDismissible: false, + ); + } +} + +final downloadStateProvider = + StateNotifierProvider( + ((ref) => DownloadStateNotifier( + ref.watch(downloadServiceProvider), + ref.watch(shareServiceProvider), + )), +); diff --git a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart deleted file mode 100644 index 631011f200bbd..0000000000000 --- a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:fluttertoast/fluttertoast.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/models/asset_viewer/asset_viewer_page_state.model.dart'; -import 'package:immich_mobile/services/image_viewer.service.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/services/share.service.dart'; -import 'package:immich_mobile/widgets/common/immich_toast.dart'; -import 'package:immich_mobile/widgets/common/share_dialog.dart'; - -class ImageViewerStateNotifier extends StateNotifier { - final ImageViewerService _imageViewerService; - final ShareService _shareService; - final AlbumService _albumService; - - ImageViewerStateNotifier( - this._imageViewerService, - this._shareService, - this._albumService, - ) : super( - AssetViewerPageState( - downloadAssetStatus: DownloadAssetStatus.idle, - ), - ); - - void downloadAsset(Asset asset, BuildContext context) async { - state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.loading); - - ImmichToast.show( - context: context, - msg: 'download_started'.tr(), - toastType: ToastType.info, - gravity: ToastGravity.BOTTOM, - ); - - bool isSuccess = await _imageViewerService.downloadAsset(asset); - - if (isSuccess) { - state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success); - - ImmichToast.show( - context: context, - msg: Platform.isAndroid - ? 'download_sucess_android'.tr() - : 'download_sucess'.tr(), - toastType: ToastType.success, - gravity: ToastGravity.BOTTOM, - ); - _albumService.refreshDeviceAlbums(); - } else { - state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error); - ImmichToast.show( - context: context, - msg: 'download_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - - state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.idle); - } - - void shareAsset(Asset asset, BuildContext context) async { - showDialog( - context: context, - builder: (BuildContext buildContext) { - _shareService.shareAsset(asset, context).then( - (bool status) { - if (!status) { - ImmichToast.show( - context: context, - msg: 'image_viewer_page_state_provider_share_error'.tr(), - toastType: ToastType.error, - gravity: ToastGravity.BOTTOM, - ); - } - buildContext.pop(); - }, - ); - return const ShareDialog(); - }, - barrierDismissible: false, - ); - } -} - -final imageViewerStateProvider = - StateNotifierProvider( - ((ref) => ImageViewerStateNotifier( - ref.watch(imageViewerServiceProvider), - ref.watch(shareServiceProvider), - ref.watch(albumServiceProvider), - )), -); 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/map/map_state.provider.g.dart b/mobile/lib/providers/map/map_state.provider.g.dart index eff7b4b68e60f..23a570d1c8789 100644 --- a/mobile/lib/providers/map/map_state.provider.g.dart +++ b/mobile/lib/providers/map/map_state.provider.g.dart @@ -6,7 +6,7 @@ part of 'map_state.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$mapStateNotifierHash() => r'31fafe17aa85c48379a22ed3db3cc94af59ce5b8'; +String _$mapStateNotifierHash() => r'22e4e571bd0730dbc34b109255a62b920e9c7d66'; /// See also [MapStateNotifier]. @ProviderFor(MapStateNotifier) diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index e2c243354b536..7c956f0a37b52 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -1,14 +1,14 @@ +import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/services/person.service.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'people.provider.g.dart'; @riverpod -Future> getAllPeople( +Future> getAllPeople( GetAllPeopleRef ref, ) async { final PersonService personService = ref.read(personServiceProvider); diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index db2edfb9567aa..c5ff6287cd7a8 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -6,12 +6,11 @@ part of 'people.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$getAllPeopleHash() => r'4eff6666be5a74710d1e8587e01d8154310d85bd'; +String _$getAllPeopleHash() => r'3417b7e0c211382d4480a415e352139995d57b6d'; /// See also [getAllPeople]. @ProviderFor(getAllPeople) -final getAllPeopleProvider = - AutoDisposeFutureProvider>.internal( +final getAllPeopleProvider = AutoDisposeFutureProvider>.internal( getAllPeople, name: r'getAllPeopleProvider', debugGetCreateSourceHash: @@ -20,7 +19,7 @@ final getAllPeopleProvider = allTransitiveDependencies: null, ); -typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; +typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832'; /// Copied from Dart SDK 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.repository.dart b/mobile/lib/repositories/album.repository.dart new file mode 100644 index 0000000000000..08c939aa6ca87 --- /dev/null +++ b/mobile/lib/repositories/album.repository.dart @@ -0,0 +1,85 @@ +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.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final albumRepositoryProvider = + Provider((ref) => AlbumRepository(ref.watch(dbProvider))); + +class AlbumRepository implements IAlbumRepository { + final Isar _db; + + AlbumRepository( + this._db, + ); + + @override + Future count({bool? local}) { + if (local == true) return _db.albums.where().localIdIsNotNull().count(); + if (local == false) return _db.albums.where().remoteIdIsNotNull().count(); + return _db.albums.count(); + } + + @override + Future create(Album album) => + _db.writeTxn(() => _db.albums.store(album)); + + @override + Future getByName(String name, {bool? shared, bool? remote}) { + var query = _db.albums.filter().nameEqualTo(name); + if (shared != null) { + query = query.sharedEqualTo(shared); + } + if (remote == true) { + query = query.localIdIsNull(); + } else if (remote == false) { + query = query.remoteIdIsNull(); + } + return query.findFirst(); + } + + @override + Future update(Album album) => + _db.writeTxn(() => _db.albums.store(album)); + + @override + Future delete(int albumId) => + _db.writeTxn(() => _db.albums.delete(albumId)); + + @override + Future> getAll({bool? shared}) { + final baseQuery = _db.albums.filter(); + QueryBuilder? query; + if (shared != null) { + query = baseQuery.sharedEqualTo(true); + } + return query?.findAll() ?? _db.albums.where().findAll(); + } + + @override + Future getById(int id) => _db.albums.get(id); + + @override + Future removeUsers(Album album, List users) => + _db.writeTxn(() => album.sharedUsers.update(unlink: users)); + + @override + Future addAssets(Album album, List assets) => + _db.writeTxn(() => album.assets.update(link: assets)); + + @override + Future removeAssets(Album album, List assets) => + _db.writeTxn(() => album.assets.update(unlink: assets)); + + @override + Future recalculateMetadata(Album album) async { + album.startDate = await album.assets.filter().fileCreatedAtProperty().min(); + album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); + album.lastModifiedAssetTimestamp = + await album.assets.filter().updatedAtProperty().max(); + return album; + } +} 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 new file mode 100644 index 0000000000000..087344302a417 --- /dev/null +++ b/mobile/lib/repositories/asset.repository.dart @@ -0,0 +1,143 @@ +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/entities/android_device_asset.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/device_asset.entity.dart'; +import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final assetRepositoryProvider = + Provider((ref) => AssetRepository(ref.watch(dbProvider))); + +class AssetRepository implements IAssetRepository { + final Isar _db; + + AssetRepository( + this._db, + ); + + @override + Future> getByAlbum(Album album, {User? notOwnedBy}) { + var query = album.assets.filter(); + if (notOwnedBy != null) { + query = query.not().ownerIdEqualTo(notOwnedBy.isarId); + } + return query.findAll(); + } + + @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> updateAll(List assets) async { + await _db.writeTxn(() => _db.assets.putAll(assets)); + return assets; + } + + @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); + } + + @override + Future> getDeviceAssetsById(List ids) => + Platform.isAndroid + ? _db.androidDeviceAssets.getAll(ids.cast()) + : _db.iOSDeviceAssets.getAllById(ids.cast()); + + @override + Future upsertDeviceAssets(List deviceAssets) => + _db.writeTxn( + () => Platform.isAndroid + ? _db.androidDeviceAssets.putAll(deviceAssets.cast()) + : _db.iOSDeviceAssets.putAll(deviceAssets.cast()), + ); +} + +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..eb796f6c6b5d2 --- /dev/null +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -0,0 +1,52 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final assetApiRepositoryProvider = Provider( + (ref) => AssetApiRepository( + ref.watch(apiServiceProvider).assetsApi, + ref.watch(apiServiceProvider).searchApi, + ), +); + +class AssetApiRepository extends BaseApiRepository + implements IAssetApiRepository { + final AssetsApi _api; + final SearchApi _searchApi; + + AssetApiRepository(this._api, this._searchApi); + + @override + Future update(String id, {String? description}) async { + final response = await checkNull( + _api.updateAsset(id, UpdateAssetDto(description: description)), + ); + return Asset.remote(response); + } + + @override + Future> search({List personIds = const []}) async { + // TODO this always fetches all assets, change API and usage to actually do pagination + final List result = []; + bool hasNext = true; + int currentPage = 1; + while (hasNext) { + final response = await checkNull( + _searchApi.searchMetadata( + MetadataSearchDto( + personIds: personIds, + page: currentPage, + size: 1000, + ), + ), + ); + result.addAll(response.assets.items.map(Asset.remote)); + hasNext = response.assets.nextPage != null; + currentPage++; + } + return result; + } +} diff --git a/mobile/lib/repositories/asset_media.repository.dart b/mobile/lib/repositories/asset_media.repository.dart new file mode 100644 index 0000000000000..68fffa08a6fcb --- /dev/null +++ b/mobile/lib/repositories/asset_media.repository.dart @@ -0,0 +1,59 @@ +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; + } + + @override + Future getOriginalFilename(String id) async { + final entity = await AssetEntity.fromId(id); + + if (entity == null) { + return null; + } + + // titleAsync gets the correct original filename for some assets on iOS + // otherwise using the `entity.title` would return a random GUID + return await entity.titleAsync; + } +} diff --git a/mobile/lib/repositories/backup.repository.dart b/mobile/lib/repositories/backup.repository.dart new file mode 100644 index 0000000000000..c9d93f787769b --- /dev/null +++ b/mobile/lib/repositories/backup.repository.dart @@ -0,0 +1,20 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final backupRepositoryProvider = + Provider((ref) => BackupRepository(ref.watch(dbProvider))); + +class BackupRepository implements IBackupRepository { + final Isar _db; + + BackupRepository( + this._db, + ); + + @override + Future> getIdsBySelection(BackupSelection backup) => + _db.backupAlbums.filter().selectionEqualTo(backup).idProperty().findAll(); +} 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/download.repository.dart b/mobile/lib/repositories/download.repository.dart new file mode 100644 index 0000000000000..5b42f66b02148 --- /dev/null +++ b/mobile/lib/repositories/download.repository.dart @@ -0,0 +1,68 @@ +import 'package:background_downloader/background_downloader.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/download.interface.dart'; +import 'package:immich_mobile/utils/download.dart'; + +final downloadRepositoryProvider = Provider((ref) => DownloadRepository()); + +class DownloadRepository implements IDownloadRepository { + @override + void Function(TaskStatusUpdate)? onImageDownloadStatus; + + @override + void Function(TaskStatusUpdate)? onVideoDownloadStatus; + + @override + void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus; + + @override + void Function(TaskProgressUpdate)? onTaskProgress; + + DownloadRepository() { + FileDownloader().registerCallbacks( + group: downloadGroupImage, + taskStatusCallback: (update) => onImageDownloadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + + FileDownloader().registerCallbacks( + group: downloadGroupVideo, + taskStatusCallback: (update) => onVideoDownloadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + + FileDownloader().registerCallbacks( + group: downloadGroupLivePhoto, + taskStatusCallback: (update) => onLivePhotoDownloadStatus?.call(update), + taskProgressCallback: (update) => onTaskProgress?.call(update), + ); + } + + @override + Future download(DownloadTask task) { + return FileDownloader().enqueue(task); + } + + @override + Future deleteAllTrackingRecords() { + return FileDownloader().database.deleteAllRecords(); + } + + @override + Future cancel(String id) { + return FileDownloader().cancelTaskWithId(id); + } + + @override + Future> getLiveVideoTasks() { + return FileDownloader().database.allRecordsWithStatus( + TaskStatus.complete, + group: downloadGroupLivePhoto, + ); + } + + @override + Future deleteRecordsWithIds(List ids) { + return FileDownloader().database.deleteRecordsWithIds(ids); + } +} 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/partner_api.repository.dart b/mobile/lib/repositories/partner_api.repository.dart new file mode 100644 index 0000000000000..3419a2bc77244 --- /dev/null +++ b/mobile/lib/repositories/partner_api.repository.dart @@ -0,0 +1,51 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final partnerApiRepositoryProvider = Provider( + (ref) => PartnerApiRepository( + ref.watch(apiServiceProvider).partnersApi, + ), +); + +class PartnerApiRepository extends BaseApiRepository + implements IPartnerApiRepository { + final PartnersApi _api; + + PartnerApiRepository(this._api); + + @override + Future> getAll(Direction direction) async { + final response = await checkNull( + _api.getPartners( + direction == Direction.sharedByMe + ? PartnerDirection.by + : PartnerDirection.with_, + ), + ); + return response.map(User.fromPartnerDto).toList(); + } + + @override + Future create(String id) async { + final dto = await checkNull(_api.createPartner(id)); + return User.fromPartnerDto(dto); + } + + @override + Future delete(String id) => checkNull(_api.removePartner(id)); + + @override + Future update(String id, {required bool inTimeline}) async { + final dto = await checkNull( + _api.updatePartner( + id, + UpdatePartnerDto(inTimeline: inTimeline), + ), + ); + return User.fromPartnerDto(dto); + } +} diff --git a/mobile/lib/repositories/person_api.repository.dart b/mobile/lib/repositories/person_api.repository.dart new file mode 100644 index 0000000000000..8071c33dc2cea --- /dev/null +++ b/mobile/lib/repositories/person_api.repository.dart @@ -0,0 +1,38 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final personApiRepositoryProvider = Provider( + (ref) => PersonApiRepository(ref.watch(apiServiceProvider).peopleApi), +); + +class PersonApiRepository extends BaseApiRepository + implements IPersonApiRepository { + final PeopleApi _api; + + PersonApiRepository(this._api); + + @override + Future> getAll() async { + final dto = await checkNull(_api.getAllPeople()); + return dto.people.map(_toPerson).toList(); + } + + @override + Future update(String id, {String? name}) async { + final dto = await checkNull( + _api.updatePerson(id, PersonUpdateDto(name: name)), + ); + return _toPerson(dto); + } + + static Person _toPerson(PersonResponseDto dto) => Person( + birthDate: dto.birthDate, + id: dto.id, + isHidden: dto.isHidden, + name: dto.name, + thumbnailPath: dto.thumbnailPath, + ); +} diff --git a/mobile/lib/repositories/user.repository.dart b/mobile/lib/repositories/user.repository.dart new file mode 100644 index 0000000000000..796b1f421b863 --- /dev/null +++ b/mobile/lib/repositories/user.repository.dart @@ -0,0 +1,39 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +final userRepositoryProvider = + Provider((ref) => UserRepository(ref.watch(dbProvider))); + +class UserRepository implements IUserRepository { + final Isar _db; + + UserRepository( + this._db, + ); + + @override + Future> getByIds(List ids) async => + (await _db.users.getAllById(ids)).cast(); + + @override + Future get(String id) => _db.users.getById(id); + + @override + Future> getAll({bool self = true}) { + if (self) { + return _db.users.where().findAll(); + } + final int userId = Store.get(StoreKey.currentUser).isarId; + return _db.users.where().isarIdNotEqualTo(userId).findAll(); + } + + @override + Future update(User user) async { + await _db.writeTxn(() => _db.users.put(user)); + return user; + } +} diff --git a/mobile/lib/repositories/user_api.repository.dart b/mobile/lib/repositories/user_api.repository.dart new file mode 100644 index 0000000000000..ffc50ae4c3e6c --- /dev/null +++ b/mobile/lib/repositories/user_api.repository.dart @@ -0,0 +1,41 @@ +import 'dart:typed_data'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:http/http.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/user_api.interface.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/base_api.repository.dart'; +import 'package:openapi/api.dart'; + +final userApiRepositoryProvider = Provider( + (ref) => UserApiRepository( + ref.watch(apiServiceProvider).usersApi, + ), +); + +class UserApiRepository extends BaseApiRepository + implements IUserApiRepository { + final UsersApi _api; + + UserApiRepository(this._api); + + @override + Future> getAll() async { + final dto = await checkNull(_api.searchUsers()); + return dto.map(User.fromSimpleUserDto).toList(); + } + + @override + Future<({String profileImagePath})> createProfileImage({ + required String name, + required Uint8List data, + }) async { + final response = await checkNull( + _api.createProfileImage( + MultipartFile.fromBytes('file', data, filename: name), + ), + ); + return (profileImagePath: response.profileImagePath); + } +} 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 ef56f9bf6c12a..dd021e698e094 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -5,54 +5,64 @@ import 'dart:io'; 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/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/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/repositories/album.repository.dart'; +import 'package:immich_mobile/repositories/album_api.repository.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/album_media.repository.dart'; +import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; -import 'package:isar/isar.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(dbProvider), + ref.watch(entityServiceProvider), + ref.watch(albumRepositoryProvider), + ref.watch(assetRepositoryProvider), + ref.watch(backupRepositoryProvider), + ref.watch(albumMediaRepositoryProvider), + ref.watch(albumApiRepositoryProvider), ), ); class AlbumService { - final ApiService _apiService; final UserService _userService; final SyncService _syncService; - final Isar _db; + final EntityService _entityService; + final IAlbumRepository _albumRepository; + final IAssetRepository _assetRepository; + 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._db, + this._entityService, + this._albumRepository, + this._assetRepository, + this._backupAlbumRepository, + this._albumMediaRepository, + this._albumApiRepository, ); - QueryBuilder - selectedAlbumsQuery() => - _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select); - QueryBuilder - excludedAlbumsQuery() => - _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); - /// Checks all selected device albums for changes of albums and their assets /// Updates the local database and returns `true` if there were any changes Future refreshDeviceAlbums() async { @@ -65,22 +75,18 @@ class AlbumService { final Stopwatch sw = Stopwatch()..start(); bool changes = false; try { - final List excludedIds = - await excludedAlbumsQuery().idProperty().findAll(); - final List selectedIds = - await selectedAlbumsQuery().idProperty().findAll(); + final List excludedIds = await _backupAlbumRepository + .getIdsBySelection(BackupSelection.exclude); + final List selectedIds = await _backupAlbumRepository + .getIdsBySelection(BackupSelection.select); if (selectedIds.isEmpty) { - final numLocal = await _db.albums.where().localIdIsNotNull().count(); + final numLocal = await _albumRepository.count(local: true); if (numLocal > 0) { _syncService.removeAllLocalAlbumsAndAssets(); } 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) { @@ -96,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) { @@ -114,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 = @@ -128,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; @@ -154,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); @@ -178,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) { - Album album = await Album.remote(remote); - await _db.writeTxn(() => _db.albums.store(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); } /* @@ -212,8 +197,7 @@ class AlbumService { for (int round = 0;; round++) { final proposedName = "$baseName${round == 0 ? "" : " ($round)"}"; - if (null == - await _db.albums.filter().nameEqualTo(proposedName).findFirst()) { + if (null == await _albumRepository.getByName(proposedName)) { return proposedName; } } @@ -234,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()}"); } @@ -268,20 +241,15 @@ class AlbumService { Future _updateAssets( int albumId, { - Iterable add = const [], - Iterable remove = const [], - }) { - return _db.writeTxn(() async { - final album = await _db.albums.get(albumId); - if (album == null) return; - await album.assets.update(link: add, unlink: remove); - album.startDate = - await album.assets.filter().fileCreatedAtProperty().min(); - album.endDate = await album.assets.filter().fileCreatedAtProperty().max(); - album.lastModifiedAssetTimestamp = - await album.assets.filter().updatedAtProperty().max(); - await _db.albums.put(album); - }); + List add = const [], + List remove = const [], + }) async { + final album = await _albumRepository.getById(albumId); + if (album == null) return; + await _albumRepository.addAssets(album, add); + await _albumRepository.removeAssets(album, remove); + await _albumRepository.recalculateMetadata(album); + await _albumRepository.update(album); } Future addAdditionalUserToAlbum( @@ -289,24 +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 _db.users.getAllById(sharedUserIds)).cast()); - album.shared = result.shared; - await _db.writeTxn(() async { - await _db.albums.put(album); - await album.sharedUsers.save(); - }); - return true; - } + final updatedAlbum = + await _albumApiRepository.addUsers(album.remoteId!, sharedUserIds); + await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum); + await _albumRepository.update(updatedAlbum); + return true; } catch (e) { debugPrint("Error addAdditionalUserToAlbum ${e.toString()}"); } @@ -315,15 +270,13 @@ class AlbumService { Future 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 _db.writeTxn(() => _db.albums.put(album)); - return true; - } + await _entityService.fillAlbumWithDatabaseEntities(updatedAlbum); + await _albumRepository.update(updatedAlbum); + return true; } catch (e) { debugPrint("Error setActivityEnabled ${e.toString()}"); } @@ -332,29 +285,29 @@ class AlbumService { Future deleteAlbum(Album album) async { try { - final userId = Store.get(StoreKey.currentUser).isarId; - if (album.owner.value?.isarId == userId) { - await _apiService.albumsApi.deleteAlbum(album.remoteId!); + final user = Store.get(StoreKey.currentUser); + if (album.owner.value?.isarId == user.isarId) { + await _albumApiRepository.delete(album.remoteId!); } if (album.shared) { final foreignAssets = - await album.assets.filter().not().ownerIdEqualTo(userId).findAll(); - await _db.writeTxn(() => _db.albums.delete(album.id)); - final List albums = - await _db.albums.filter().sharedEqualTo(true).findAll(); + await _assetRepository.getByAlbum(album, notOwnedBy: user); + await _albumRepository.delete(album.id); + + final List albums = await _albumRepository.getAll(shared: true); final List existing = []; - for (Album a in albums) { + for (Album album in albums) { existing.addAll( - await a.assets.filter().not().ownerIdEqualTo(userId).findAll(), + await _assetRepository.getByAlbum(album, notOwnedBy: user), ); } final List idsToRemove = _syncService.sharedAssetsToRemove(foreignAssets, existing); if (idsToRemove.isNotEmpty) { - await _db.writeTxn(() => _db.assets.deleteAll(idsToRemove)); + await _assetRepository.deleteById(idsToRemove); } } else { - await _db.writeTxn(() => _db.albums.delete(album.id)); + await _albumRepository.delete(album.id); } return true; } catch (e) { @@ -365,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()}"); @@ -378,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); - return true; - } + final toRemove = result.removed + .map((id) => assets.firstWhere((asset) => asset.remoteId == id)); + await _updateAssets(album.id, remove: toRemove.toList()); + return true; } catch (e) { debugPrint("Error removeAssetFromAlbum ${e.toString()}"); } @@ -404,18 +350,16 @@ class AlbumService { User user, ) async { try { - await _apiService.albumsApi.removeUserFromAlbum( + await _albumApiRepository.removeUser( album.remoteId!, - user.id, + userId: user.id, ); album.sharedUsers.remove(user); - await _db.writeTxn(() async { - await album.sharedUsers.update(unlink: [user]); - final a = await _db.albums.get(album.id); - // trigger watcher - await _db.albums.put(a!); - }); + await _albumRepository.removeUsers(album, [user]); + final a = await _albumRepository.getById(album.id); + // trigger watcher + await _albumRepository.update(a!); return true; } catch (e) { @@ -429,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 _db.writeTxn(() => _db.albums.put(album)); - + await _entityService.fillAlbumWithDatabaseEntities(album); + await _albumRepository.update(album); return true; } catch (e) { debugPrint("Error changeTitleAlbum ${e.toString()}"); @@ -445,14 +386,8 @@ class AlbumService { } } - Future getAlbumByName(String name, bool remoteOnly) async { - return _db.albums - .filter() - .optional(remoteOnly, (q) => q.localIdIsNull()) - .nameEqualTo(name) - .sharedEqualTo(false) - .findFirst(); - } + Future getAlbumByName(String name, bool remoteOnly) => + _albumRepository.getByName(name, remote: remoteOnly ? true : null); /// /// Add the uploaded asset to the selected albums @@ -464,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 fc3feb174d582..86dfd0c5998c5 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -12,7 +12,18 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/asset_media.repository.dart'; +import 'package:immich_mobile/repositories/backup.repository.dart'; +import 'package:immich_mobile/repositories/album_media.repository.dart'; +import 'package:immich_mobile/repositories/file_media.repository.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; @@ -22,7 +33,6 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:immich_mobile/services/partner.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; @@ -30,7 +40,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(), @@ -354,15 +364,55 @@ class BackgroundService { apiService.setAccessToken(Store.get(StoreKey.accessToken)); AppSettingsService settingService = AppSettingsService(); AppSettingsService settingsService = AppSettingsService(); - PartnerService partnerService = PartnerService(apiService, db); - HashService hashService = HashService(db, this); - SyncService syncSerive = SyncService(db, hashService); - UserService userService = - UserService(apiService, db, syncSerive, partnerService); - AlbumService albumService = - AlbumService(apiService, userService, syncSerive, db); - BackupService backupService = - BackupService(apiService, db, settingService, albumService); + AlbumRepository albumRepository = AlbumRepository(db); + AssetRepository assetRepository = AssetRepository(db); + BackupRepository backupAlbumRepository = BackupRepository(db); + AlbumMediaRepository albumMediaRepository = AlbumMediaRepository(); + FileMediaRepository fileMediaRepository = FileMediaRepository(); + AssetMediaRepository assetMediaRepository = AssetMediaRepository(); + UserRepository userRepository = UserRepository(db); + UserApiRepository userApiRepository = + UserApiRepository(apiService.usersApi); + AlbumApiRepository albumApiRepository = + AlbumApiRepository(apiService.albumsApi); + PartnerApiRepository partnerApiRepository = + PartnerApiRepository(apiService.partnersApi); + HashService hashService = + HashService(assetRepository, this, albumMediaRepository); + EntityService entityService = + EntityService(assetRepository, userRepository); + SyncService syncSerive = SyncService( + db, + hashService, + entityService, + albumMediaRepository, + albumApiRepository, + ); + UserService userService = UserService( + partnerApiRepository, + userApiRepository, + userRepository, + syncSerive, + ); + AlbumService albumService = AlbumService( + userService, + syncSerive, + entityService, + albumRepository, + assetRepository, + backupAlbumRepository, + albumMediaRepository, + albumApiRepository, + ); + BackupService backupService = BackupService( + apiService, + db, + settingService, + albumService, + albumMediaRepository, + fileMediaRepository, + assetMediaRepository, + ); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); @@ -370,7 +420,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..683339f271ed2 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -6,9 +6,14 @@ 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/asset_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 +21,9 @@ 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/asset_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 +32,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 +40,9 @@ final backupServiceProvider = Provider( ref.watch(dbProvider), ref.watch(appSettingsServiceProvider), ref.watch(albumServiceProvider), + ref.watch(albumMediaRepositoryProvider), + ref.watch(fileMediaRepositoryProvider), + ref.watch(assetMediaRepositoryProvider), ), ); @@ -42,12 +53,18 @@ class BackupService { final Logger _log = Logger("BackupService"); final AppSettingsService _appSetting; final AlbumService _albumService; + final IAlbumMediaRepository _albumMediaRepository; + final IFileMediaRepository _fileMediaRepository; + final IAssetMediaRepository _assetMediaRepository; BackupService( this._apiService, this._db, this._appSetting, this._albumService, + this._albumMediaRepository, + this._fileMediaRepository, + this._assetMediaRepository, ); Future?> getDeviceBackupAsset() async { @@ -86,44 +103,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 +122,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 +190,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 +203,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 +219,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 +238,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 +249,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 +285,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 +302,43 @@ 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 = + await _assetMediaRepository.getOriginalFilename(asset.localId!); + 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 +362,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 +412,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 +437,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/download.service.dart b/mobile/lib/services/download.service.dart new file mode 100644 index 0000000000000..996cbe61f192f --- /dev/null +++ b/mobile/lib/services/download.service.dart @@ -0,0 +1,193 @@ +import 'dart:io'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/download.interface.dart'; +import 'package:immich_mobile/interfaces/file_media.interface.dart'; +import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; +import 'package:immich_mobile/repositories/download.repository.dart'; +import 'package:immich_mobile/repositories/file_media.repository.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/download.dart'; + +final downloadServiceProvider = Provider( + (ref) => DownloadService( + ref.watch(fileMediaRepositoryProvider), + ref.watch(downloadRepositoryProvider), + ), +); + +class DownloadService { + final IDownloadRepository _downloadRepository; + final IFileMediaRepository _fileMediaRepository; + void Function(TaskStatusUpdate)? onImageDownloadStatus; + void Function(TaskStatusUpdate)? onVideoDownloadStatus; + void Function(TaskStatusUpdate)? onLivePhotoDownloadStatus; + void Function(TaskProgressUpdate)? onTaskProgress; + + DownloadService( + this._fileMediaRepository, + this._downloadRepository, + ) { + _downloadRepository.onImageDownloadStatus = _onImageDownloadCallback; + _downloadRepository.onVideoDownloadStatus = _onVideoDownloadCallback; + _downloadRepository.onLivePhotoDownloadStatus = + _onLivePhotoDownloadCallback; + _downloadRepository.onTaskProgress = _onTaskProgressCallback; + } + + void _onTaskProgressCallback(TaskProgressUpdate update) { + onTaskProgress?.call(update); + } + + void _onImageDownloadCallback(TaskStatusUpdate update) { + onImageDownloadStatus?.call(update); + } + + void _onVideoDownloadCallback(TaskStatusUpdate update) { + onVideoDownloadStatus?.call(update); + } + + void _onLivePhotoDownloadCallback(TaskStatusUpdate update) { + onLivePhotoDownloadStatus?.call(update); + } + + Future saveImage(Task task) async { + final filePath = await task.filePath(); + final title = task.filename; + final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; + final data = await File(filePath).readAsBytes(); + + final Asset? resultAsset = await _fileMediaRepository.saveImage( + data, + title: title, + relativePath: relativePath, + ); + + return resultAsset != null; + } + + Future saveVideo(Task task) async { + final filePath = await task.filePath(); + final title = task.filename; + final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; + final file = File(filePath); + + final Asset? resultAsset = await _fileMediaRepository.saveVideo( + file, + title: title, + relativePath: relativePath, + ); + + return resultAsset != null; + } + + Future saveLivePhotos( + Task task, + String livePhotosId, + ) async { + try { + final records = await _downloadRepository.getLiveVideoTasks(); + if (records.length < 2) { + return false; + } + + final imageRecord = records.firstWhere( + (record) { + final metadata = LivePhotosMetadata.fromJson(record.task.metaData); + return metadata.id == livePhotosId && + metadata.part == LivePhotosPart.image; + }, + ); + + final videoRecord = records.firstWhere((record) { + final metadata = LivePhotosMetadata.fromJson(record.task.metaData); + return metadata.id == livePhotosId && + metadata.part == LivePhotosPart.video; + }); + + final imageFilePath = await imageRecord.task.filePath(); + final videoFilePath = await videoRecord.task.filePath(); + + final resultAsset = await _fileMediaRepository.saveLivePhoto( + image: File(imageFilePath), + video: File(videoFilePath), + title: task.filename, + ); + + await _downloadRepository.deleteRecordsWithIds([ + imageRecord.task.taskId, + videoRecord.task.taskId, + ]); + + return resultAsset != null; + } catch (error) { + debugPrint("Error saving live photo: $error"); + return false; + } + } + + Future cancelDownload(String id) async { + return await FileDownloader().cancelTaskWithId(id); + } + + Future download(Asset asset) async { + if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { + await _downloadRepository.download( + _buildDownloadTask( + asset.remoteId!, + asset.fileName, + group: downloadGroupLivePhoto, + metadata: LivePhotosMetadata( + part: LivePhotosPart.image, + id: asset.remoteId!, + ).toJson(), + ), + ); + + await _downloadRepository.download( + _buildDownloadTask( + asset.livePhotoVideoId!, + asset.fileName.toUpperCase().replaceAll(".HEIC", '.MOV'), + group: downloadGroupLivePhoto, + metadata: LivePhotosMetadata( + part: LivePhotosPart.video, + id: asset.remoteId!, + ).toJson(), + ), + ); + } else { + await _downloadRepository.download( + _buildDownloadTask( + asset.remoteId!, + asset.fileName, + group: asset.isImage ? downloadGroupImage : downloadGroupVideo, + ), + ); + } + } + + DownloadTask _buildDownloadTask( + String id, + String filename, { + String? group, + String? metadata, + }) { + final path = r'/assets/{id}/original'.replaceAll('{id}', id); + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final headers = ApiService.getRequestHeaders(); + + return DownloadTask( + taskId: id, + url: serverEndpoint + path, + headers: headers, + filename: filename, + updates: Updates.statusAndProgress, + group: group ?? '', + metaData: metadata ?? '', + ); + } +} 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..3827e421e6108 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -2,70 +2,92 @@ 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/interfaces/asset.interface.dart'; +import 'package:immich_mobile/repositories/album_media.repository.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/device_asset.entity.dart'; import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:photo_manager/photo_manager.dart'; class HashService { - HashService(this._db, this._backgroundService); - final Isar _db; + HashService( + this._assetRepository, + this._backgroundService, + this._albumMediaRepository, + ); + final IAssetRepository _assetRepository; 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 hashes = + await _assetRepository.getDeviceAssetsById(ids); final List toAdd = []; final List toHash = []; 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,15 +108,9 @@ 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 - Future> _lookupHashes(List ids) => - Platform.isAndroid - ? _db.androidDeviceAssets.getAll(ids.cast()) - : _db.iOSDeviceAssets.getAllById(ids.cast()); - /// Processes a batch of files and saves any successfully hashed /// values to the DB table. Future _processBatch( @@ -114,11 +130,7 @@ class HashService { final validHashes = anyNull ? toAdd.where((e) => e.hash.length == 20).toList(growable: false) : toAdd; - await _db.writeTxn( - () => Platform.isAndroid - ? _db.androidDeviceAssets.putAll(validHashes.cast()) - : _db.iOSDeviceAssets.putAll(validHashes.cast()), - ); + await _assetRepository.upsertDeviceAssets(validHashes); _log.fine("Hashed ${validHashes.length}/${toHash.length} assets"); } @@ -133,15 +145,16 @@ class HashService { return hashes; } - /// Converts [AssetEntity]s that were successfully hashed to [Asset]s - List _mapAllHashedAssets( - List assets, + /// Returns all successfully hashed [Asset]s with their hash value set + List _getHashedAssets( + List assets, List hashes, ) { final List result = []; for (int i = 0; i < assets.length; i++) { if (hashes[i] != null && hashes[i]!.hash.isNotEmpty) { - result.add(Asset.local(assets[i], hashes[i]!.hash)); + assets[i].byteHash = hashes[i]!.hash; + result.add(assets[i]); } } return result; @@ -150,7 +163,8 @@ class HashService { final hashServiceProvider = Provider( (ref) => HashService( - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ref.watch(backgroundServiceProvider), + ref.watch(albumMediaRepositoryProvider), ), ); diff --git a/mobile/lib/services/image_viewer.service.dart b/mobile/lib/services/image_viewer.service.dart deleted file mode 100644 index 9bcaba1d26b95..0000000000000 --- a/mobile/lib/services/image_viewer.service.dart +++ /dev/null @@ -1,114 +0,0 @@ -import 'dart:io'; - -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/response_extensions.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:logging/logging.dart'; - -import 'package:photo_manager/photo_manager.dart'; -import 'package:path_provider/path_provider.dart'; - -final imageViewerServiceProvider = - Provider((ref) => ImageViewerService(ref.watch(apiServiceProvider))); - -class ImageViewerService { - final ApiService _apiService; - final Logger _log = Logger("ImageViewerService"); - - ImageViewerService(this._apiService); - - Future downloadAsset(Asset asset) async { - File? imageFile; - File? videoFile; - try { - // Download LivePhotos image and motion part - if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) { - var imageResponse = - await _apiService.assetsApi.downloadAssetWithHttpInfo( - asset.remoteId!, - ); - - var motionResponse = - await _apiService.assetsApi.downloadAssetWithHttpInfo( - asset.livePhotoVideoId!, - ); - - if (imageResponse.statusCode != 200 || - motionResponse.statusCode != 200) { - final failedResponse = - imageResponse.statusCode != 200 ? imageResponse : motionResponse; - _log.severe( - "Motion asset download failed", - failedResponse.toLoggerString(), - ); - return false; - } - - AssetEntity? entity; - - final tempDir = await getTemporaryDirectory(); - videoFile = await File('${tempDir.path}/livephoto.mov').create(); - imageFile = await File('${tempDir.path}/livephoto.heic').create(); - videoFile.writeAsBytesSync(motionResponse.bodyBytes); - imageFile.writeAsBytesSync(imageResponse.bodyBytes); - - entity = await PhotoManager.editor.darwin.saveLivePhoto( - imageFile: imageFile, - videoFile: videoFile, - title: asset.fileName, - ); - - if (entity == null) { - _log.warning( - "Asset cannot be saved as a live photo. This is most likely a motion photo. Saving only the image file", - ); - - entity = await PhotoManager.editor.saveImage( - imageResponse.bodyBytes, - title: asset.fileName, - ); - } - - return entity != null; - } else { - var res = await _apiService.assetsApi - .downloadAssetWithHttpInfo(asset.remoteId!); - - if (res.statusCode != 200) { - _log.severe("Asset download failed", res.toLoggerString()); - return false; - } - - final AssetEntity? entity; - final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null; - - if (asset.isImage) { - entity = await PhotoManager.editor.saveImage( - res.bodyBytes, - title: asset.fileName, - relativePath: relativePath, - ); - } else { - final tempDir = await getTemporaryDirectory(); - videoFile = await File('${tempDir.path}/${asset.fileName}').create(); - videoFile.writeAsBytesSync(res.bodyBytes); - entity = await PhotoManager.editor.saveVideo( - videoFile, - title: asset.fileName, - relativePath: relativePath, - ); - } - return entity != null; - } - } catch (error, stack) { - _log.severe("Error saving downloaded asset", error, stack); - return false; - } finally { - // Clear temp files - imageFile?.delete(); - videoFile?.delete(); - } - } -} diff --git a/mobile/lib/services/memory.service.dart b/mobile/lib/services/memory.service.dart index ea07f7c019ea8..b95899df678ec 100644 --- a/mobile/lib/services/memory.service.dart +++ b/mobile/lib/services/memory.service.dart @@ -1,18 +1,17 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; final memoryServiceProvider = StateProvider((ref) { return MemoryService( ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ); }); @@ -20,9 +19,9 @@ class MemoryService { final log = Logger("MemoryService"); final ApiService _apiService; - final Isar _db; + final IAssetRepository _assetRepository; - MemoryService(this._apiService, this._db); + MemoryService(this._apiService, this._assetRepository); Future?> getMemoryLane() async { try { @@ -39,7 +38,7 @@ class MemoryService { List memories = []; for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) { final dbAssets = - await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); + await _assetRepository.getAllByRemoteId(assets.map((e) => e.id)); if (dbAssets.isNotEmpty) { final String title = yearsAgo <= 1 ? 'memories_year_ago'.tr() diff --git a/mobile/lib/services/partner.service.dart b/mobile/lib/services/partner.service.dart index 8cd2fe424f190..67d7f4e1d1f56 100644 --- a/mobile/lib/services/partner.service.dart +++ b/mobile/lib/services/partner.service.dart @@ -1,43 +1,33 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; final partnerServiceProvider = Provider( (ref) => PartnerService( - ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(partnerApiRepositoryProvider), + ref.watch(userRepositoryProvider), ), ); class PartnerService { - final ApiService _apiService; - final Isar _db; + final IPartnerApiRepository _partnerApiRepository; + final IUserRepository _userRepository; final Logger _log = Logger("PartnerService"); - PartnerService(this._apiService, this._db); - - Future?> getPartners(PartnerDirection direction) async { - try { - final userDtos = await _apiService.partnersApi.getPartners(direction); - if (userDtos != null) { - return userDtos.map((u) => User.fromPartnerDto(u)).toList(); - } - } catch (e) { - _log.warning("Failed to get partners for direction $direction", e); - } - return null; - } + PartnerService( + this._partnerApiRepository, + this._userRepository, + ); Future removePartner(User partner) async { try { - await _apiService.partnersApi.removePartner(partner.id); + await _partnerApiRepository.delete(partner.id); partner.isPartnerSharedBy = false; - await _db.writeTxn(() => _db.users.put(partner)); + await _userRepository.update(partner); } catch (e) { _log.warning("Failed to remove partner ${partner.id}", e); return false; @@ -47,12 +37,10 @@ class PartnerService { Future addPartner(User partner) async { try { - final dto = await _apiService.partnersApi.createPartner(partner.id); - if (dto != null) { - partner.isPartnerSharedBy = true; - await _db.writeTxn(() => _db.users.put(partner)); - return true; - } + await _partnerApiRepository.create(partner.id); + partner.isPartnerSharedBy = true; + await _userRepository.update(partner); + return true; } catch (e) { _log.warning("Failed to add partner ${partner.id}", e); } @@ -61,13 +49,13 @@ class PartnerService { Future updatePartner(User partner, {required bool inTimeline}) async { try { - final dto = await _apiService.partnersApi - .updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline)); - if (dto != null) { - partner.inTimeline = dto.inTimeline ?? partner.inTimeline; - await _db.writeTxn(() => _db.users.put(partner)); - return true; - } + final dto = await _partnerApiRepository.update( + partner.id, + inTimeline: inTimeline, + ); + partner.inTimeline = dto.inTimeline; + await _userRepository.update(partner); + return true; } catch (e) { _log.warning("Failed to update partner ${partner.id}", e); } diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index ddb61f5e48a40..5b325acdc591b 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -1,29 +1,37 @@ import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; +import 'package:immich_mobile/interfaces/asset_api.interface.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/person_api.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'person.service.g.dart'; @riverpod -PersonService personService(PersonServiceRef ref) => - PersonService(ref.read(apiServiceProvider), ref.read(dbProvider)); +PersonService personService(PersonServiceRef ref) => PersonService( + ref.watch(personApiRepositoryProvider), + ref.watch(assetApiRepositoryProvider), + ref.read(assetRepositoryProvider), + ); class PersonService { final Logger _log = Logger("PersonService"); - final ApiService _apiService; - final Isar _db; + final IPersonApiRepository _personApiRepository; + final IAssetApiRepository _assetApiRepository; + final IAssetRepository _assetRepository; - PersonService(this._apiService, this._db); + PersonService( + this._personApiRepository, + this._assetApiRepository, + this._assetRepository, + ); - Future> getAllPeople() async { + Future> getAllPeople() async { try { - final peopleResponseDto = await _apiService.peopleApi.getAllPeople(); - return peopleResponseDto?.people ?? []; + return await _personApiRepository.getAll(); } catch (error, stack) { _log.severe("Error while fetching curated people", error, stack); return []; @@ -31,50 +39,19 @@ class PersonService { } Future> getPersonAssets(String id) async { - List result = []; - var hasNext = true; - var currentPage = 1; - try { - while (hasNext) { - final response = await _apiService.searchApi.searchMetadata( - MetadataSearchDto( - personIds: [id], - page: currentPage, - size: 1000, - ), - ); - - if (response == null) { - break; - } - - if (response.assets.nextPage == null) { - hasNext = false; - } - - final assets = response.assets.items; - final mapAssets = - await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); - result.addAll(mapAssets); - - currentPage++; - } + final assets = await _assetApiRepository.search(personIds: [id]); + return await _assetRepository + .getAllByRemoteId(assets.map((a) => a.remoteId!)); } catch (error, stack) { _log.severe("Error while fetching person assets", error, stack); } - - return result; + return []; } - Future updateName(String id, String name) async { + Future updateName(String id, String name) async { try { - return await _apiService.peopleApi.updatePerson( - id, - PersonUpdateDto( - name: name, - ), - ); + return await _personApiRepository.update(id, name: name); } catch (error, stack) { _log.severe("Error while updating person name", error, stack); } diff --git a/mobile/lib/services/person.service.g.dart b/mobile/lib/services/person.service.g.dart index 01a5ed8f30943..9a24069fbffa5 100644 --- a/mobile/lib/services/person.service.g.dart +++ b/mobile/lib/services/person.service.g.dart @@ -6,7 +6,7 @@ part of 'person.service.dart'; // RiverpodGenerator // ************************************************************************** -String _$personServiceHash() => r'54e6df4b8eea744f6de009f8315c9fe6230f6798'; +String _$personServiceHash() => r'32f28cb5a3de0553c17447e33a0efde7409a43ed'; /// See also [personService]. @ProviderFor(personService) diff --git a/mobile/lib/services/search.service.dart b/mobile/lib/services/search.service.dart index cf3905e5ca240..336fe450108d3 100644 --- a/mobile/lib/services/search.service.dart +++ b/mobile/lib/services/search.service.dart @@ -1,27 +1,27 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; final searchServiceProvider = Provider( (ref) => SearchService( ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ), ); class SearchService { final ApiService _apiService; - final Isar _db; + final IAssetRepository _assetRepository; final _log = Logger("SearchService"); - SearchService(this._apiService, this._db); + SearchService(this._apiService, this._assetRepository); Future?> getSearchSuggestions( SearchSuggestionType type, { @@ -103,7 +103,7 @@ class SearchService { return null; } - return _db.assets + return _assetRepository .getAllByRemoteId(response.assets.items.map((e) => e.id)); } catch (error, stackTrace) { _log.severe("Failed to search for assets", error, stackTrace); diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart index 75074101c2ff8..8bff21fef61a6 100644 --- a/mobile/lib/services/stack.service.dart +++ b/mobile/lib/services/stack.service.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/asset.interface.dart'; import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/asset.repository.dart'; import 'package:immich_mobile/services/api.service.dart'; -import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; class StackService { - StackService(this._api, this._db); + StackService(this._api, this._assetRepository); final ApiService _api; - final Isar _db; + final IAssetRepository _assetRepository; Future getStack(String stackId) async { try { @@ -61,10 +61,7 @@ class StackService { removeAssets.add(asset); } - - _db.writeTxn(() async { - await _db.assets.putAll(removeAssets); - }); + await _assetRepository.updateAll(removeAssets); } catch (error) { debugPrint("Error while deleting stack: $error"); } @@ -74,6 +71,6 @@ class StackService { final stackServiceProvider = Provider( (ref) => StackService( ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(assetRepositoryProvider), ), ); diff --git a/mobile/lib/services/sync.service.dart b/mobile/lib/services/sync.service.dart index 8ec56e925f7f8..c3f927fc93937 100644 --- a/mobile/lib/services/sync.service.dart +++ b/mobile/lib/services/sync.service.dart @@ -8,7 +8,12 @@ import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/interfaces/album_api.interface.dart'; +import 'package:immich_mobile/interfaces/album_media.interface.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/repositories/album_api.repository.dart'; +import 'package:immich_mobile/repositories/album_media.repository.dart'; +import 'package:immich_mobile/services/entity.service.dart'; import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; @@ -16,20 +21,33 @@ import 'package:immich_mobile/utils/datetime_comparison.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; -import 'package:photo_manager/photo_manager.dart'; final syncServiceProvider = Provider( - (ref) => SyncService(ref.watch(dbProvider), ref.watch(hashServiceProvider)), + (ref) => SyncService( + ref.watch(dbProvider), + ref.watch(hashServiceProvider), + ref.watch(entityServiceProvider), + ref.watch(albumMediaRepositoryProvider), + ref.watch(albumApiRepositoryProvider), + ), ); class SyncService { final Isar _db; final HashService _hashService; + final EntityService _entityService; + final IAlbumMediaRepository _albumMediaRepository; + final IAlbumApiRepository _albumApiRepository; final AsyncMutex _lock = AsyncMutex(); final Logger _log = Logger('SyncService'); - SyncService(this._db, this._hashService); + SyncService( + this._db, + this._hashService, + this._entityService, + this._albumMediaRepository, + this._albumApiRepository, + ); // public methods: @@ -59,16 +77,15 @@ class SyncService { /// Syncs remote albums to the database /// returns `true` if there were any changes Future syncRemoteAlbumsToDb( - List remote, { + List remote, { required bool isShared, - required FutureOr Function(AlbumResponseDto) loadDetails, }) => - _lock.run(() => _syncRemoteAlbumsToDb(remote, isShared, loadDetails)); + _lock.run(() => _syncRemoteAlbumsToDb(remote, isShared)); /// Syncs all device albums and their assets to the database /// Returns `true` if there were any changes Future syncLocalAlbumAssetsToDb( - List onDevice, [ + List onDevice, [ Set? excludedAssets, ]) => _lock.run(() => _syncLocalAlbumAssetsToDb(onDevice, excludedAssets)); @@ -283,11 +300,10 @@ class SyncService { /// Syncs remote albums to the database /// returns `true` if there were any changes Future _syncRemoteAlbumsToDb( - List remote, + List remoteAlbums, bool isShared, - FutureOr Function(AlbumResponseDto) loadDetails, ) async { - remote.sortBy((e) => e.id); + remoteAlbums.sortBy((e) => e.remoteId!); final baseQuery = _db.albums.where().remoteIdIsNotNull().filter(); final QueryBuilder query; @@ -304,14 +320,14 @@ class SyncService { final List existing = []; final bool changes = await diffSortedLists( - remote, + remoteAlbums, dbAlbums, - compare: (AlbumResponseDto a, Album b) => a.id.compareTo(b.remoteId!), - both: (AlbumResponseDto a, Album b) => - _syncRemoteAlbum(a, b, toDelete, existing, loadDetails), - onlyFirst: (AlbumResponseDto a) => - _addAlbumFromServer(a, existing, loadDetails), - onlySecond: (Album a) => _removeAlbumFromDb(a, toDelete), + compare: (remoteAlbum, dbAlbum) => + remoteAlbum.remoteId!.compareTo(dbAlbum.remoteId!), + both: (remoteAlbum, dbAlbum) => + _syncRemoteAlbum(remoteAlbum, dbAlbum, toDelete, existing), + onlyFirst: (remoteAlbum) => _addAlbumFromServer(remoteAlbum, existing), + onlySecond: (dbAlbum) => _removeAlbumFromDb(dbAlbum, toDelete), ); if (isShared && toDelete.isNotEmpty) { @@ -332,26 +348,22 @@ class SyncService { /// syncing changes from local back to server) /// accumulates Future _syncRemoteAlbum( - AlbumResponseDto dto, + Album dto, Album album, List deleteCandidates, List existing, - FutureOr Function(AlbumResponseDto) loadDetails, ) async { - if (!_hasAlbumResponseDtoChanged(dto, album)) { + if (!_hasRemoteAlbumChanged(dto, album)) { return false; } // loadDetails (/api/album/:id) will not include lastModifiedAssetTimestamp, // i.e. it will always be null. Save it here. final originalDto = dto; - dto = await loadDetails(dto); - if (dto.assetCount != dto.assets.length) { - return false; - } + dto = await _albumApiRepository.get(dto.remoteId!); final assetsInDb = await album.assets.filter().sortByOwnerId().thenByChecksum().findAll(); assert(assetsInDb.isSorted(Asset.compareByOwnerChecksum), "inDb unsorted!"); - final List assetsOnRemote = dto.getAssets(); + final List assetsOnRemote = dto.remoteAssets.toList(); assetsOnRemote.sort(Asset.compareByOwnerChecksum); final (toAdd, toUpdate, toUnlink) = _diffAssets( assetsOnRemote, @@ -362,15 +374,16 @@ class SyncService { // update shared users final List sharedUsers = album.sharedUsers.toList(growable: false); sharedUsers.sort((a, b) => a.id.compareTo(b.id)); - dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id)); + final List users = dto.remoteUsers.toList() + ..sort((a, b) => a.id.compareTo(b.id)); final List userIdsToAdd = []; final List usersToUnlink = []; diffSortedListsSync( - dto.albumUsers, + users, sharedUsers, - compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id), + compare: (User a, User b) => a.id.compareTo(b.id), both: (a, b) => false, - onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id), + onlyFirst: (User a) => userIdsToAdd.add(a.id), onlySecond: (User a) => usersToUnlink.add(a), ); @@ -380,19 +393,19 @@ class SyncService { final assetsToLink = existingInDb + updated; final usersToLink = (await _db.users.getAllById(userIdsToAdd)).cast(); - album.name = dto.albumName; + album.name = dto.name; album.shared = dto.shared; album.createdAt = dto.createdAt; - album.modifiedAt = dto.updatedAt; + album.modifiedAt = dto.modifiedAt; album.startDate = dto.startDate; album.endDate = dto.endDate; album.lastModifiedAssetTimestamp = originalDto.lastModifiedAssetTimestamp; album.shared = dto.shared; - album.activityEnabled = dto.isActivityEnabled; - if (album.thumbnail.value?.remoteId != dto.albumThumbnailAssetId) { + album.activityEnabled = dto.activityEnabled; + if (album.thumbnail.value?.remoteId != dto.remoteThumbnailAssetId) { album.thumbnail.value = await _db.assets .where() - .remoteIdEqualTo(dto.albumThumbnailAssetId) + .remoteIdEqualTo(dto.remoteThumbnailAssetId) .findFirst(); } @@ -428,27 +441,26 @@ class SyncService { /// (shared) assets to the database beforehand /// accumulates assets already existing in the database Future _addAlbumFromServer( - AlbumResponseDto dto, + Album album, List existing, - FutureOr Function(AlbumResponseDto) loadDetails, ) async { - if (dto.assetCount != dto.assets.length) { - dto = await loadDetails(dto); + if (album.remoteAssetCount != album.remoteAssets.length) { + album = await _albumApiRepository.get(album.remoteId!); } - if (dto.assetCount == dto.assets.length) { + if (album.remoteAssetCount == album.remoteAssets.length) { // in case an album contains assets not yet present in local DB: // put missing album assets into local DB final (existingInDb, updated) = - await _linkWithExistingFromDb(dto.getAssets()); + await _linkWithExistingFromDb(album.remoteAssets.toList()); existing.addAll(existingInDb); await upsertAssetsWithExif(updated); - final Album a = await Album.remote(dto); - await _db.writeTxn(() => _db.albums.store(a)); + await _entityService.fillAlbumWithDatabaseEntities(album); + await _db.writeTxn(() => _db.albums.store(album)); } else { _log.warning( - "Failed to add album from server: assetCount ${dto.assetCount} != " - "asset array length ${dto.assets.length} for album ${dto.albumName}"); + "Failed to add album from server: assetCount ${album.remoteAssetCount} != " + "asset array length ${album.remoteAssets.length} for album ${album.name}"); } } @@ -492,7 +504,7 @@ class SyncService { /// Syncs all device albums and their assets to the database /// Returns `true` if there were any changes Future _syncLocalAlbumAssetsToDb( - List onDevice, [ + List onDevice, [ Set? excludedAssets, ]) async { onDevice.sort((a, b) => a.id.compareTo(b.id)); @@ -504,16 +516,15 @@ class SyncService { final bool anyChanges = await diffSortedLists( onDevice, inDb, - compare: (AssetPathEntity a, Album b) => a.id.compareTo(b.localId!), - both: (AssetPathEntity ape, Album album) => _syncAlbumInDbAndOnDevice( - ape, - album, + compare: (Album a, Album b) => a.localId!.compareTo(b.localId!), + both: (Album a, Album b) => _syncAlbumInDbAndOnDevice( + a, + b, deleteCandidates, existing, excludedAssets, ), - onlyFirst: (AssetPathEntity ape) => - _addAlbumFromDevice(ape, existing, excludedAssets), + onlyFirst: (Album a) => _addAlbumFromDevice(a, existing, excludedAssets), onlySecond: (Album a) => _removeAlbumFromDb(a, deleteCandidates), ); _log.fine( @@ -541,58 +552,65 @@ class SyncService { /// returns `true` if there were any changes /// Accumulates asset candidates to delete and those already existing in DB Future _syncAlbumInDbAndOnDevice( - AssetPathEntity ape, - Album album, + Album deviceAlbum, + Album dbAlbum, List deleteCandidates, List existing, [ Set? excludedAssets, bool forceRefresh = false, ]) async { - if (!forceRefresh && !await _hasAssetPathEntityChanged(ape, album)) { - _log.fine("Local album ${ape.name} has not changed. Skipping sync."); + if (!forceRefresh && !await _hasAlbumChangeOnDevice(deviceAlbum, dbAlbum)) { + _log.fine( + "Local album ${deviceAlbum.name} has not changed. Skipping sync.", + ); return false; } if (!forceRefresh && excludedAssets == null && - await _syncDeviceAlbumFast(ape, album)) { + await _syncDeviceAlbumFast(deviceAlbum, dbAlbum)) { return true; } // general case, e.g. some assets have been deleted or there are excluded albums on iOS - final inDb = await album.assets + final inDb = await dbAlbum.assets .filter() .ownerIdEqualTo(Store.get(StoreKey.currentUser).isarId) .sortByChecksum() .findAll(); assert(inDb.isSorted(Asset.compareByChecksum), "inDb not sorted!"); - final int assetCountOnDevice = await ape.assetCountAsync; - final List onDevice = - await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets); + final int assetCountOnDevice = + await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); + final List onDevice = await _hashService.getHashedAssets( + deviceAlbum, + excludedAssets: excludedAssets, + ); _removeDuplicates(onDevice); // _removeDuplicates sorts `onDevice` by checksum final (toAdd, toUpdate, toDelete) = _diffAssets(onDevice, inDb); if (toAdd.isEmpty && toUpdate.isEmpty && toDelete.isEmpty && - album.name == ape.name && - ape.lastModified != null && - album.modifiedAt.isAtSameMomentAs(ape.lastModified!)) { + dbAlbum.name == deviceAlbum.name && + dbAlbum.modifiedAt.isAtSameMomentAs(deviceAlbum.modifiedAt)) { // changes only affeted excluded albums _log.fine( - "Only excluded assets in local album ${ape.name} changed. Stopping sync.", + "Only excluded assets in local album ${deviceAlbum.name} changed. Stopping sync.", ); if (assetCountOnDevice != - _db.eTags.getByIdSync(ape.eTagKeyAssetCount)?.assetCount) { + _db.eTags.getByIdSync(deviceAlbum.eTagKeyAssetCount)?.assetCount) { await _db.writeTxn( () => _db.eTags.put( - ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice), + ETag( + id: deviceAlbum.eTagKeyAssetCount, + assetCount: assetCountOnDevice, + ), ), ); } return false; } _log.fine( - "Syncing local album ${ape.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", + "Syncing local album ${deviceAlbum.name}. ${toAdd.length} assets to add, ${toUpdate.length} to update, ${toDelete.length} to delete", ); final (existingInDb, updated) = await _linkWithExistingFromDb(toAdd); _log.fine( @@ -600,28 +618,31 @@ class SyncService { ); deleteCandidates.addAll(toDelete); existing.addAll(existingInDb); - album.name = ape.name; - album.modifiedAt = ape.lastModified ?? DateTime.now(); - if (album.thumbnail.value != null && - toDelete.contains(album.thumbnail.value)) { - album.thumbnail.value = null; + dbAlbum.name = deviceAlbum.name; + dbAlbum.modifiedAt = deviceAlbum.modifiedAt; + if (dbAlbum.thumbnail.value != null && + toDelete.contains(dbAlbum.thumbnail.value)) { + dbAlbum.thumbnail.value = null; } try { await _db.writeTxn(() async { await _db.assets.putAll(updated); await _db.assets.putAll(toUpdate); - await album.assets + await dbAlbum.assets .update(link: existingInDb + updated, unlink: toDelete); - await _db.albums.put(album); - album.thumbnail.value ??= await album.assets.filter().findFirst(); - await album.thumbnail.save(); + await _db.albums.put(dbAlbum); + dbAlbum.thumbnail.value ??= await dbAlbum.assets.filter().findFirst(); + await dbAlbum.thumbnail.save(); await _db.eTags.put( - ETag(id: ape.eTagKeyAssetCount, assetCount: assetCountOnDevice), + ETag( + id: deviceAlbum.eTagKeyAssetCount, + assetCount: assetCountOnDevice, + ), ); }); - _log.info("Synced changes of local album ${ape.name} to DB"); + _log.info("Synced changes of local album ${deviceAlbum.name} to DB"); } on IsarError catch (e) { - _log.severe("Failed to update synced album ${ape.name} in DB", e); + _log.severe("Failed to update synced album ${deviceAlbum.name} in DB", e); } return true; @@ -629,45 +650,45 @@ class SyncService { /// fast path for common case: only new assets were added to device album /// returns `true` if successfull, else `false` - Future _syncDeviceAlbumFast(AssetPathEntity ape, Album album) async { - if (!(ape.lastModified ?? DateTime.now()).isAfter(album.modifiedAt)) { + Future _syncDeviceAlbumFast(Album deviceAlbum, Album dbAlbum) async { + if (!deviceAlbum.modifiedAt.isAfter(dbAlbum.modifiedAt)) { return false; } - final int totalOnDevice = await ape.assetCountAsync; + final int totalOnDevice = + await _albumMediaRepository.getAssetCount(deviceAlbum.localId!); final int lastKnownTotal = - (await _db.eTags.getById(ape.eTagKeyAssetCount))?.assetCount ?? 0; - final AssetPathEntity? modified = totalOnDevice > lastKnownTotal - ? await ape.fetchPathProperties( - filterOptionGroup: FilterOptionGroup( - updateTimeCond: DateTimeCond( - min: album.modifiedAt.add(const Duration(seconds: 1)), - max: ape.lastModified ?? DateTime.now(), - ), - ), - ) - : null; - if (modified == null) { + (await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount))?.assetCount ?? + 0; + if (totalOnDevice <= lastKnownTotal) { return false; } - final List newAssets = await _hashService.getHashedAssets(modified); + final List newAssets = await _hashService.getHashedAssets( + deviceAlbum, + modifiedFrom: dbAlbum.modifiedAt.add(const Duration(seconds: 1)), + modifiedUntil: deviceAlbum.modifiedAt, + ); if (totalOnDevice != lastKnownTotal + newAssets.length) { return false; } - album.modifiedAt = ape.lastModified ?? DateTime.now(); + dbAlbum.modifiedAt = deviceAlbum.modifiedAt; _removeDuplicates(newAssets); final (existingInDb, updated) = await _linkWithExistingFromDb(newAssets); try { await _db.writeTxn(() async { await _db.assets.putAll(updated); - await album.assets.update(link: existingInDb + updated); - await _db.albums.put(album); - await _db.eTags - .put(ETag(id: ape.eTagKeyAssetCount, assetCount: totalOnDevice)); + await dbAlbum.assets.update(link: existingInDb + updated); + await _db.albums.put(dbAlbum); + await _db.eTags.put( + ETag(id: deviceAlbum.eTagKeyAssetCount, assetCount: totalOnDevice), + ); }); - _log.info("Fast synced local album ${ape.name} to DB"); + _log.info("Fast synced local album ${deviceAlbum.name} to DB"); } on IsarError catch (e) { - _log.severe("Failed to fast sync local album ${ape.name} to DB", e); + _log.severe( + "Failed to fast sync local album ${deviceAlbum.name} to DB", + e, + ); return false; } @@ -677,14 +698,15 @@ class SyncService { /// Adds a new album from the device to the database and Accumulates all /// assets already existing in the database to the list of `existing` assets Future _addAlbumFromDevice( - AssetPathEntity ape, + Album album, List existing, [ Set? excludedAssets, ]) async { - _log.info("Syncing a new local album to DB: ${ape.name}"); - final Album a = Album.local(ape); - final assets = - await _hashService.getHashedAssets(ape, excludedAssets: excludedAssets); + _log.info("Syncing a new local album to DB: ${album.name}"); + final assets = await _hashService.getHashedAssets( + album, + excludedAssets: excludedAssets, + ); _removeDuplicates(assets); final (existingInDb, updated) = await _linkWithExistingFromDb(assets); _log.info( @@ -692,15 +714,15 @@ class SyncService { ); await upsertAssetsWithExif(updated); existing.addAll(existingInDb); - a.assets.addAll(existingInDb); - a.assets.addAll(updated); + album.assets.addAll(existingInDb); + album.assets.addAll(updated); final thumb = existingInDb.firstOrNull ?? updated.firstOrNull; - a.thumbnail.value = thumb; + album.thumbnail.value = thumb; try { - await _db.writeTxn(() => _db.albums.store(a)); - _log.info("Added a new local album to DB: ${ape.name}"); + await _db.writeTxn(() => _db.albums.store(album)); + _log.info("Added a new local album to DB: ${album.name}"); } on IsarError catch (e) { - _log.severe("Failed to add new local album ${ape.name} to DB", e); + _log.severe("Failed to add new local album ${album.name} to DB", e); } } @@ -798,12 +820,15 @@ class SyncService { } /// returns `true` if the albums differ on the surface - Future _hasAssetPathEntityChanged(AssetPathEntity a, Album b) async { - return a.name != b.name || - a.lastModified == null || - !a.lastModified!.isAtSameMomentAs(b.modifiedAt) || - await a.assetCountAsync != - (await _db.eTags.getById(a.eTagKeyAssetCount))?.assetCount; + Future _hasAlbumChangeOnDevice( + Album deviceAlbum, + Album dbAlbum, + ) async { + return deviceAlbum.name != dbAlbum.name || + !deviceAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || + await _albumMediaRepository.getAssetCount(deviceAlbum.localId!) != + (await _db.eTags.getById(deviceAlbum.eTagKeyAssetCount)) + ?.assetCount; } Future _removeAllLocalAlbumsAndAssets() async { @@ -900,17 +925,17 @@ class SyncService { } /// returns `true` if the albums differ on the surface -bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) { - return dto.assetCount != a.assetCount || - dto.albumName != a.name || - dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId || - dto.shared != a.shared || - dto.albumUsers.length != a.sharedUsers.length || - !dto.updatedAt.isAtSameMomentAs(a.modifiedAt) || - !isAtSameMomentAs(dto.startDate, a.startDate) || - !isAtSameMomentAs(dto.endDate, a.endDate) || +bool _hasRemoteAlbumChanged(Album remoteAlbum, Album dbAlbum) { + return remoteAlbum.remoteAssetCount != dbAlbum.assetCount || + remoteAlbum.name != dbAlbum.name || + remoteAlbum.remoteThumbnailAssetId != dbAlbum.thumbnail.value?.remoteId || + remoteAlbum.shared != dbAlbum.shared || + remoteAlbum.remoteUsers.length != dbAlbum.sharedUsers.length || + !remoteAlbum.modifiedAt.isAtSameMomentAs(dbAlbum.modifiedAt) || + !isAtSameMomentAs(remoteAlbum.startDate, dbAlbum.startDate) || + !isAtSameMomentAs(remoteAlbum.endDate, dbAlbum.endDate) || !isAtSameMomentAs( - dto.lastModifiedAssetTimestamp, - a.lastModifiedAssetTimestamp, + remoteAlbum.lastModifiedAssetTimestamp, + dbAlbum.lastModifiedAssetTimestamp, ); } diff --git a/mobile/lib/services/user.service.dart b/mobile/lib/services/user.service.dart index 9631141c416c9..4c2b3cbbd00fb 100644 --- a/mobile/lib/services/user.service.dart +++ b/mobile/lib/services/user.service.dart @@ -1,68 +1,48 @@ import 'package:collection/collection.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:http/http.dart'; import 'package:image_picker/image_picker.dart'; -import 'package:immich_mobile/services/partner.service.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/interfaces/partner_api.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:immich_mobile/interfaces/user_api.interface.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/user.repository.dart'; +import 'package:immich_mobile/repositories/user_api.repository.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/utils/diff.dart'; -import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart'; final userServiceProvider = Provider( (ref) => UserService( - ref.watch(apiServiceProvider), - ref.watch(dbProvider), + ref.watch(partnerApiRepositoryProvider), + ref.watch(userApiRepositoryProvider), + ref.watch(userRepositoryProvider), ref.watch(syncServiceProvider), - ref.watch(partnerServiceProvider), ), ); class UserService { - final ApiService _apiService; - final Isar _db; + final IPartnerApiRepository _partnerApiRepository; + final IUserApiRepository _userApiRepository; + final IUserRepository _userRepository; final SyncService _syncService; - final PartnerService _partnerService; final Logger _log = Logger("UserService"); UserService( - this._apiService, - this._db, + this._partnerApiRepository, + this._userApiRepository, + this._userRepository, this._syncService, - this._partnerService, ); - Future?> _getAllUsers() async { - try { - final dto = await _apiService.usersApi.searchUsers(); - return dto?.map(User.fromSimpleUserDto).toList(); - } catch (e) { - _log.warning("Failed get all users", e); - return null; - } - } + Future> getUsers({bool self = false}) => + _userRepository.getAll(self: self); - Future> getUsersInDb({bool self = false}) async { - if (self) { - return _db.users.where().findAll(); - } - final int userId = Store.get(StoreKey.currentUser).isarId; - return _db.users.where().isarIdNotEqualTo(userId).findAll(); - } - - Future uploadProfileImage(XFile image) async { + Future<({String profileImagePath})?> uploadProfileImage(XFile image) async { try { - return await _apiService.usersApi.createProfileImage( - MultipartFile.fromBytes( - 'file', - await image.readAsBytes(), - filename: image.name, - ), + return await _userApiRepository.createProfileImage( + name: image.name, + data: await image.readAsBytes(), ); } catch (e) { _log.warning("Failed to upload profile image", e); @@ -71,13 +51,19 @@ class UserService { } Future?> getUsersFromServer() async { - final List? users = await _getAllUsers(); - final List? sharedBy = - await _partnerService.getPartners(PartnerDirection.by); - final List? sharedWith = - await _partnerService.getPartners(PartnerDirection.with_); + List? users; + try { + users = await _userApiRepository.getAll(); + } catch (e) { + _log.warning("Failed to fetch users", e); + users = null; + } + final List sharedBy = + await _partnerApiRepository.getAll(Direction.sharedByMe); + final List sharedWith = + await _partnerApiRepository.getAll(Direction.sharedWithMe); - if (users == null || sharedBy == null || sharedWith == null) { + if (users == null) { _log.warning("Failed to refresh users"); return null; } diff --git a/mobile/lib/utils/download.dart b/mobile/lib/utils/download.dart new file mode 100644 index 0000000000000..c701f353a2e6e --- /dev/null +++ b/mobile/lib/utils/download.dart @@ -0,0 +1,3 @@ +const downloadGroupImage = 'group_image'; +const downloadGroupVideo = 'group_video'; +const downloadGroupLivePhoto = 'group_livephoto'; diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index e7a1b9e39eefe..9fc7b13eed1c6 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -1,7 +1,7 @@ +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; -import 'package:isar/isar.dart'; import 'package:openapi/api.dart'; String getThumbnailUrl( @@ -61,7 +61,7 @@ String getOriginalUrlForRemoteId(final String id) { String getImageCacheKey(final Asset asset) { // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = asset.id == Isar.autoIncrement; + final isFromDto = asset.id == noDbId; return '${isFromDto ? asset.remoteId : asset.id}_fullStage'; } diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 349b2322afac1..255ad01247aa0 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -12,6 +12,29 @@ dynamic upgradeDto(dynamic value, String targetType) { addDefault(value, 'tags', TagsResponse().toJson()); } break; + case 'ServerConfigDto': + if (value is Map) { + addDefault( + value, + 'mapLightStyleUrl', + 'https://tiles.immich.cloud/v1/style/light.json', + ); + addDefault( + value, + 'mapDarkStyleUrl', + 'https://tiles.immich.cloud/v1/style/dark.json', + ); + } + case 'UserResponseDto': + if (value is Map) { + addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); + } + break; + case 'UserAdminResponseDto': + if (value is Map) { + addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String()); + } + break; } } diff --git a/mobile/lib/utils/provider_utils.dart b/mobile/lib/utils/provider_utils.dart new file mode 100644 index 0000000000000..3eac55089dcd4 --- /dev/null +++ b/mobile/lib/utils/provider_utils.dart @@ -0,0 +1,16 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/repositories/activity_api.repository.dart'; +import 'package:immich_mobile/repositories/album_api.repository.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/partner_api.repository.dart'; +import 'package:immich_mobile/repositories/person_api.repository.dart'; +import 'package:immich_mobile/repositories/user_api.repository.dart'; + +void invalidateAllApiRepositoryProviders(WidgetRef ref) { + ref.invalidate(userApiRepositoryProvider); + ref.invalidate(activityApiRepositoryProvider); + ref.invalidate(partnerApiRepositoryProvider); + ref.invalidate(albumApiRepositoryProvider); + ref.invalidate(personApiRepositoryProvider); + ref.invalidate(assetApiRepositoryProvider); +} diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index 8e818f64fb7cc..6cadef763d730 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; -import 'package:isar/isar.dart'; class ThumbnailImage extends ConsumerWidget { /// The asset to show the thumbnail image for @@ -46,7 +46,7 @@ class ThumbnailImage extends ConsumerWidget { ? context.primaryColor.darken(amount: 0.6) : context.primaryColor.lighten(amount: 0.8); // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id - final isFromDto = asset.id == Isar.autoIncrement; + final isFromDto = asset.id == noDbId; Widget buildSelectionIcon(Asset asset) { if (isSelected) { diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 7e6136c256192..c3f1390dba04a 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -9,7 +9,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; -import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; @@ -181,20 +181,12 @@ class BottomGalleryBar extends ConsumerWidget { ); return; } - ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context); + ref.read(downloadStateProvider.notifier).shareAsset(asset, context); } void handleEdit() async { final image = Image(image: ImmichImage.imageProvider(asset: asset)); - if (asset.isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_edit_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } + Navigator.of(context).push( MaterialPageRoute( builder: (context) => EditImagePage( @@ -229,7 +221,7 @@ class BottomGalleryBar extends ConsumerWidget { return; } - ref.read(imageViewerStateProvider.notifier).downloadAsset( + ref.read(downloadStateProvider.notifier).downloadAsset( asset, context, ); diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 18ef394e2d266..3fdd40130a710 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -8,7 +8,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/asset_description.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:logging/logging.dart'; @@ -29,14 +29,16 @@ class DescriptionInput extends HookConsumerWidget { final focusNode = useFocusNode(); final isFocus = useState(false); final isTextEmpty = useState(controller.text.isEmpty); - final descriptionProvider = ref.watch(assetDescriptionServiceProvider); + final assetService = ref.watch(assetServiceProvider); final owner = ref.watch(currentUserProvider); final hasError = useState(false); final assetWithExif = ref.watch(assetDetailProvider(asset)); useEffect( () { - controller.text = descriptionProvider.getAssetDescription(asset); + assetService + .getDescription(asset) + .then((value) => controller.text = value); return null; }, [assetWithExif.value], @@ -45,7 +47,7 @@ class DescriptionInput extends HookConsumerWidget { submitDescription(String description) async { hasError.value = false; try { - await descriptionProvider.setDescription( + await assetService.setDescription( asset, description, ); diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 6de8f5da33944..f400224e0a0be 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -5,7 +5,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/widgets/album/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/download.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/top_control_app_bar.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; @@ -94,7 +94,7 @@ class GalleryAppBar extends ConsumerWidget { } handleDownloadAsset() { - ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context); + ref.read(downloadStateProvider.notifier).downloadAsset(asset, context); } return IgnorePointer( diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 2157a1aebbf36..984b61f50cc05 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -183,8 +183,7 @@ class TopControlAppBar extends HookConsumerWidget { if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.livePhotoVideoId != null) buildLivePhotoButton(), if (asset.isLocal && !asset.isRemote) buildUploadButton(), - if (asset.isRemote && !asset.isLocal && !asset.isOffline && isOwner) - buildDownloadButton(), + if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(), if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed) buildAddToAlbumButton(), if (asset.isTrashed) buildRestoreButton(), diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart index 0c9cd2d89d33c..7b04855809353 100644 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ b/mobile/lib/widgets/backup/album_info_card.dart @@ -183,23 +183,13 @@ class AlbumInfoCard extends HookConsumerWidget { ), Padding( padding: const EdgeInsets.only(top: 2.0), - child: FutureBuilder( - builder: ((context, snapshot) { - if (snapshot.hasData) { - return Text( - snapshot.data.toString() + - (album.isAll - ? " (${'backup_all'.tr()})" - : ""), - style: TextStyle( - fontSize: 12, - color: Colors.grey[600], - ), - ); - } - return const Text("0"); - }), - future: album.assetCount, + child: Text( + album.assetCount.toString() + + (album.isAll ? " (${'backup_all'.tr()})" : ""), + style: TextStyle( + fontSize: 12, + color: Colors.grey[600], + ), ), ), ], @@ -208,7 +198,7 @@ class AlbumInfoCard extends HookConsumerWidget { IconButton( onPressed: () { context.pushRoute( - AlbumPreviewRoute(album: album.albumEntity), + AlbumPreviewRoute(album: album.album), ); }, icon: Icon( diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index d326bad3e0fc7..a263c004bdf2c 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -1,6 +1,5 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -24,19 +23,10 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - final assetCount = useState(0); final syncAlbum = ref .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.syncAlbums); - useEffect( - () { - album.assetCount.then((value) => assetCount.value = value); - return null; - }, - [album], - ); - buildTileColor() { if (isSelected) { return context.isDarkTheme @@ -117,11 +107,11 @@ class AlbumInfoListTile extends HookConsumerWidget { fontWeight: FontWeight.bold, ), ), - subtitle: Text(assetCount.value.toString()), + subtitle: Text(album.assetCount.toString()), trailing: IconButton( onPressed: () { context.pushRoute( - AlbumPreviewRoute(album: album.albumEntity), + AlbumPreviewRoute(album: album.album), ); }, icon: Icon( diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart index 8e58905aaa51f..f2f84e271f155 100644 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart @@ -2,18 +2,19 @@ import 'dart:io'; import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/foundation.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/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; +import 'package:immich_mobile/repositories/asset_media.repository.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class CurrentUploadingAssetInfoBox extends HookConsumerWidget { const CurrentUploadingAssetInfoBox({super.key}); @@ -148,17 +149,6 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ); } - buildAssetThumbnail() async { - var assetEntity = await AssetEntity.fromId(asset.id); - - if (assetEntity != null) { - return assetEntity.thumbnailDataWithSize( - const ThumbnailSize(500, 500), - quality: 100, - ); - } - } - buildiCloudDownloadProgerssBar() { if (asset.iCloudAsset != null && asset.iCloudAsset!) { return Padding( @@ -239,8 +229,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ); } - return FutureBuilder( - future: buildAssetThumbnail(), + return FutureBuilder( + future: ref.read(assetMediaRepositoryProvider).get(asset.id), builder: (context, thumbnail) => ListTile( isThreeLine: true, leading: AnimatedCrossFade( @@ -250,9 +240,8 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: thumbnail.hasData ? ClipRRect( borderRadius: BorderRadius.circular(5), - child: Image.memory( - thumbnail.data!, - fit: BoxFit.cover, + child: ImmichThumbnail( + asset: thumbnail.data, width: 50, height: 50, ), diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index 14a4e89dd6066..01b717ef5b977 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; +import 'package:immich_mobile/utils/provider_utils.dart'; import 'package:immich_mobile/utils/version_compatibility.dart'; import 'package:immich_mobile/widgets/common/immich_logo.dart'; import 'package:immich_mobile/widgets/common/immich_title_text.dart'; @@ -175,7 +176,7 @@ class LoginForm extends HookConsumerWidget { populateTestLoginInfo1() { usernameController.text = 'testuser@email.com'; passwordController.text = 'password'; - serverEndpointController.text = 'http://10.1.15.216:2283/api'; + serverEndpointController.text = 'http://192.168.1.16:2283/api'; } login() async { @@ -186,6 +187,9 @@ class LoginForm extends HookConsumerWidget { // This will remove current cache asset state of previous user login. ref.read(assetProvider.notifier).clearAllAsset(); + // Invalidate all api repository provider instance to take into account new access token + invalidateAllApiRepositoryProviders(ref); + try { final isAuthenticated = await ref.read(authenticationProvider.notifier).login( diff --git a/mobile/lib/widgets/search/search_filter/people_picker.dart b/mobile/lib/widgets/search/search_filter/people_picker.dart index d79ae5bd95d3b..dfc435c807158 100644 --- a/mobile/lib/widgets/search/search_filter/people_picker.dart +++ b/mobile/lib/widgets/search/search_filter/people_picker.dart @@ -3,23 +3,23 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/interfaces/person_api.interface.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; -import 'package:openapi/api.dart'; class PeoplePicker extends HookConsumerWidget { const PeoplePicker({super.key, required this.onSelect, this.filter}); - final Function(Set) onSelect; - final Set? filter; + final Function(Set) onSelect; + final Set? filter; @override Widget build(BuildContext context, WidgetRef ref) { var imageSize = 45.0; final people = ref.watch(getAllPeopleProvider); final headers = ApiService.getRequestHeaders(); - final selectedPeople = useState>(filter ?? {}); + final selectedPeople = useState>(filter ?? {}); return people.widgetWhen( onData: (people) { diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index bb845157979b6..81827a9079e5a 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.114.0 +- API version: 1.116.2 - Generator version: 7.8.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen @@ -116,6 +116,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets | +*DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | *DuplicatesApi* | [**getAssetDuplicates**](doc//DuplicatesApi.md#getassetduplicates) | **GET** /duplicates | @@ -124,6 +125,7 @@ Class | Method | HTTP request | Description *FileReportsApi* | [**fixAuditFiles**](doc//FileReportsApi.md#fixauditfiles) | **POST** /reports/fix | *FileReportsApi* | [**getAuditFiles**](doc//FileReportsApi.md#getauditfiles) | **GET** /reports | *FileReportsApi* | [**getFileChecksums**](doc//FileReportsApi.md#getfilechecksums) | **POST** /reports/checksum | +*JobsApi* | [**createJob**](doc//JobsApi.md#createjob) | **POST** /jobs | *JobsApi* | [**getAllJobsStatus**](doc//JobsApi.md#getalljobsstatus) | **GET** /jobs | *JobsApi* | [**sendJobCommand**](doc//JobsApi.md#sendjobcommand) | **PUT** /jobs/{id} | *LibrariesApi* | [**createLibrary**](doc//LibrariesApi.md#createlibrary) | **POST** /libraries | @@ -131,12 +133,10 @@ Class | Method | HTTP request | Description *LibrariesApi* | [**getAllLibraries**](doc//LibrariesApi.md#getalllibraries) | **GET** /libraries | *LibrariesApi* | [**getLibrary**](doc//LibrariesApi.md#getlibrary) | **GET** /libraries/{id} | *LibrariesApi* | [**getLibraryStatistics**](doc//LibrariesApi.md#getlibrarystatistics) | **GET** /libraries/{id}/statistics | -*LibrariesApi* | [**removeOfflineFiles**](doc//LibrariesApi.md#removeofflinefiles) | **POST** /libraries/{id}/removeOffline | *LibrariesApi* | [**scanLibrary**](doc//LibrariesApi.md#scanlibrary) | **POST** /libraries/{id}/scan | *LibrariesApi* | [**updateLibrary**](doc//LibrariesApi.md#updatelibrary) | **PUT** /libraries/{id} | *LibrariesApi* | [**validate**](doc//LibrariesApi.md#validate) | **POST** /libraries/{id}/validate | *MapApi* | [**getMapMarkers**](doc//MapApi.md#getmapmarkers) | **GET** /map/markers | -*MapApi* | [**getMapStyle**](doc//MapApi.md#getmapstyle) | **GET** /map/style.json | *MapApi* | [**reverseGeocode**](doc//MapApi.md#reversegeocode) | **GET** /map/reverse-geocode | *MemoriesApi* | [**addMemoryAssets**](doc//MemoriesApi.md#addmemoryassets) | **PUT** /memories/{id}/assets | *MemoriesApi* | [**createMemory**](doc//MemoriesApi.md#creatememory) | **POST** /memories | @@ -171,6 +171,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**searchMetadata**](doc//SearchApi.md#searchmetadata) | **POST** /search/metadata | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | +*SearchApi* | [**searchRandom**](doc//SearchApi.md#searchrandom) | **POST** /search/random | *SearchApi* | [**searchSmart**](doc//SearchApi.md#searchsmart) | **POST** /search/smart | *ServerApi* | [**deleteServerLicense**](doc//ServerApi.md#deleteserverlicense) | **DELETE** /server/license | *ServerApi* | [**getAboutInfo**](doc//ServerApi.md#getaboutinfo) | **GET** /server/about | @@ -330,6 +331,7 @@ Class | Method | HTTP request | Description - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) - [JobCountsDto](doc//JobCountsDto.md) + - [JobCreateDto](doc//JobCreateDto.md) - [JobName](doc//JobName.md) - [JobSettingsDto](doc//JobSettingsDto.md) - [JobStatusDto](doc//JobStatusDto.md) @@ -341,9 +343,9 @@ Class | Method | HTTP request | Description - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) - [LogoutResponseDto](doc//LogoutResponseDto.md) + - [ManualJobName](doc//ManualJobName.md) - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - - [MapTheme](doc//MapTheme.md) - [MemoriesResponse](doc//MemoriesResponse.md) - [MemoriesUpdate](doc//MemoriesUpdate.md) - [MemoryCreateDto](doc//MemoryCreateDto.md) @@ -376,12 +378,12 @@ Class | Method | HTTP request | Description - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) + - [RandomSearchDto](doc//RandomSearchDto.md) - [RatingsResponse](doc//RatingsResponse.md) - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) - - [ScanLibraryDto](doc//ScanLibraryDto.md) - [SearchAlbumResponseDto](doc//SearchAlbumResponseDto.md) - [SearchAssetResponseDto](doc//SearchAssetResponseDto.md) - [SearchExploreItem](doc//SearchExploreItem.md) @@ -414,6 +416,7 @@ Class | Method | HTTP request | Description - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md) + - [SystemConfigGeneratedImageDto](doc//SystemConfigGeneratedImageDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md) - [SystemConfigJobDto](doc//SystemConfigJobDto.md) - [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md) @@ -444,11 +447,13 @@ Class | Method | HTTP request | Description - [TagUpsertDto](doc//TagUpsertDto.md) - [TagsResponse](doc//TagsResponse.md) - [TagsUpdate](doc//TagsUpdate.md) + - [TestEmailResponseDto](doc//TestEmailResponseDto.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketSize](doc//TimeBucketSize.md) - [ToneMapping](doc//ToneMapping.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodePolicy](doc//TranscodePolicy.md) + - [TrashResponseDto](doc//TrashResponseDto.md) - [UpdateAlbumDto](doc//UpdateAlbumDto.md) - [UpdateAlbumUserDto](doc//UpdateAlbumUserDto.md) - [UpdateAssetDto](doc//UpdateAssetDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 091e900145ab3..8be44029805d5 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -144,6 +144,7 @@ part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; part 'model/job_counts_dto.dart'; +part 'model/job_create_dto.dart'; part 'model/job_name.dart'; part 'model/job_settings_dto.dart'; part 'model/job_status_dto.dart'; @@ -155,9 +156,9 @@ part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; part 'model/logout_response_dto.dart'; +part 'model/manual_job_name.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; -part 'model/map_theme.dart'; part 'model/memories_response.dart'; part 'model/memories_update.dart'; part 'model/memory_create_dto.dart'; @@ -190,12 +191,12 @@ part 'model/places_response_dto.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; part 'model/queue_status_dto.dart'; +part 'model/random_search_dto.dart'; part 'model/ratings_response.dart'; part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; -part 'model/scan_library_dto.dart'; part 'model/search_album_response_dto.dart'; part 'model/search_asset_response_dto.dart'; part 'model/search_explore_item.dart'; @@ -228,6 +229,7 @@ part 'model/stack_update_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_faces_dto.dart'; +part 'model/system_config_generated_image_dto.dart'; part 'model/system_config_image_dto.dart'; part 'model/system_config_job_dto.dart'; part 'model/system_config_library_dto.dart'; @@ -258,11 +260,13 @@ part 'model/tag_update_dto.dart'; part 'model/tag_upsert_dto.dart'; part 'model/tags_response.dart'; part 'model/tags_update.dart'; +part 'model/test_email_response_dto.dart'; part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_size.dart'; part 'model/tone_mapping.dart'; part 'model/transcode_hw_accel.dart'; part 'model/transcode_policy.dart'; +part 'model/trash_response_dto.dart'; part 'model/update_album_dto.dart'; part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index ceba3574cd17a..fd899869803fd 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -449,7 +449,10 @@ class AssetsApi { return null; } - /// Performs an HTTP 'GET /assets/random' operation and returns the [Response]. + /// This property was deprecated in v1.116.0 + /// + /// Note: This method returns the HTTP [Response]. + /// /// Parameters: /// /// * [num] count: @@ -482,6 +485,8 @@ class AssetsApi { ); } + /// This property was deprecated in v1.116.0 + /// /// Parameters: /// /// * [num] count: @@ -828,14 +833,12 @@ class AssetsApi { /// /// * [bool] isFavorite: /// - /// * [bool] isOffline: - /// /// * [bool] isVisible: /// /// * [String] livePhotoVideoId: /// /// * [MultipartFile] sidecarData: - Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isOffline, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { + Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { // ignore: prefer_const_declarations final path = r'/assets'; @@ -891,10 +894,6 @@ class AssetsApi { hasFields = true; mp.fields[r'isFavorite'] = parameterToString(isFavorite); } - if (isOffline != null) { - hasFields = true; - mp.fields[r'isOffline'] = parameterToString(isOffline); - } if (isVisible != null) { hasFields = true; mp.fields[r'isVisible'] = parameterToString(isVisible); @@ -946,15 +945,13 @@ class AssetsApi { /// /// * [bool] isFavorite: /// - /// * [bool] isOffline: - /// /// * [bool] isVisible: /// /// * [String] livePhotoVideoId: /// /// * [MultipartFile] sidecarData: - Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isOffline, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { - final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isOffline: isOffline, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, ); + Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? xImmichChecksum, String? duration, bool? isArchived, bool? isFavorite, bool? isVisible, String? livePhotoVideoId, MultipartFile? sidecarData, }) async { + final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, xImmichChecksum: xImmichChecksum, duration: duration, isArchived: isArchived, isFavorite: isFavorite, isVisible: isVisible, livePhotoVideoId: livePhotoVideoId, sidecarData: sidecarData, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 96cb3c2ef0ad2..bc8f50092a030 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -71,4 +71,63 @@ class DeprecatedApi { } return null; } + + /// This property was deprecated in v1.116.0 + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [num] count: + Future getRandomWithHttpInfo({ num? count, }) async { + // ignore: prefer_const_declarations + final path = r'/assets/random'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (count != null) { + queryParams.addAll(_queryParams('', 'count', count)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This property was deprecated in v1.116.0 + /// + /// Parameters: + /// + /// * [num] count: + Future?> getRandom({ num? count, }) async { + final response = await getRandomWithHttpInfo( count: count, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } } diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 5f9501d126f8e..78afc15c93580 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -16,6 +16,45 @@ class JobsApi { final ApiClient apiClient; + /// Performs an HTTP 'POST /jobs' operation and returns the [Response]. + /// Parameters: + /// + /// * [JobCreateDto] jobCreateDto (required): + Future createJobWithHttpInfo(JobCreateDto jobCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/jobs'; + + // ignore: prefer_final_locals + Object? postBody = jobCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [JobCreateDto] jobCreateDto (required): + Future createJob(JobCreateDto jobCreateDto,) async { + final response = await createJobWithHttpInfo(jobCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + /// Performs an HTTP 'GET /jobs' operation and returns the [Response]. Future getAllJobsStatusWithHttpInfo() async { // ignore: prefer_const_declarations diff --git a/mobile/openapi/lib/api/libraries_api.dart b/mobile/openapi/lib/api/libraries_api.dart index 53ab0e19ce77b..36d98d9a88a78 100644 --- a/mobile/openapi/lib/api/libraries_api.dart +++ b/mobile/openapi/lib/api/libraries_api.dart @@ -243,13 +243,13 @@ class LibrariesApi { return null; } - /// Performs an HTTP 'POST /libraries/{id}/removeOffline' operation and returns the [Response]. + /// Performs an HTTP 'POST /libraries/{id}/scan' operation and returns the [Response]. /// Parameters: /// /// * [String] id (required): - Future removeOfflineFilesWithHttpInfo(String id,) async { + Future scanLibraryWithHttpInfo(String id,) async { // ignore: prefer_const_declarations - final path = r'/libraries/{id}/removeOffline' + final path = r'/libraries/{id}/scan' .replaceAll('{id}', id); // ignore: prefer_final_locals @@ -276,52 +276,8 @@ class LibrariesApi { /// Parameters: /// /// * [String] id (required): - Future removeOfflineFiles(String id,) async { - final response = await removeOfflineFilesWithHttpInfo(id,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - - /// Performs an HTTP 'POST /libraries/{id}/scan' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [ScanLibraryDto] scanLibraryDto (required): - Future scanLibraryWithHttpInfo(String id, ScanLibraryDto scanLibraryDto,) async { - // ignore: prefer_const_declarations - final path = r'/libraries/{id}/scan' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody = scanLibraryDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'POST', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [ScanLibraryDto] scanLibraryDto (required): - Future scanLibrary(String id, ScanLibraryDto scanLibraryDto,) async { - final response = await scanLibraryWithHttpInfo(id, scanLibraryDto,); + Future scanLibrary(String id,) async { + final response = await scanLibraryWithHttpInfo(id,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api/map_api.dart b/mobile/openapi/lib/api/map_api.dart index 2846dae6c3582..9644fbfc5c08b 100644 --- a/mobile/openapi/lib/api/map_api.dart +++ b/mobile/openapi/lib/api/map_api.dart @@ -105,62 +105,6 @@ class MapApi { return null; } - /// Performs an HTTP 'GET /map/style.json' operation and returns the [Response]. - /// Parameters: - /// - /// * [MapTheme] theme (required): - /// - /// * [String] key: - Future getMapStyleWithHttpInfo(MapTheme theme, { String? key, }) async { - // ignore: prefer_const_declarations - final path = r'/map/style.json'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - queryParams.addAll(_queryParams('', 'theme', theme)); - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [MapTheme] theme (required): - /// - /// * [String] key: - Future getMapStyle(MapTheme theme, { String? key, }) async { - final response = await getMapStyleWithHttpInfo(theme, key: key, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'Object',) as Object; - - } - return null; - } - /// Performs an HTTP 'GET /map/reverse-geocode' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index a3506b9bc1a7f..0681d582479ad 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -48,10 +48,18 @@ class NotificationsApi { /// Parameters: /// /// * [SystemConfigSmtpDto] systemConfigSmtpDto (required): - Future sendTestEmail(SystemConfigSmtpDto systemConfigSmtpDto,) async { + Future sendTestEmail(SystemConfigSmtpDto systemConfigSmtpDto,) async { final response = await sendTestEmailWithHttpInfo(systemConfigSmtpDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TestEmailResponseDto',) as TestEmailResponseDto; + + } + return null; } } diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 4b6cdfea78aa4..985029f106d27 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -351,6 +351,56 @@ class SearchApi { return null; } + /// Performs an HTTP 'POST /search/random' operation and returns the [Response]. + /// Parameters: + /// + /// * [RandomSearchDto] randomSearchDto (required): + Future searchRandomWithHttpInfo(RandomSearchDto randomSearchDto,) async { + // ignore: prefer_const_declarations + final path = r'/search/random'; + + // ignore: prefer_final_locals + Object? postBody = randomSearchDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [RandomSearchDto] randomSearchDto (required): + Future?> searchRandom(RandomSearchDto randomSearchDto,) async { + final response = await searchRandomWithHttpInfo(randomSearchDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// Performs an HTTP 'POST /search/smart' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/trash_api.dart b/mobile/openapi/lib/api/trash_api.dart index 9c346870ecea1..8f8c6ffb3a190 100644 --- a/mobile/openapi/lib/api/trash_api.dart +++ b/mobile/openapi/lib/api/trash_api.dart @@ -42,11 +42,19 @@ class TrashApi { ); } - Future emptyTrash() async { + Future emptyTrash() async { final response = await emptyTrashWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto; + + } + return null; } /// Performs an HTTP 'POST /trash/restore/assets' operation and returns the [Response]. @@ -81,11 +89,19 @@ class TrashApi { /// Parameters: /// /// * [BulkIdsDto] bulkIdsDto (required): - Future restoreAssets(BulkIdsDto bulkIdsDto,) async { + Future restoreAssets(BulkIdsDto bulkIdsDto,) async { final response = await restoreAssetsWithHttpInfo(bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto; + + } + return null; } /// Performs an HTTP 'POST /trash/restore' operation and returns the [Response]. @@ -114,10 +130,18 @@ class TrashApi { ); } - Future restoreTrash() async { + Future restoreTrash() async { final response = await restoreTrashWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TrashResponseDto',) as TrashResponseDto; + + } + return null; } } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 9ec00aecc87aa..9e38eaf30a8a9 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -166,7 +166,6 @@ class ApiClient { /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { - upgradeDto(value, targetType); try { switch (targetType) { case 'String': @@ -343,6 +342,8 @@ class ApiClient { return JobCommandDto.fromJson(value); case 'JobCountsDto': return JobCountsDto.fromJson(value); + case 'JobCreateDto': + return JobCreateDto.fromJson(value); case 'JobName': return JobNameTypeTransformer().decode(value); case 'JobSettingsDto': @@ -365,12 +366,12 @@ class ApiClient { return LoginResponseDto.fromJson(value); case 'LogoutResponseDto': return LogoutResponseDto.fromJson(value); + case 'ManualJobName': + return ManualJobNameTypeTransformer().decode(value); case 'MapMarkerResponseDto': return MapMarkerResponseDto.fromJson(value); case 'MapReverseGeocodeResponseDto': return MapReverseGeocodeResponseDto.fromJson(value); - case 'MapTheme': - return MapThemeTypeTransformer().decode(value); case 'MemoriesResponse': return MemoriesResponse.fromJson(value); case 'MemoriesUpdate': @@ -435,6 +436,8 @@ class ApiClient { return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); + case 'RandomSearchDto': + return RandomSearchDto.fromJson(value); case 'RatingsResponse': return RatingsResponse.fromJson(value); case 'RatingsUpdate': @@ -445,8 +448,6 @@ class ApiClient { return ReactionTypeTypeTransformer().decode(value); case 'ReverseGeocodingStateResponseDto': return ReverseGeocodingStateResponseDto.fromJson(value); - case 'ScanLibraryDto': - return ScanLibraryDto.fromJson(value); case 'SearchAlbumResponseDto': return SearchAlbumResponseDto.fromJson(value); case 'SearchAssetResponseDto': @@ -511,6 +512,8 @@ class ApiClient { return SystemConfigFFmpegDto.fromJson(value); case 'SystemConfigFacesDto': return SystemConfigFacesDto.fromJson(value); + case 'SystemConfigGeneratedImageDto': + return SystemConfigGeneratedImageDto.fromJson(value); case 'SystemConfigImageDto': return SystemConfigImageDto.fromJson(value); case 'SystemConfigJobDto': @@ -571,6 +574,8 @@ class ApiClient { return TagsResponse.fromJson(value); case 'TagsUpdate': return TagsUpdate.fromJson(value); + case 'TestEmailResponseDto': + return TestEmailResponseDto.fromJson(value); case 'TimeBucketResponseDto': return TimeBucketResponseDto.fromJson(value); case 'TimeBucketSize': @@ -581,6 +586,8 @@ class ApiClient { return TranscodeHWAccelTypeTransformer().decode(value); case 'TranscodePolicy': return TranscodePolicyTypeTransformer().decode(value); + case 'TrashResponseDto': + return TrashResponseDto.fromJson(value); case 'UpdateAlbumDto': return UpdateAlbumDto.fromJson(value); case 'UpdateAlbumUserDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 8dcef880f59a4..b7c6ad5e010d3 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -97,8 +97,8 @@ String parameterToString(dynamic value) { if (value is LogLevel) { return LogLevelTypeTransformer().encode(value).toString(); } - if (value is MapTheme) { - return MapThemeTypeTransformer().encode(value).toString(); + if (value is ManualJobName) { + return ManualJobNameTypeTransformer().encode(value).toString(); } if (value is MemoryType) { return MemoryTypeTypeTransformer().encode(value).toString(); diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index b54fa2ca72bce..ce4b4a01766a4 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -78,6 +78,7 @@ class ActivityCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ActivityCreateDto? fromJson(dynamic value) { + upgradeDto(value, "ActivityCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index bfffd8485b0a9..25fb0f53f8707 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -78,6 +78,7 @@ class ActivityResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ActivityResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ActivityResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 20d4696b1b60e..ad0b814a58774 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -40,6 +40,7 @@ class ActivityStatisticsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ActivityStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ActivityStatisticsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/add_users_dto.dart b/mobile/openapi/lib/model/add_users_dto.dart index 2daa571265da6..531c1ec785b45 100644 --- a/mobile/openapi/lib/model/add_users_dto.dart +++ b/mobile/openapi/lib/model/add_users_dto.dart @@ -40,6 +40,7 @@ class AddUsersDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AddUsersDto? fromJson(dynamic value) { + upgradeDto(value, "AddUsersDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart index 2277f0958c813..298bf318a292b 100644 --- a/mobile/openapi/lib/model/admin_onboarding_update_dto.dart +++ b/mobile/openapi/lib/model/admin_onboarding_update_dto.dart @@ -40,6 +40,7 @@ class AdminOnboardingUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AdminOnboardingUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "AdminOnboardingUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index c98a95775d2c8..547a6a70fd221 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -186,6 +186,7 @@ class AlbumResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 90dbe520163bb..9e19002cf18c2 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -52,6 +52,7 @@ class AlbumStatisticsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumStatisticsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index e654a2ff5d7b0..3f72d5c893e18 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -56,6 +56,7 @@ class AlbumUserAddDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumUserAddDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumUserAddDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 708acd472beed..93a0661b30251 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -46,6 +46,7 @@ class AlbumUserCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumUserCreateDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumUserCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8f86cf254ea92..bbae03fba74c3 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -46,6 +46,7 @@ class AlbumUserResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AlbumUserResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AlbumUserResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart index 1ee5253c38bde..6ec248a638e80 100644 --- a/mobile/openapi/lib/model/all_job_status_response_dto.dart +++ b/mobile/openapi/lib/model/all_job_status_response_dto.dart @@ -118,6 +118,7 @@ class AllJobStatusResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AllJobStatusResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AllJobStatusResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index 433855c4cfe17..848774e9c9cf7 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -56,6 +56,7 @@ class APIKeyCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyCreateDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_create_response_dto.dart b/mobile/openapi/lib/model/api_key_create_response_dto.dart index 93065654ac331..cdaa70e37de71 100644 --- a/mobile/openapi/lib/model/api_key_create_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_response_dto.dart @@ -46,6 +46,7 @@ class APIKeyCreateResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyCreateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyCreateResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index b6ca86c050944..fd0d91f6737e3 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -64,6 +64,7 @@ class APIKeyResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyResponseDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/api_key_update_dto.dart b/mobile/openapi/lib/model/api_key_update_dto.dart index 318f4936e16b2..7295d1ea1f19b 100644 --- a/mobile/openapi/lib/model/api_key_update_dto.dart +++ b/mobile/openapi/lib/model/api_key_update_dto.dart @@ -40,6 +40,7 @@ class APIKeyUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static APIKeyUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "APIKeyUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart index 0f6913a7f4ebc..c4453054b1b92 100644 --- a/mobile/openapi/lib/model/asset_bulk_delete_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_delete_dto.dart @@ -56,6 +56,7 @@ class AssetBulkDeleteDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkDeleteDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index c9b21683fbcec..da23d2f09d2e0 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -148,6 +148,7 @@ class AssetBulkUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart index 55ea41b598e75..36c13bfdf6889 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_dto.dart @@ -40,6 +40,7 @@ class AssetBulkUploadCheckDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart index 16294cdae6d06..13dfa340fad0c 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_item.dart @@ -47,6 +47,7 @@ class AssetBulkUploadCheckItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckItem? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart index 5bfacbff570d2..8c3651e9fa189 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_response_dto.dart @@ -40,6 +40,7 @@ class AssetBulkUploadCheckResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index a016b357e7e6b..88e46dae7daa7 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -88,6 +88,7 @@ class AssetBulkUploadCheckResult { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetBulkUploadCheckResult? fromJson(dynamic value) { + upgradeDto(value, "AssetBulkUploadCheckResult"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart index a5ee10f33e17e..845aadcdcd3b4 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_dto.dart @@ -46,6 +46,7 @@ class AssetDeltaSyncDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetDeltaSyncDto? fromJson(dynamic value) { + upgradeDto(value, "AssetDeltaSyncDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart index 3b14fa68cf8e7..a64e1a2fbee42 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart @@ -52,6 +52,7 @@ class AssetDeltaSyncResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetDeltaSyncResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetDeltaSyncResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 7a8588ce5c4af..c05b511649236 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -102,6 +102,7 @@ class AssetFaceResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_update_dto.dart b/mobile/openapi/lib/model/asset_face_update_dto.dart index 58def49ae1ae4..71bdde8e9a6a3 100644 --- a/mobile/openapi/lib/model/asset_face_update_dto.dart +++ b/mobile/openapi/lib/model/asset_face_update_dto.dart @@ -40,6 +40,7 @@ class AssetFaceUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_update_item.dart b/mobile/openapi/lib/model/asset_face_update_item.dart index 5ea37ea4db5f5..c2c48032595dc 100644 --- a/mobile/openapi/lib/model/asset_face_update_item.dart +++ b/mobile/openapi/lib/model/asset_face_update_item.dart @@ -46,6 +46,7 @@ class AssetFaceUpdateItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceUpdateItem? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceUpdateItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index ecfe06bd7d6ce..8bf07e15347ca 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -92,6 +92,7 @@ class AssetFaceWithoutPersonResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFaceWithoutPersonResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFaceWithoutPersonResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart index e80638f6b0869..7151094b9588c 100644 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_full_sync_dto.dart @@ -79,6 +79,7 @@ class AssetFullSyncDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetFullSyncDto? fromJson(dynamic value) { + upgradeDto(value, "AssetFullSyncDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_ids_dto.dart b/mobile/openapi/lib/model/asset_ids_dto.dart index c8c7a69b8907c..b44888f3962b3 100644 --- a/mobile/openapi/lib/model/asset_ids_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_dto.dart @@ -40,6 +40,7 @@ class AssetIdsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetIdsDto? fromJson(dynamic value) { + upgradeDto(value, "AssetIdsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index a642c0924cba8..ff63091caa577 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -56,6 +56,7 @@ class AssetIdsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetIdsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetIdsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 16ed2644fd6cd..0f8bfab009fa1 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -46,6 +46,7 @@ class AssetJobsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetJobsDto? fromJson(dynamic value) { + upgradeDto(value, "AssetJobsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index c2801c93cce55..75428ec5f61d8 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -46,6 +46,7 @@ class AssetMediaResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetMediaResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetMediaResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index bfb461efdc4c5..c11dedcbfd230 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -293,6 +293,7 @@ class AssetResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 89d30f7810682..bb4becb129c17 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -52,6 +52,7 @@ class AssetStackResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetStackResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetStackResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index c21d7fdbffa4c..d11ce55a5cc5c 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -52,6 +52,7 @@ class AssetStatsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AssetStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AssetStatsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/audit_deletes_response_dto.dart b/mobile/openapi/lib/model/audit_deletes_response_dto.dart index 690a52e811466..6b1df74eb4d79 100644 --- a/mobile/openapi/lib/model/audit_deletes_response_dto.dart +++ b/mobile/openapi/lib/model/audit_deletes_response_dto.dart @@ -46,6 +46,7 @@ class AuditDeletesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AuditDeletesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "AuditDeletesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/avatar_response.dart b/mobile/openapi/lib/model/avatar_response.dart index edd242df4e3be..8ce0287565f2d 100644 --- a/mobile/openapi/lib/model/avatar_response.dart +++ b/mobile/openapi/lib/model/avatar_response.dart @@ -40,6 +40,7 @@ class AvatarResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AvatarResponse? fromJson(dynamic value) { + upgradeDto(value, "AvatarResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index b92eb8dcbdb2d..875eb138a8dbf 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -50,6 +50,7 @@ class AvatarUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static AvatarUpdate? fromJson(dynamic value) { + upgradeDto(value, "AvatarUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index ef3cf2e0dbe00..67a587e8d001e 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -56,6 +56,7 @@ class BulkIdResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static BulkIdResponseDto? fromJson(dynamic value) { + upgradeDto(value, "BulkIdResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/bulk_ids_dto.dart b/mobile/openapi/lib/model/bulk_ids_dto.dart index 6942875f0a2e6..6a7f8ceeec6f6 100644 --- a/mobile/openapi/lib/model/bulk_ids_dto.dart +++ b/mobile/openapi/lib/model/bulk_ids_dto.dart @@ -40,6 +40,7 @@ class BulkIdsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static BulkIdsDto? fromJson(dynamic value) { + upgradeDto(value, "BulkIdsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/change_password_dto.dart b/mobile/openapi/lib/model/change_password_dto.dart index 1074aaf74d4f8..33b7f4a607390 100644 --- a/mobile/openapi/lib/model/change_password_dto.dart +++ b/mobile/openapi/lib/model/change_password_dto.dart @@ -46,6 +46,7 @@ class ChangePasswordDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ChangePasswordDto? fromJson(dynamic value) { + upgradeDto(value, "ChangePasswordDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/check_existing_assets_dto.dart b/mobile/openapi/lib/model/check_existing_assets_dto.dart index 49ef36cc093e0..42ce6d5c3ea0c 100644 --- a/mobile/openapi/lib/model/check_existing_assets_dto.dart +++ b/mobile/openapi/lib/model/check_existing_assets_dto.dart @@ -46,6 +46,7 @@ class CheckExistingAssetsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CheckExistingAssetsDto? fromJson(dynamic value) { + upgradeDto(value, "CheckExistingAssetsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart index d8b0f43a6d0e1..ad93578ebc34c 100644 --- a/mobile/openapi/lib/model/check_existing_assets_response_dto.dart +++ b/mobile/openapi/lib/model/check_existing_assets_response_dto.dart @@ -40,6 +40,7 @@ class CheckExistingAssetsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CheckExistingAssetsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "CheckExistingAssetsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/clip_config.dart b/mobile/openapi/lib/model/clip_config.dart index 6e95c15fbfbe2..b500d20f2e6eb 100644 --- a/mobile/openapi/lib/model/clip_config.dart +++ b/mobile/openapi/lib/model/clip_config.dart @@ -46,6 +46,7 @@ class CLIPConfig { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CLIPConfig? fromJson(dynamic value) { + upgradeDto(value, "CLIPConfig"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/create_album_dto.dart b/mobile/openapi/lib/model/create_album_dto.dart index fa28b782acee9..ff8c1df647fdc 100644 --- a/mobile/openapi/lib/model/create_album_dto.dart +++ b/mobile/openapi/lib/model/create_album_dto.dart @@ -68,6 +68,7 @@ class CreateAlbumDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CreateAlbumDto? fromJson(dynamic value) { + upgradeDto(value, "CreateAlbumDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 65ceec8e8a4f4..bffa5f427950d 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -68,6 +68,7 @@ class CreateLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CreateLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "CreateLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index c9ae3ea6516bd..ee98142e86097 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -13,30 +13,36 @@ part of openapi.api; class CreateProfileImageResponseDto { /// Returns a new [CreateProfileImageResponseDto] instance. CreateProfileImageResponseDto({ + required this.profileChangedAt, required this.profileImagePath, required this.userId, }); + DateTime profileChangedAt; + String profileImagePath; String userId; @override bool operator ==(Object other) => identical(this, other) || other is CreateProfileImageResponseDto && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath && other.userId == userId; @override int get hashCode => // ignore: unnecessary_parenthesis + (profileChangedAt.hashCode) + (profileImagePath.hashCode) + (userId.hashCode); @override - String toString() => 'CreateProfileImageResponseDto[profileImagePath=$profileImagePath, userId=$userId]'; + String toString() => 'CreateProfileImageResponseDto[profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, userId=$userId]'; Map toJson() { final json = {}; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; json[r'userId'] = this.userId; return json; @@ -46,10 +52,12 @@ class CreateProfileImageResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static CreateProfileImageResponseDto? fromJson(dynamic value) { + upgradeDto(value, "CreateProfileImageResponseDto"); if (value is Map) { final json = value.cast(); return CreateProfileImageResponseDto( + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, userId: mapValueOfType(json, r'userId')!, ); @@ -99,6 +107,7 @@ class CreateProfileImageResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'profileChangedAt', 'profileImagePath', 'userId', }; diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index e324850bdcf0b..5f3fd1a8c1f3d 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -46,6 +46,7 @@ class DownloadArchiveInfo { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadArchiveInfo? fromJson(dynamic value) { + upgradeDto(value, "DownloadArchiveInfo"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index 4c387690102a7..6f4777975c6b2 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -89,6 +89,7 @@ class DownloadInfoDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadInfoDto? fromJson(dynamic value) { + upgradeDto(value, "DownloadInfoDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 25c5159a8b655..041da44b718bc 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -46,6 +46,7 @@ class DownloadResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadResponse? fromJson(dynamic value) { + upgradeDto(value, "DownloadResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index f32cba92537ac..5c6bd112661e8 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -46,6 +46,7 @@ class DownloadResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadResponseDto? fromJson(dynamic value) { + upgradeDto(value, "DownloadResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 2c3839a6878dc..8df825a922315 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -67,6 +67,7 @@ class DownloadUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DownloadUpdate? fromJson(dynamic value) { + upgradeDto(value, "DownloadUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/duplicate_detection_config.dart b/mobile/openapi/lib/model/duplicate_detection_config.dart index 0bc60917848c4..e4fc352028ec0 100644 --- a/mobile/openapi/lib/model/duplicate_detection_config.dart +++ b/mobile/openapi/lib/model/duplicate_detection_config.dart @@ -48,6 +48,7 @@ class DuplicateDetectionConfig { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DuplicateDetectionConfig? fromJson(dynamic value) { + upgradeDto(value, "DuplicateDetectionConfig"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/duplicate_response_dto.dart b/mobile/openapi/lib/model/duplicate_response_dto.dart index b93ecfe5f5775..6ac7c468711e0 100644 --- a/mobile/openapi/lib/model/duplicate_response_dto.dart +++ b/mobile/openapi/lib/model/duplicate_response_dto.dart @@ -46,6 +46,7 @@ class DuplicateResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static DuplicateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "DuplicateResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/email_notifications_response.dart b/mobile/openapi/lib/model/email_notifications_response.dart index cef92957c6d78..d6dcfb9273be6 100644 --- a/mobile/openapi/lib/model/email_notifications_response.dart +++ b/mobile/openapi/lib/model/email_notifications_response.dart @@ -52,6 +52,7 @@ class EmailNotificationsResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static EmailNotificationsResponse? fromJson(dynamic value) { + upgradeDto(value, "EmailNotificationsResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/email_notifications_update.dart b/mobile/openapi/lib/model/email_notifications_update.dart index dcd1ec432206d..dad0a52fdef28 100644 --- a/mobile/openapi/lib/model/email_notifications_update.dart +++ b/mobile/openapi/lib/model/email_notifications_update.dart @@ -82,6 +82,7 @@ class EmailNotificationsUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static EmailNotificationsUpdate? fromJson(dynamic value) { + upgradeDto(value, "EmailNotificationsUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 0185f300fac5b..17397b20815e0 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -254,6 +254,7 @@ class ExifResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ExifResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ExifResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/face_dto.dart b/mobile/openapi/lib/model/face_dto.dart index 4fcc86debf22e..c84a518b8c784 100644 --- a/mobile/openapi/lib/model/face_dto.dart +++ b/mobile/openapi/lib/model/face_dto.dart @@ -40,6 +40,7 @@ class FaceDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FaceDto? fromJson(dynamic value) { + upgradeDto(value, "FaceDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 52400fd7e135d..439efbbfaeeb1 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -22,14 +22,14 @@ class FacialRecognitionConfig { bool enabled; - /// Minimum value: 0 + /// Minimum value: 0.1 /// Maximum value: 2 double maxDistance; /// Minimum value: 1 int minFaces; - /// Minimum value: 0 + /// Minimum value: 0.1 /// Maximum value: 1 double minScore; @@ -69,6 +69,7 @@ class FacialRecognitionConfig { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FacialRecognitionConfig? fromJson(dynamic value) { + upgradeDto(value, "FacialRecognitionConfig"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_checksum_dto.dart b/mobile/openapi/lib/model/file_checksum_dto.dart index c7e8aa1da60b3..7dc9ccdf2f93e 100644 --- a/mobile/openapi/lib/model/file_checksum_dto.dart +++ b/mobile/openapi/lib/model/file_checksum_dto.dart @@ -40,6 +40,7 @@ class FileChecksumDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileChecksumDto? fromJson(dynamic value) { + upgradeDto(value, "FileChecksumDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_checksum_response_dto.dart b/mobile/openapi/lib/model/file_checksum_response_dto.dart index d4bae3c273ddc..7b963c8bd539e 100644 --- a/mobile/openapi/lib/model/file_checksum_response_dto.dart +++ b/mobile/openapi/lib/model/file_checksum_response_dto.dart @@ -46,6 +46,7 @@ class FileChecksumResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileChecksumResponseDto? fromJson(dynamic value) { + upgradeDto(value, "FileChecksumResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_report_dto.dart b/mobile/openapi/lib/model/file_report_dto.dart index 422215ff6c63e..3dc892e5e7f78 100644 --- a/mobile/openapi/lib/model/file_report_dto.dart +++ b/mobile/openapi/lib/model/file_report_dto.dart @@ -46,6 +46,7 @@ class FileReportDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileReportDto? fromJson(dynamic value) { + upgradeDto(value, "FileReportDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_report_fix_dto.dart b/mobile/openapi/lib/model/file_report_fix_dto.dart index cf09242b0fa15..d46cdeb4b784b 100644 --- a/mobile/openapi/lib/model/file_report_fix_dto.dart +++ b/mobile/openapi/lib/model/file_report_fix_dto.dart @@ -40,6 +40,7 @@ class FileReportFixDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileReportFixDto? fromJson(dynamic value) { + upgradeDto(value, "FileReportFixDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/file_report_item_dto.dart b/mobile/openapi/lib/model/file_report_item_dto.dart index 5255005daaaf1..1ef08c2b48511 100644 --- a/mobile/openapi/lib/model/file_report_item_dto.dart +++ b/mobile/openapi/lib/model/file_report_item_dto.dart @@ -74,6 +74,7 @@ class FileReportItemDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FileReportItemDto? fromJson(dynamic value) { + upgradeDto(value, "FileReportItemDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 5bfc4c793deed..248b64b054c83 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -46,6 +46,7 @@ class FoldersResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FoldersResponse? fromJson(dynamic value) { + upgradeDto(value, "FoldersResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart index 088c98a4d8fd2..02347177545d0 100644 --- a/mobile/openapi/lib/model/folders_update.dart +++ b/mobile/openapi/lib/model/folders_update.dart @@ -66,6 +66,7 @@ class FoldersUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static FoldersUpdate? fromJson(dynamic value) { + upgradeDto(value, "FoldersUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_command_dto.dart b/mobile/openapi/lib/model/job_command_dto.dart index 5c56715644f22..649e0128a7a97 100644 --- a/mobile/openapi/lib/model/job_command_dto.dart +++ b/mobile/openapi/lib/model/job_command_dto.dart @@ -46,6 +46,7 @@ class JobCommandDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobCommandDto? fromJson(dynamic value) { + upgradeDto(value, "JobCommandDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_counts_dto.dart b/mobile/openapi/lib/model/job_counts_dto.dart index cf1d0b457d821..afc90d108467d 100644 --- a/mobile/openapi/lib/model/job_counts_dto.dart +++ b/mobile/openapi/lib/model/job_counts_dto.dart @@ -70,6 +70,7 @@ class JobCountsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobCountsDto? fromJson(dynamic value) { + upgradeDto(value, "JobCountsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart new file mode 100644 index 0000000000000..fe6743cba09d9 --- /dev/null +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class JobCreateDto { + /// Returns a new [JobCreateDto] instance. + JobCreateDto({ + required this.name, + }); + + ManualJobName name; + + @override + bool operator ==(Object other) => identical(this, other) || other is JobCreateDto && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (name.hashCode); + + @override + String toString() => 'JobCreateDto[name=$name]'; + + Map toJson() { + final json = {}; + json[r'name'] = this.name; + return json; + } + + /// Returns a new [JobCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static JobCreateDto? fromJson(dynamic value) { + upgradeDto(value, "JobCreateDto"); + if (value is Map) { + final json = value.cast(); + + return JobCreateDto( + name: ManualJobName.fromJson(json[r'name'])!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = JobCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = JobCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of JobCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = JobCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'name', + }; +} + diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index 9c59d503cac85..af354bef9e953 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -41,6 +41,7 @@ class JobSettingsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobSettingsDto? fromJson(dynamic value) { + upgradeDto(value, "JobSettingsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/job_status_dto.dart b/mobile/openapi/lib/model/job_status_dto.dart index fd925bd53a5fc..18fab8dfb3fe2 100644 --- a/mobile/openapi/lib/model/job_status_dto.dart +++ b/mobile/openapi/lib/model/job_status_dto.dart @@ -46,6 +46,7 @@ class JobStatusDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static JobStatusDto? fromJson(dynamic value) { + upgradeDto(value, "JobStatusDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index e27b48910439c..3cf12485080ac 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -92,6 +92,7 @@ class LibraryResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LibraryResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LibraryResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 8cfb292855104..afe67da31a251 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -58,6 +58,7 @@ class LibraryStatsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LibraryStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LibraryStatsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index aece85f81e9a0..d27d579bb4831 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -46,6 +46,7 @@ class LicenseKeyDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LicenseKeyDto? fromJson(dynamic value) { + upgradeDto(value, "LicenseKeyDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart index f83668af575c9..6d3009433fd80 100644 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ b/mobile/openapi/lib/model/license_response_dto.dart @@ -52,6 +52,7 @@ class LicenseResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LicenseResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LicenseResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/login_credential_dto.dart b/mobile/openapi/lib/model/login_credential_dto.dart index ac2f5116916f2..7e892ab5fbcb6 100644 --- a/mobile/openapi/lib/model/login_credential_dto.dart +++ b/mobile/openapi/lib/model/login_credential_dto.dart @@ -46,6 +46,7 @@ class LoginCredentialDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LoginCredentialDto? fromJson(dynamic value) { + upgradeDto(value, "LoginCredentialDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/login_response_dto.dart b/mobile/openapi/lib/model/login_response_dto.dart index 6a0eb2355ce55..dbc82d07ba1bb 100644 --- a/mobile/openapi/lib/model/login_response_dto.dart +++ b/mobile/openapi/lib/model/login_response_dto.dart @@ -76,6 +76,7 @@ class LoginResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LoginResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LoginResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/logout_response_dto.dart b/mobile/openapi/lib/model/logout_response_dto.dart index ca1e8d23bbf32..aa94904e2a7df 100644 --- a/mobile/openapi/lib/model/logout_response_dto.dart +++ b/mobile/openapi/lib/model/logout_response_dto.dart @@ -46,6 +46,7 @@ class LogoutResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static LogoutResponseDto? fromJson(dynamic value) { + upgradeDto(value, "LogoutResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart new file mode 100644 index 0000000000000..7e8d9d51b2bab --- /dev/null +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -0,0 +1,88 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class ManualJobName { + /// Instantiate a new enum with the provided [value]. + const ManualJobName._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const personCleanup = ManualJobName._(r'person-cleanup'); + static const tagCleanup = ManualJobName._(r'tag-cleanup'); + static const userCleanup = ManualJobName._(r'user-cleanup'); + + /// List of all possible values in this [enum][ManualJobName]. + static const values = [ + personCleanup, + tagCleanup, + userCleanup, + ]; + + static ManualJobName? fromJson(dynamic value) => ManualJobNameTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = ManualJobName.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [ManualJobName] to String, +/// and [decode] dynamic data back to [ManualJobName]. +class ManualJobNameTypeTransformer { + factory ManualJobNameTypeTransformer() => _instance ??= const ManualJobNameTypeTransformer._(); + + const ManualJobNameTypeTransformer._(); + + String encode(ManualJobName data) => data.value; + + /// Decodes a [dynamic value][data] to a ManualJobName. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + ManualJobName? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'person-cleanup': return ManualJobName.personCleanup; + case r'tag-cleanup': return ManualJobName.tagCleanup; + case r'user-cleanup': return ManualJobName.userCleanup; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [ManualJobNameTypeTransformer] instance. + static ManualJobNameTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart index ca1ec3c8a1ced..74ac51a271478 100644 --- a/mobile/openapi/lib/model/map_marker_response_dto.dart +++ b/mobile/openapi/lib/model/map_marker_response_dto.dart @@ -82,6 +82,7 @@ class MapMarkerResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MapMarkerResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MapMarkerResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart index ac99dd91a9915..6d8757d39ff61 100644 --- a/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart +++ b/mobile/openapi/lib/model/map_reverse_geocode_response_dto.dart @@ -64,6 +64,7 @@ class MapReverseGeocodeResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MapReverseGeocodeResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MapReverseGeocodeResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/map_theme.dart b/mobile/openapi/lib/model/map_theme.dart deleted file mode 100644 index e2553790c6cc1..0000000000000 --- a/mobile/openapi/lib/model/map_theme.dart +++ /dev/null @@ -1,85 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class MapTheme { - /// Instantiate a new enum with the provided [value]. - const MapTheme._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const light = MapTheme._(r'light'); - static const dark = MapTheme._(r'dark'); - - /// List of all possible values in this [enum][MapTheme]. - static const values = [ - light, - dark, - ]; - - static MapTheme? fromJson(dynamic value) => MapThemeTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = MapTheme.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [MapTheme] to String, -/// and [decode] dynamic data back to [MapTheme]. -class MapThemeTypeTransformer { - factory MapThemeTypeTransformer() => _instance ??= const MapThemeTypeTransformer._(); - - const MapThemeTypeTransformer._(); - - String encode(MapTheme data) => data.value; - - /// Decodes a [dynamic value][data] to a MapTheme. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - MapTheme? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'light': return MapTheme.light; - case r'dark': return MapTheme.dark; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [MapThemeTypeTransformer] instance. - static MapThemeTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index e215a66a03f67..b9f8b5d8b1862 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -40,6 +40,7 @@ class MemoriesResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoriesResponse? fromJson(dynamic value) { + upgradeDto(value, "MemoriesResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index d30949136197e..71efd71ae7866 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -50,6 +50,7 @@ class MemoriesUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoriesUpdate? fromJson(dynamic value) { + upgradeDto(value, "MemoriesUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 2efdf88936093..15985f2f1c175 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -90,6 +90,7 @@ class MemoryCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryCreateDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_lane_response_dto.dart b/mobile/openapi/lib/model/memory_lane_response_dto.dart index 4abe607381f35..27248d05c1f64 100644 --- a/mobile/openapi/lib/model/memory_lane_response_dto.dart +++ b/mobile/openapi/lib/model/memory_lane_response_dto.dart @@ -46,6 +46,7 @@ class MemoryLaneResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryLaneResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryLaneResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index f794be53cd5d3..652c993536aa4 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -120,6 +120,7 @@ class MemoryResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryResponseDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index 318f4b42add00..e750f9faad330 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -82,6 +82,7 @@ class MemoryUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MemoryUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "MemoryUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/merge_person_dto.dart b/mobile/openapi/lib/model/merge_person_dto.dart index ea23042e2c5e0..fd225276b6f0a 100644 --- a/mobile/openapi/lib/model/merge_person_dto.dart +++ b/mobile/openapi/lib/model/merge_person_dto.dart @@ -40,6 +40,7 @@ class MergePersonDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MergePersonDto? fromJson(dynamic value) { + upgradeDto(value, "MergePersonDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index fabf7a26107ec..0aef1f623efd0 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -637,6 +637,7 @@ class MetadataSearchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static MetadataSearchDto? fromJson(dynamic value) { + upgradeDto(value, "MetadataSearchDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart index ffd017f8168d6..869c3be753f70 100644 --- a/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart +++ b/mobile/openapi/lib/model/o_auth_authorize_response_dto.dart @@ -40,6 +40,7 @@ class OAuthAuthorizeResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OAuthAuthorizeResponseDto? fromJson(dynamic value) { + upgradeDto(value, "OAuthAuthorizeResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/o_auth_callback_dto.dart b/mobile/openapi/lib/model/o_auth_callback_dto.dart index 89ad0f60b0b09..d0b98d5c6f503 100644 --- a/mobile/openapi/lib/model/o_auth_callback_dto.dart +++ b/mobile/openapi/lib/model/o_auth_callback_dto.dart @@ -40,6 +40,7 @@ class OAuthCallbackDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OAuthCallbackDto? fromJson(dynamic value) { + upgradeDto(value, "OAuthCallbackDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/o_auth_config_dto.dart b/mobile/openapi/lib/model/o_auth_config_dto.dart index 7d7675886497b..86c79b4e04ff6 100644 --- a/mobile/openapi/lib/model/o_auth_config_dto.dart +++ b/mobile/openapi/lib/model/o_auth_config_dto.dart @@ -40,6 +40,7 @@ class OAuthConfigDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OAuthConfigDto? fromJson(dynamic value) { + upgradeDto(value, "OAuthConfigDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index be170caf853a9..bfcc4fd630589 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -41,6 +41,7 @@ class OnThisDayDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static OnThisDayDto? fromJson(dynamic value) { + upgradeDto(value, "OnThisDayDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 7c3cf03bd9b44..f61df86b42bf4 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -18,6 +18,7 @@ class PartnerResponseDto { required this.id, this.inTimeline, required this.name, + required this.profileChangedAt, required this.profileImagePath, }); @@ -37,6 +38,8 @@ class PartnerResponseDto { String name; + DateTime profileChangedAt; + String profileImagePath; @override @@ -46,6 +49,7 @@ class PartnerResponseDto { other.id == id && other.inTimeline == inTimeline && other.name == name && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath; @override @@ -56,10 +60,11 @@ class PartnerResponseDto { (id.hashCode) + (inTimeline == null ? 0 : inTimeline!.hashCode) + (name.hashCode) + + (profileChangedAt.hashCode) + (profileImagePath.hashCode); @override - String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileImagePath=$profileImagePath]'; + String toString() => 'PartnerResponseDto[avatarColor=$avatarColor, email=$email, id=$id, inTimeline=$inTimeline, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]'; Map toJson() { final json = {}; @@ -72,6 +77,7 @@ class PartnerResponseDto { // json[r'inTimeline'] = null; } json[r'name'] = this.name; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; return json; } @@ -80,6 +86,7 @@ class PartnerResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PartnerResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PartnerResponseDto"); if (value is Map) { final json = value.cast(); @@ -89,6 +96,7 @@ class PartnerResponseDto { id: mapValueOfType(json, r'id')!, inTimeline: mapValueOfType(json, r'inTimeline'), name: mapValueOfType(json, r'name')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, ); } @@ -141,6 +149,7 @@ class PartnerResponseDto { 'email', 'id', 'name', + 'profileChangedAt', 'profileImagePath', }; } diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index e12f86eeab5ba..1312c738744ef 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -46,6 +46,7 @@ class PeopleResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleResponse? fromJson(dynamic value) { + upgradeDto(value, "PeopleResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index 87e8c34fb0d7d..49f0e85aad421 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -69,6 +69,7 @@ class PeopleResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PeopleResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart index 7803e6297036a..fb4eeeb434fda 100644 --- a/mobile/openapi/lib/model/people_update.dart +++ b/mobile/openapi/lib/model/people_update.dart @@ -66,6 +66,7 @@ class PeopleUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleUpdate? fromJson(dynamic value) { + upgradeDto(value, "PeopleUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_update_dto.dart b/mobile/openapi/lib/model/people_update_dto.dart index 9fcfdc8761018..f771084f75c63 100644 --- a/mobile/openapi/lib/model/people_update_dto.dart +++ b/mobile/openapi/lib/model/people_update_dto.dart @@ -40,6 +40,7 @@ class PeopleUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "PeopleUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/people_update_item.dart b/mobile/openapi/lib/model/people_update_item.dart index 8af0a8b11ab8d..042e4fa36f969 100644 --- a/mobile/openapi/lib/model/people_update_item.dart +++ b/mobile/openapi/lib/model/people_update_item.dart @@ -103,6 +103,7 @@ class PeopleUpdateItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PeopleUpdateItem? fromJson(dynamic value) { + upgradeDto(value, "PeopleUpdateItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_create_dto.dart b/mobile/openapi/lib/model/person_create_dto.dart index 9889328dee639..36bd6dfee9072 100644 --- a/mobile/openapi/lib/model/person_create_dto.dart +++ b/mobile/openapi/lib/model/person_create_dto.dart @@ -79,6 +79,7 @@ class PersonCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonCreateDto? fromJson(dynamic value) { + upgradeDto(value, "PersonCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_response_dto.dart b/mobile/openapi/lib/model/person_response_dto.dart index 50ee28f0af5fe..0b36fcde3b271 100644 --- a/mobile/openapi/lib/model/person_response_dto.dart +++ b/mobile/openapi/lib/model/person_response_dto.dart @@ -85,6 +85,7 @@ class PersonResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PersonResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index 929fbc29d2585..d9f84e9f4c226 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -40,6 +40,7 @@ class PersonStatisticsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PersonStatisticsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_update_dto.dart b/mobile/openapi/lib/model/person_update_dto.dart index 1af03890a2aa3..51a7ea25d07b3 100644 --- a/mobile/openapi/lib/model/person_update_dto.dart +++ b/mobile/openapi/lib/model/person_update_dto.dart @@ -96,6 +96,7 @@ class PersonUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "PersonUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index af2e7101c3477..b14bad789505b 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -91,6 +91,7 @@ class PersonWithFacesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PersonWithFacesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PersonWithFacesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/places_response_dto.dart b/mobile/openapi/lib/model/places_response_dto.dart index d3e1fc449bc5e..4f77788263450 100644 --- a/mobile/openapi/lib/model/places_response_dto.dart +++ b/mobile/openapi/lib/model/places_response_dto.dart @@ -84,6 +84,7 @@ class PlacesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PlacesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "PlacesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart index 284d8995289ec..a1172069771ea 100644 --- a/mobile/openapi/lib/model/purchase_response.dart +++ b/mobile/openapi/lib/model/purchase_response.dart @@ -46,6 +46,7 @@ class PurchaseResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PurchaseResponse? fromJson(dynamic value) { + upgradeDto(value, "PurchaseResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart index ca0a27e3bc4ba..69057e6c55a46 100644 --- a/mobile/openapi/lib/model/purchase_update.dart +++ b/mobile/openapi/lib/model/purchase_update.dart @@ -66,6 +66,7 @@ class PurchaseUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static PurchaseUpdate? fromJson(dynamic value) { + upgradeDto(value, "PurchaseUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/queue_status_dto.dart b/mobile/openapi/lib/model/queue_status_dto.dart index 7f7d310f6ff07..77591affe2f3d 100644 --- a/mobile/openapi/lib/model/queue_status_dto.dart +++ b/mobile/openapi/lib/model/queue_status_dto.dart @@ -46,6 +46,7 @@ class QueueStatusDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static QueueStatusDto? fromJson(dynamic value) { + upgradeDto(value, "QueueStatusDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart new file mode 100644 index 0000000000000..3fcab05bbb275 --- /dev/null +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -0,0 +1,566 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class RandomSearchDto { + /// Returns a new [RandomSearchDto] instance. + RandomSearchDto({ + this.city, + this.country, + this.createdAfter, + this.createdBefore, + this.deviceId, + this.isArchived, + this.isEncoded, + this.isFavorite, + this.isMotion, + this.isNotInAlbum, + this.isOffline, + this.isVisible, + this.lensModel, + this.libraryId, + this.make, + this.model, + this.personIds = const [], + this.size, + this.state, + this.takenAfter, + this.takenBefore, + this.trashedAfter, + this.trashedBefore, + this.type, + this.updatedAfter, + this.updatedBefore, + this.withArchived = false, + this.withDeleted, + this.withExif, + this.withPeople, + this.withStacked, + }); + + String? city; + + String? country; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? createdAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? createdBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? deviceId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isArchived; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isEncoded; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isFavorite; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isMotion; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isNotInAlbum; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isOffline; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isVisible; + + String? lensModel; + + String? libraryId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? make; + + String? model; + + List personIds; + + /// Minimum value: 1 + /// Maximum value: 1000 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? size; + + String? state; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? takenAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? takenBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? trashedAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? trashedBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetTypeEnum? type; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? updatedAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? updatedBefore; + + bool withArchived; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withDeleted; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withExif; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withPeople; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? withStacked; + + @override + bool operator ==(Object other) => identical(this, other) || other is RandomSearchDto && + other.city == city && + other.country == country && + other.createdAfter == createdAfter && + other.createdBefore == createdBefore && + other.deviceId == deviceId && + other.isArchived == isArchived && + other.isEncoded == isEncoded && + other.isFavorite == isFavorite && + other.isMotion == isMotion && + other.isNotInAlbum == isNotInAlbum && + other.isOffline == isOffline && + other.isVisible == isVisible && + other.lensModel == lensModel && + other.libraryId == libraryId && + other.make == make && + other.model == model && + _deepEquality.equals(other.personIds, personIds) && + other.size == size && + other.state == state && + other.takenAfter == takenAfter && + other.takenBefore == takenBefore && + other.trashedAfter == trashedAfter && + other.trashedBefore == trashedBefore && + other.type == type && + other.updatedAfter == updatedAfter && + other.updatedBefore == updatedBefore && + other.withArchived == withArchived && + other.withDeleted == withDeleted && + other.withExif == withExif && + other.withPeople == withPeople && + other.withStacked == withStacked; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (city == null ? 0 : city!.hashCode) + + (country == null ? 0 : country!.hashCode) + + (createdAfter == null ? 0 : createdAfter!.hashCode) + + (createdBefore == null ? 0 : createdBefore!.hashCode) + + (deviceId == null ? 0 : deviceId!.hashCode) + + (isArchived == null ? 0 : isArchived!.hashCode) + + (isEncoded == null ? 0 : isEncoded!.hashCode) + + (isFavorite == null ? 0 : isFavorite!.hashCode) + + (isMotion == null ? 0 : isMotion!.hashCode) + + (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + + (isOffline == null ? 0 : isOffline!.hashCode) + + (isVisible == null ? 0 : isVisible!.hashCode) + + (lensModel == null ? 0 : lensModel!.hashCode) + + (libraryId == null ? 0 : libraryId!.hashCode) + + (make == null ? 0 : make!.hashCode) + + (model == null ? 0 : model!.hashCode) + + (personIds.hashCode) + + (size == null ? 0 : size!.hashCode) + + (state == null ? 0 : state!.hashCode) + + (takenAfter == null ? 0 : takenAfter!.hashCode) + + (takenBefore == null ? 0 : takenBefore!.hashCode) + + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + + (trashedBefore == null ? 0 : trashedBefore!.hashCode) + + (type == null ? 0 : type!.hashCode) + + (updatedAfter == null ? 0 : updatedAfter!.hashCode) + + (updatedBefore == null ? 0 : updatedBefore!.hashCode) + + (withArchived.hashCode) + + (withDeleted == null ? 0 : withDeleted!.hashCode) + + (withExif == null ? 0 : withExif!.hashCode) + + (withPeople == null ? 0 : withPeople!.hashCode) + + (withStacked == null ? 0 : withStacked!.hashCode); + + @override + String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + + Map toJson() { + final json = {}; + if (this.city != null) { + json[r'city'] = this.city; + } else { + // json[r'city'] = null; + } + if (this.country != null) { + json[r'country'] = this.country; + } else { + // json[r'country'] = null; + } + if (this.createdAfter != null) { + json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + } else { + // json[r'createdAfter'] = null; + } + if (this.createdBefore != null) { + json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + } else { + // json[r'createdBefore'] = null; + } + if (this.deviceId != null) { + json[r'deviceId'] = this.deviceId; + } else { + // json[r'deviceId'] = null; + } + if (this.isArchived != null) { + json[r'isArchived'] = this.isArchived; + } else { + // json[r'isArchived'] = null; + } + if (this.isEncoded != null) { + json[r'isEncoded'] = this.isEncoded; + } else { + // json[r'isEncoded'] = null; + } + if (this.isFavorite != null) { + json[r'isFavorite'] = this.isFavorite; + } else { + // json[r'isFavorite'] = null; + } + if (this.isMotion != null) { + json[r'isMotion'] = this.isMotion; + } else { + // json[r'isMotion'] = null; + } + if (this.isNotInAlbum != null) { + json[r'isNotInAlbum'] = this.isNotInAlbum; + } else { + // json[r'isNotInAlbum'] = null; + } + if (this.isOffline != null) { + json[r'isOffline'] = this.isOffline; + } else { + // json[r'isOffline'] = null; + } + if (this.isVisible != null) { + json[r'isVisible'] = this.isVisible; + } else { + // json[r'isVisible'] = null; + } + if (this.lensModel != null) { + json[r'lensModel'] = this.lensModel; + } else { + // json[r'lensModel'] = null; + } + if (this.libraryId != null) { + json[r'libraryId'] = this.libraryId; + } else { + // json[r'libraryId'] = null; + } + if (this.make != null) { + json[r'make'] = this.make; + } else { + // json[r'make'] = null; + } + if (this.model != null) { + json[r'model'] = this.model; + } else { + // json[r'model'] = null; + } + json[r'personIds'] = this.personIds; + if (this.size != null) { + json[r'size'] = this.size; + } else { + // json[r'size'] = null; + } + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } + if (this.takenAfter != null) { + json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + } else { + // json[r'takenAfter'] = null; + } + if (this.takenBefore != null) { + json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + } else { + // json[r'takenBefore'] = null; + } + if (this.trashedAfter != null) { + json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + } else { + // json[r'trashedAfter'] = null; + } + if (this.trashedBefore != null) { + json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + } else { + // json[r'trashedBefore'] = null; + } + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + if (this.updatedAfter != null) { + json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + } else { + // json[r'updatedAfter'] = null; + } + if (this.updatedBefore != null) { + json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + } else { + // json[r'updatedBefore'] = null; + } + json[r'withArchived'] = this.withArchived; + if (this.withDeleted != null) { + json[r'withDeleted'] = this.withDeleted; + } else { + // json[r'withDeleted'] = null; + } + if (this.withExif != null) { + json[r'withExif'] = this.withExif; + } else { + // json[r'withExif'] = null; + } + if (this.withPeople != null) { + json[r'withPeople'] = this.withPeople; + } else { + // json[r'withPeople'] = null; + } + if (this.withStacked != null) { + json[r'withStacked'] = this.withStacked; + } else { + // json[r'withStacked'] = null; + } + return json; + } + + /// Returns a new [RandomSearchDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static RandomSearchDto? fromJson(dynamic value) { + upgradeDto(value, "RandomSearchDto"); + if (value is Map) { + final json = value.cast(); + + return RandomSearchDto( + city: mapValueOfType(json, r'city'), + country: mapValueOfType(json, r'country'), + createdAfter: mapDateTime(json, r'createdAfter', r''), + createdBefore: mapDateTime(json, r'createdBefore', r''), + deviceId: mapValueOfType(json, r'deviceId'), + isArchived: mapValueOfType(json, r'isArchived'), + isEncoded: mapValueOfType(json, r'isEncoded'), + isFavorite: mapValueOfType(json, r'isFavorite'), + isMotion: mapValueOfType(json, r'isMotion'), + isNotInAlbum: mapValueOfType(json, r'isNotInAlbum'), + isOffline: mapValueOfType(json, r'isOffline'), + isVisible: mapValueOfType(json, r'isVisible'), + lensModel: mapValueOfType(json, r'lensModel'), + libraryId: mapValueOfType(json, r'libraryId'), + make: mapValueOfType(json, r'make'), + model: mapValueOfType(json, r'model'), + personIds: json[r'personIds'] is Iterable + ? (json[r'personIds'] as Iterable).cast().toList(growable: false) + : const [], + size: num.parse('${json[r'size']}'), + state: mapValueOfType(json, r'state'), + takenAfter: mapDateTime(json, r'takenAfter', r''), + takenBefore: mapDateTime(json, r'takenBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', r''), + trashedBefore: mapDateTime(json, r'trashedBefore', r''), + type: AssetTypeEnum.fromJson(json[r'type']), + updatedAfter: mapDateTime(json, r'updatedAfter', r''), + updatedBefore: mapDateTime(json, r'updatedBefore', r''), + withArchived: mapValueOfType(json, r'withArchived') ?? false, + withDeleted: mapValueOfType(json, r'withDeleted'), + withExif: mapValueOfType(json, r'withExif'), + withPeople: mapValueOfType(json, r'withPeople'), + withStacked: mapValueOfType(json, r'withStacked'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = RandomSearchDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = RandomSearchDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of RandomSearchDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = RandomSearchDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index c8791aa91a5ee..8e1951277ae80 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -40,6 +40,7 @@ class RatingsResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static RatingsResponse? fromJson(dynamic value) { + upgradeDto(value, "RatingsResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/ratings_update.dart b/mobile/openapi/lib/model/ratings_update.dart index bde51bad1b360..5d9f9a655f0ad 100644 --- a/mobile/openapi/lib/model/ratings_update.dart +++ b/mobile/openapi/lib/model/ratings_update.dart @@ -50,6 +50,7 @@ class RatingsUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static RatingsUpdate? fromJson(dynamic value) { + upgradeDto(value, "RatingsUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart index eb414be984015..5b3648b46bb2a 100644 --- a/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart +++ b/mobile/openapi/lib/model/reverse_geocoding_state_response_dto.dart @@ -54,6 +54,7 @@ class ReverseGeocodingStateResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ReverseGeocodingStateResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ReverseGeocodingStateResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/scan_library_dto.dart b/mobile/openapi/lib/model/scan_library_dto.dart deleted file mode 100644 index 1b31aaaf01702..0000000000000 --- a/mobile/openapi/lib/model/scan_library_dto.dart +++ /dev/null @@ -1,124 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class ScanLibraryDto { - /// Returns a new [ScanLibraryDto] instance. - ScanLibraryDto({ - this.refreshAllFiles, - this.refreshModifiedFiles, - }); - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? refreshAllFiles; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? refreshModifiedFiles; - - @override - bool operator ==(Object other) => identical(this, other) || other is ScanLibraryDto && - other.refreshAllFiles == refreshAllFiles && - other.refreshModifiedFiles == refreshModifiedFiles; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (refreshAllFiles == null ? 0 : refreshAllFiles!.hashCode) + - (refreshModifiedFiles == null ? 0 : refreshModifiedFiles!.hashCode); - - @override - String toString() => 'ScanLibraryDto[refreshAllFiles=$refreshAllFiles, refreshModifiedFiles=$refreshModifiedFiles]'; - - Map toJson() { - final json = {}; - if (this.refreshAllFiles != null) { - json[r'refreshAllFiles'] = this.refreshAllFiles; - } else { - // json[r'refreshAllFiles'] = null; - } - if (this.refreshModifiedFiles != null) { - json[r'refreshModifiedFiles'] = this.refreshModifiedFiles; - } else { - // json[r'refreshModifiedFiles'] = null; - } - return json; - } - - /// Returns a new [ScanLibraryDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static ScanLibraryDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return ScanLibraryDto( - refreshAllFiles: mapValueOfType(json, r'refreshAllFiles'), - refreshModifiedFiles: mapValueOfType(json, r'refreshModifiedFiles'), - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = ScanLibraryDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = ScanLibraryDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of ScanLibraryDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = ScanLibraryDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - }; -} - diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 46ce5273ac947..e9b47e85ec9a3 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -58,6 +58,7 @@ class SearchAlbumResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchAlbumResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchAlbumResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index 21ddbbb2132f2..3d214e61d9fef 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -68,6 +68,7 @@ class SearchAssetResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchAssetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchAssetResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_explore_item.dart b/mobile/openapi/lib/model/search_explore_item.dart index 951fdd1bc8ce2..d44b2cd704a9a 100644 --- a/mobile/openapi/lib/model/search_explore_item.dart +++ b/mobile/openapi/lib/model/search_explore_item.dart @@ -46,6 +46,7 @@ class SearchExploreItem { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchExploreItem? fromJson(dynamic value) { + upgradeDto(value, "SearchExploreItem"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_explore_response_dto.dart b/mobile/openapi/lib/model/search_explore_response_dto.dart index 5bc601de9e59b..3b5d4f984933a 100644 --- a/mobile/openapi/lib/model/search_explore_response_dto.dart +++ b/mobile/openapi/lib/model/search_explore_response_dto.dart @@ -46,6 +46,7 @@ class SearchExploreResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchExploreResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchExploreResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index b40710e5251e4..f8eee844859e3 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -46,6 +46,7 @@ class SearchFacetCountResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchFacetCountResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchFacetCountResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 0784921c6b3e4..aeec873c8ddfb 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -46,6 +46,7 @@ class SearchFacetResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchFacetResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchFacetResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/search_response_dto.dart b/mobile/openapi/lib/model/search_response_dto.dart index 9b2b7fd3cf75d..ca742ae35ccbc 100644 --- a/mobile/openapi/lib/model/search_response_dto.dart +++ b/mobile/openapi/lib/model/search_response_dto.dart @@ -46,6 +46,7 @@ class SearchResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SearchResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_about_response_dto.dart b/mobile/openapi/lib/model/server_about_response_dto.dart index 9c71d1fccdcb4..1ab51a80f1362 100644 --- a/mobile/openapi/lib/model/server_about_response_dto.dart +++ b/mobile/openapi/lib/model/server_about_response_dto.dart @@ -276,6 +276,7 @@ class ServerAboutResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerAboutResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerAboutResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index 47cc52fb2c3fe..bd5c2405e29d3 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -17,6 +17,8 @@ class ServerConfigDto { required this.isInitialized, required this.isOnboarded, required this.loginPageMessage, + required this.mapDarkStyleUrl, + required this.mapLightStyleUrl, required this.oauthButtonText, required this.trashDays, required this.userDeleteDelay, @@ -30,6 +32,10 @@ class ServerConfigDto { String loginPageMessage; + String mapDarkStyleUrl; + + String mapLightStyleUrl; + String oauthButtonText; int trashDays; @@ -42,6 +48,8 @@ class ServerConfigDto { other.isInitialized == isInitialized && other.isOnboarded == isOnboarded && other.loginPageMessage == loginPageMessage && + other.mapDarkStyleUrl == mapDarkStyleUrl && + other.mapLightStyleUrl == mapLightStyleUrl && other.oauthButtonText == oauthButtonText && other.trashDays == trashDays && other.userDeleteDelay == userDeleteDelay; @@ -53,12 +61,14 @@ class ServerConfigDto { (isInitialized.hashCode) + (isOnboarded.hashCode) + (loginPageMessage.hashCode) + + (mapDarkStyleUrl.hashCode) + + (mapLightStyleUrl.hashCode) + (oauthButtonText.hashCode) + (trashDays.hashCode) + (userDeleteDelay.hashCode); @override - String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, oauthButtonText=$oauthButtonText, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; + String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, mapDarkStyleUrl=$mapDarkStyleUrl, mapLightStyleUrl=$mapLightStyleUrl, oauthButtonText=$oauthButtonText, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; Map toJson() { final json = {}; @@ -66,6 +76,8 @@ class ServerConfigDto { json[r'isInitialized'] = this.isInitialized; json[r'isOnboarded'] = this.isOnboarded; json[r'loginPageMessage'] = this.loginPageMessage; + json[r'mapDarkStyleUrl'] = this.mapDarkStyleUrl; + json[r'mapLightStyleUrl'] = this.mapLightStyleUrl; json[r'oauthButtonText'] = this.oauthButtonText; json[r'trashDays'] = this.trashDays; json[r'userDeleteDelay'] = this.userDeleteDelay; @@ -76,6 +88,7 @@ class ServerConfigDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerConfigDto? fromJson(dynamic value) { + upgradeDto(value, "ServerConfigDto"); if (value is Map) { final json = value.cast(); @@ -84,6 +97,8 @@ class ServerConfigDto { isInitialized: mapValueOfType(json, r'isInitialized')!, isOnboarded: mapValueOfType(json, r'isOnboarded')!, loginPageMessage: mapValueOfType(json, r'loginPageMessage')!, + mapDarkStyleUrl: mapValueOfType(json, r'mapDarkStyleUrl')!, + mapLightStyleUrl: mapValueOfType(json, r'mapLightStyleUrl')!, oauthButtonText: mapValueOfType(json, r'oauthButtonText')!, trashDays: mapValueOfType(json, r'trashDays')!, userDeleteDelay: mapValueOfType(json, r'userDeleteDelay')!, @@ -138,6 +153,8 @@ class ServerConfigDto { 'isInitialized', 'isOnboarded', 'loginPageMessage', + 'mapDarkStyleUrl', + 'mapLightStyleUrl', 'oauthButtonText', 'trashDays', 'userDeleteDelay', diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart index 0a7d8a4b4774a..5149c3796a9da 100644 --- a/mobile/openapi/lib/model/server_features_dto.dart +++ b/mobile/openapi/lib/model/server_features_dto.dart @@ -118,6 +118,7 @@ class ServerFeaturesDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerFeaturesDto? fromJson(dynamic value) { + upgradeDto(value, "ServerFeaturesDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_media_types_response_dto.dart b/mobile/openapi/lib/model/server_media_types_response_dto.dart index 35ddef195601a..506cbb44b4d76 100644 --- a/mobile/openapi/lib/model/server_media_types_response_dto.dart +++ b/mobile/openapi/lib/model/server_media_types_response_dto.dart @@ -52,6 +52,7 @@ class ServerMediaTypesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerMediaTypesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerMediaTypesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_ping_response.dart b/mobile/openapi/lib/model/server_ping_response.dart index e23dc15c61af1..621ebfa2945ad 100644 --- a/mobile/openapi/lib/model/server_ping_response.dart +++ b/mobile/openapi/lib/model/server_ping_response.dart @@ -40,6 +40,7 @@ class ServerPingResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerPingResponse? fromJson(dynamic value) { + upgradeDto(value, "ServerPingResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index 6996e49aa5e24..654a34ee6b0e7 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -58,6 +58,7 @@ class ServerStatsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerStatsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerStatsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 89d97d32ead2d..8d12e77834bec 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -76,6 +76,7 @@ class ServerStorageResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerStorageResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerStorageResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_theme_dto.dart b/mobile/openapi/lib/model/server_theme_dto.dart index 65b9b9163e874..69e1b2d2c87a7 100644 --- a/mobile/openapi/lib/model/server_theme_dto.dart +++ b/mobile/openapi/lib/model/server_theme_dto.dart @@ -40,6 +40,7 @@ class ServerThemeDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerThemeDto? fromJson(dynamic value) { + upgradeDto(value, "ServerThemeDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index e507f3372abfd..751347fabd2c1 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -52,6 +52,7 @@ class ServerVersionResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ServerVersionResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ServerVersionResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/session_response_dto.dart b/mobile/openapi/lib/model/session_response_dto.dart index 82673b3874b67..92e2dc60676af 100644 --- a/mobile/openapi/lib/model/session_response_dto.dart +++ b/mobile/openapi/lib/model/session_response_dto.dart @@ -70,6 +70,7 @@ class SessionResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SessionResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SessionResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 623bc3125fd48..bc96b31fd24f9 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -132,6 +132,7 @@ class SharedLinkCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SharedLinkCreateDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index 2369c85db1a12..a394ba9b3b8fd 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -141,6 +141,7 @@ class SharedLinkEditDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SharedLinkEditDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkEditDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index 018a1a51de2a4..9cc8b3ac80add 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -144,6 +144,7 @@ class SharedLinkResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SharedLinkResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SharedLinkResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/sign_up_dto.dart b/mobile/openapi/lib/model/sign_up_dto.dart index 772749fdba48c..7e0ff4045c7b5 100644 --- a/mobile/openapi/lib/model/sign_up_dto.dart +++ b/mobile/openapi/lib/model/sign_up_dto.dart @@ -52,6 +52,7 @@ class SignUpDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SignUpDto? fromJson(dynamic value) { + upgradeDto(value, "SignUpDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/smart_info_response_dto.dart b/mobile/openapi/lib/model/smart_info_response_dto.dart index 52e7c108b8291..4631eccf2cb1c 100644 --- a/mobile/openapi/lib/model/smart_info_response_dto.dart +++ b/mobile/openapi/lib/model/smart_info_response_dto.dart @@ -54,6 +54,7 @@ class SmartInfoResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SmartInfoResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SmartInfoResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 2a42b75768420..4e1408cafa737 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -467,6 +467,7 @@ class SmartSearchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SmartSearchDto? fromJson(dynamic value) { + upgradeDto(value, "SmartSearchDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart index 9b37bc6e2e9aa..cb51081eb1ee4 100644 --- a/mobile/openapi/lib/model/stack_create_dto.dart +++ b/mobile/openapi/lib/model/stack_create_dto.dart @@ -41,6 +41,7 @@ class StackCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static StackCreateDto? fromJson(dynamic value) { + upgradeDto(value, "StackCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 3d0aaf91d17cc..b6cb747cafc5f 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -52,6 +52,7 @@ class StackResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static StackResponseDto? fromJson(dynamic value) { + upgradeDto(value, "StackResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart index 0e9712721048a..0101499edfc51 100644 --- a/mobile/openapi/lib/model/stack_update_dto.dart +++ b/mobile/openapi/lib/model/stack_update_dto.dart @@ -50,6 +50,7 @@ class StackUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static StackUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "StackUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index aff8062c8a139..5306370d2d1f7 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -142,6 +142,7 @@ class SystemConfigDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index a75a77c669168..73f7d35aecc30 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -175,6 +175,7 @@ class SystemConfigFFmpegDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigFFmpegDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigFFmpegDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_faces_dto.dart b/mobile/openapi/lib/model/system_config_faces_dto.dart index 980e494fb70b0..4e18eb8de20e4 100644 --- a/mobile/openapi/lib/model/system_config_faces_dto.dart +++ b/mobile/openapi/lib/model/system_config_faces_dto.dart @@ -40,6 +40,7 @@ class SystemConfigFacesDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigFacesDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigFacesDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart new file mode 100644 index 0000000000000..2192a7cb0cbd5 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -0,0 +1,118 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SystemConfigGeneratedImageDto { + /// Returns a new [SystemConfigGeneratedImageDto] instance. + SystemConfigGeneratedImageDto({ + required this.format, + required this.quality, + required this.size, + }); + + ImageFormat format; + + /// Minimum value: 1 + /// Maximum value: 100 + int quality; + + /// Minimum value: 1 + int size; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigGeneratedImageDto && + other.format == format && + other.quality == quality && + other.size == size; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (format.hashCode) + + (quality.hashCode) + + (size.hashCode); + + @override + String toString() => 'SystemConfigGeneratedImageDto[format=$format, quality=$quality, size=$size]'; + + Map toJson() { + final json = {}; + json[r'format'] = this.format; + json[r'quality'] = this.quality; + json[r'size'] = this.size; + return json; + } + + /// Returns a new [SystemConfigGeneratedImageDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigGeneratedImageDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigGeneratedImageDto"); + if (value is Map) { + final json = value.cast(); + + return SystemConfigGeneratedImageDto( + format: ImageFormat.fromJson(json[r'format'])!, + quality: mapValueOfType(json, r'quality')!, + size: mapValueOfType(json, r'size')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigGeneratedImageDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigGeneratedImageDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigGeneratedImageDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigGeneratedImageDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'format', + 'quality', + 'size', + }; +} + diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 388949c759ce0..5309f7745c44d 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -15,64 +15,42 @@ class SystemConfigImageDto { SystemConfigImageDto({ required this.colorspace, required this.extractEmbedded, - required this.previewFormat, - required this.previewSize, - required this.quality, - required this.thumbnailFormat, - required this.thumbnailSize, + required this.preview, + required this.thumbnail, }); Colorspace colorspace; bool extractEmbedded; - ImageFormat previewFormat; + SystemConfigGeneratedImageDto preview; - /// Minimum value: 1 - int previewSize; - - /// Minimum value: 1 - /// Maximum value: 100 - int quality; - - ImageFormat thumbnailFormat; - - /// Minimum value: 1 - int thumbnailSize; + SystemConfigGeneratedImageDto thumbnail; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigImageDto && other.colorspace == colorspace && other.extractEmbedded == extractEmbedded && - other.previewFormat == previewFormat && - other.previewSize == previewSize && - other.quality == quality && - other.thumbnailFormat == thumbnailFormat && - other.thumbnailSize == thumbnailSize; + other.preview == preview && + other.thumbnail == thumbnail; @override int get hashCode => // ignore: unnecessary_parenthesis (colorspace.hashCode) + (extractEmbedded.hashCode) + - (previewFormat.hashCode) + - (previewSize.hashCode) + - (quality.hashCode) + - (thumbnailFormat.hashCode) + - (thumbnailSize.hashCode); + (preview.hashCode) + + (thumbnail.hashCode); @override - String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, previewFormat=$previewFormat, previewSize=$previewSize, quality=$quality, thumbnailFormat=$thumbnailFormat, thumbnailSize=$thumbnailSize]'; + String toString() => 'SystemConfigImageDto[colorspace=$colorspace, extractEmbedded=$extractEmbedded, preview=$preview, thumbnail=$thumbnail]'; Map toJson() { final json = {}; json[r'colorspace'] = this.colorspace; json[r'extractEmbedded'] = this.extractEmbedded; - json[r'previewFormat'] = this.previewFormat; - json[r'previewSize'] = this.previewSize; - json[r'quality'] = this.quality; - json[r'thumbnailFormat'] = this.thumbnailFormat; - json[r'thumbnailSize'] = this.thumbnailSize; + json[r'preview'] = this.preview; + json[r'thumbnail'] = this.thumbnail; return json; } @@ -80,17 +58,15 @@ class SystemConfigImageDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigImageDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigImageDto"); if (value is Map) { final json = value.cast(); return SystemConfigImageDto( colorspace: Colorspace.fromJson(json[r'colorspace'])!, extractEmbedded: mapValueOfType(json, r'extractEmbedded')!, - previewFormat: ImageFormat.fromJson(json[r'previewFormat'])!, - previewSize: mapValueOfType(json, r'previewSize')!, - quality: mapValueOfType(json, r'quality')!, - thumbnailFormat: ImageFormat.fromJson(json[r'thumbnailFormat'])!, - thumbnailSize: mapValueOfType(json, r'thumbnailSize')!, + preview: SystemConfigGeneratedImageDto.fromJson(json[r'preview'])!, + thumbnail: SystemConfigGeneratedImageDto.fromJson(json[r'thumbnail'])!, ); } return null; @@ -140,11 +116,8 @@ class SystemConfigImageDto { static const requiredKeys = { 'colorspace', 'extractEmbedded', - 'previewFormat', - 'previewSize', - 'quality', - 'thumbnailFormat', - 'thumbnailSize', + 'preview', + 'thumbnail', }; } diff --git a/mobile/openapi/lib/model/system_config_job_dto.dart b/mobile/openapi/lib/model/system_config_job_dto.dart index 1bc0f6b29c1c0..c0fed5cccc06f 100644 --- a/mobile/openapi/lib/model/system_config_job_dto.dart +++ b/mobile/openapi/lib/model/system_config_job_dto.dart @@ -100,6 +100,7 @@ class SystemConfigJobDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigJobDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigJobDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_library_dto.dart b/mobile/openapi/lib/model/system_config_library_dto.dart index 4f55e33e8087b..e728b0bf20957 100644 --- a/mobile/openapi/lib/model/system_config_library_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_dto.dart @@ -46,6 +46,7 @@ class SystemConfigLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 31df272594577..6a6558b4b32ee 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -46,6 +46,7 @@ class SystemConfigLibraryScanDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLibraryScanDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLibraryScanDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_library_watch_dto.dart b/mobile/openapi/lib/model/system_config_library_watch_dto.dart index 9d152f366a898..1a1f5d7126b34 100644 --- a/mobile/openapi/lib/model/system_config_library_watch_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_watch_dto.dart @@ -40,6 +40,7 @@ class SystemConfigLibraryWatchDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLibraryWatchDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLibraryWatchDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_logging_dto.dart b/mobile/openapi/lib/model/system_config_logging_dto.dart index 60c0be3d2c2ff..f025221eff996 100644 --- a/mobile/openapi/lib/model/system_config_logging_dto.dart +++ b/mobile/openapi/lib/model/system_config_logging_dto.dart @@ -46,6 +46,7 @@ class SystemConfigLoggingDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigLoggingDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigLoggingDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index 3923bacad4211..d665f0bfa56a7 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -64,6 +64,7 @@ class SystemConfigMachineLearningDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigMachineLearningDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigMachineLearningDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 663188518275a..d53d5711db2af 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -52,6 +52,7 @@ class SystemConfigMapDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigMapDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigMapDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_metadata_dto.dart b/mobile/openapi/lib/model/system_config_metadata_dto.dart index 60ca35c835bdb..3c32fc551d4e2 100644 --- a/mobile/openapi/lib/model/system_config_metadata_dto.dart +++ b/mobile/openapi/lib/model/system_config_metadata_dto.dart @@ -40,6 +40,7 @@ class SystemConfigMetadataDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigMetadataDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigMetadataDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart index c7b8c98695c32..c63d2abc1ba30 100644 --- a/mobile/openapi/lib/model/system_config_new_version_check_dto.dart +++ b/mobile/openapi/lib/model/system_config_new_version_check_dto.dart @@ -40,6 +40,7 @@ class SystemConfigNewVersionCheckDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigNewVersionCheckDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigNewVersionCheckDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_notifications_dto.dart b/mobile/openapi/lib/model/system_config_notifications_dto.dart index 22f08b3ab4365..35d3d318339e8 100644 --- a/mobile/openapi/lib/model/system_config_notifications_dto.dart +++ b/mobile/openapi/lib/model/system_config_notifications_dto.dart @@ -40,6 +40,7 @@ class SystemConfigNotificationsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigNotificationsDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigNotificationsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 6ebbe8d25c05c..9125bb7bba65a 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -125,6 +125,7 @@ class SystemConfigOAuthDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigOAuthDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigOAuthDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_password_login_dto.dart b/mobile/openapi/lib/model/system_config_password_login_dto.dart index 61896a890c58e..69c8942bb6471 100644 --- a/mobile/openapi/lib/model/system_config_password_login_dto.dart +++ b/mobile/openapi/lib/model/system_config_password_login_dto.dart @@ -40,6 +40,7 @@ class SystemConfigPasswordLoginDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigPasswordLoginDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigPasswordLoginDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart index 2eb586cac689c..6c1673d46c07d 100644 --- a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart +++ b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart @@ -40,6 +40,7 @@ class SystemConfigReverseGeocodingDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigReverseGeocodingDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigReverseGeocodingDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_server_dto.dart b/mobile/openapi/lib/model/system_config_server_dto.dart index ccb48ee61dedb..b1b92c9515d5b 100644 --- a/mobile/openapi/lib/model/system_config_server_dto.dart +++ b/mobile/openapi/lib/model/system_config_server_dto.dart @@ -46,6 +46,7 @@ class SystemConfigServerDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigServerDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigServerDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_smtp_dto.dart b/mobile/openapi/lib/model/system_config_smtp_dto.dart index 6588d244ee5d1..fcde49cf3564e 100644 --- a/mobile/openapi/lib/model/system_config_smtp_dto.dart +++ b/mobile/openapi/lib/model/system_config_smtp_dto.dart @@ -58,6 +58,7 @@ class SystemConfigSmtpDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigSmtpDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigSmtpDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart index 63dfdca4cf07e..bdaaa426c5220 100644 --- a/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart +++ b/mobile/openapi/lib/model/system_config_smtp_transport_dto.dart @@ -66,6 +66,7 @@ class SystemConfigSmtpTransportDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigSmtpTransportDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigSmtpTransportDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_storage_template_dto.dart b/mobile/openapi/lib/model/system_config_storage_template_dto.dart index 13323aebdaba3..596aafc1950a1 100644 --- a/mobile/openapi/lib/model/system_config_storage_template_dto.dart +++ b/mobile/openapi/lib/model/system_config_storage_template_dto.dart @@ -52,6 +52,7 @@ class SystemConfigStorageTemplateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigStorageTemplateDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigStorageTemplateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart index 82e0a6f74769b..f8586d344c5aa 100644 --- a/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_storage_option_dto.dart @@ -82,6 +82,7 @@ class SystemConfigTemplateStorageOptionDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigTemplateStorageOptionDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigTemplateStorageOptionDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_theme_dto.dart b/mobile/openapi/lib/model/system_config_theme_dto.dart index 2f7f4d2f3b916..a97c2cf84c1f3 100644 --- a/mobile/openapi/lib/model/system_config_theme_dto.dart +++ b/mobile/openapi/lib/model/system_config_theme_dto.dart @@ -40,6 +40,7 @@ class SystemConfigThemeDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigThemeDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigThemeDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 336019fde4203..51b39e9a55c1f 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -47,6 +47,7 @@ class SystemConfigTrashDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigTrashDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigTrashDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index c46637446098f..8e6bd3c9c306e 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -41,6 +41,7 @@ class SystemConfigUserDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static SystemConfigUserDto? fromJson(dynamic value) { + upgradeDto(value, "SystemConfigUserDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart index c11cb66ce081f..26a575e193dc6 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart @@ -46,6 +46,7 @@ class TagBulkAssetsDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagBulkAssetsDto? fromJson(dynamic value) { + upgradeDto(value, "TagBulkAssetsDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index d4dcb91d8c45d..009f26bfe4f49 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -40,6 +40,7 @@ class TagBulkAssetsResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagBulkAssetsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TagBulkAssetsResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index dd7e537a0a021..9a5171074d622 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -66,6 +66,7 @@ class TagCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagCreateDto? fromJson(dynamic value) { + upgradeDto(value, "TagCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index 1d1a88c3cff29..cd684b163a27d 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -96,6 +96,7 @@ class TagResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TagResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_update_dto.dart b/mobile/openapi/lib/model/tag_update_dto.dart index 661f65896e56f..ab1adb127bacb 100644 --- a/mobile/openapi/lib/model/tag_update_dto.dart +++ b/mobile/openapi/lib/model/tag_update_dto.dart @@ -44,6 +44,7 @@ class TagUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "TagUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tag_upsert_dto.dart b/mobile/openapi/lib/model/tag_upsert_dto.dart index 941d25b6aee6c..d60a00f466e1f 100644 --- a/mobile/openapi/lib/model/tag_upsert_dto.dart +++ b/mobile/openapi/lib/model/tag_upsert_dto.dart @@ -40,6 +40,7 @@ class TagUpsertDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagUpsertDto? fromJson(dynamic value) { + upgradeDto(value, "TagUpsertDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 3a5ea3b20b3ec..2470edf979239 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -46,6 +46,7 @@ class TagsResponse { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagsResponse? fromJson(dynamic value) { + upgradeDto(value, "TagsResponse"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/tags_update.dart b/mobile/openapi/lib/model/tags_update.dart index 8355b00a00d49..d99236914055c 100644 --- a/mobile/openapi/lib/model/tags_update.dart +++ b/mobile/openapi/lib/model/tags_update.dart @@ -66,6 +66,7 @@ class TagsUpdate { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TagsUpdate? fromJson(dynamic value) { + upgradeDto(value, "TagsUpdate"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/test_email_response_dto.dart b/mobile/openapi/lib/model/test_email_response_dto.dart new file mode 100644 index 0000000000000..33e6c042d8d09 --- /dev/null +++ b/mobile/openapi/lib/model/test_email_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TestEmailResponseDto { + /// Returns a new [TestEmailResponseDto] instance. + TestEmailResponseDto({ + required this.messageId, + }); + + String messageId; + + @override + bool operator ==(Object other) => identical(this, other) || other is TestEmailResponseDto && + other.messageId == messageId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (messageId.hashCode); + + @override + String toString() => 'TestEmailResponseDto[messageId=$messageId]'; + + Map toJson() { + final json = {}; + json[r'messageId'] = this.messageId; + return json; + } + + /// Returns a new [TestEmailResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TestEmailResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TestEmailResponseDto"); + if (value is Map) { + final json = value.cast(); + + return TestEmailResponseDto( + messageId: mapValueOfType(json, r'messageId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TestEmailResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TestEmailResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TestEmailResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TestEmailResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'messageId', + }; +} + diff --git a/mobile/openapi/lib/model/time_bucket_response_dto.dart b/mobile/openapi/lib/model/time_bucket_response_dto.dart index 2c86a56b3c9ad..56044b27a8a81 100644 --- a/mobile/openapi/lib/model/time_bucket_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_response_dto.dart @@ -46,6 +46,7 @@ class TimeBucketResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static TimeBucketResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TimeBucketResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart new file mode 100644 index 0000000000000..2df154d06c1a0 --- /dev/null +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TrashResponseDto { + /// Returns a new [TrashResponseDto] instance. + TrashResponseDto({ + required this.count, + }); + + int count; + + @override + bool operator ==(Object other) => identical(this, other) || other is TrashResponseDto && + other.count == count; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (count.hashCode); + + @override + String toString() => 'TrashResponseDto[count=$count]'; + + Map toJson() { + final json = {}; + json[r'count'] = this.count; + return json; + } + + /// Returns a new [TrashResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TrashResponseDto? fromJson(dynamic value) { + upgradeDto(value, "TrashResponseDto"); + if (value is Map) { + final json = value.cast(); + + return TrashResponseDto( + count: mapValueOfType(json, r'count')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TrashResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TrashResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TrashResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TrashResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'count', + }; +} + diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index f9c9762887265..8353dba14e627 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -114,6 +114,7 @@ class UpdateAlbumDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateAlbumDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateAlbumDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index f77223acf5855..43218cae6e140 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -40,6 +40,7 @@ class UpdateAlbumUserDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateAlbumUserDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateAlbumUserDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 6e5be5683f484..9ebce5fd9232b 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -63,12 +63,6 @@ class UpdateAssetDto { /// num? latitude; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? livePhotoVideoId; /// @@ -164,6 +158,7 @@ class UpdateAssetDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateAssetDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateAssetDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 85847c0ddfb6f..b85df40172e69 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -62,6 +62,7 @@ class UpdateLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdateLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "UpdateLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/update_partner_dto.dart b/mobile/openapi/lib/model/update_partner_dto.dart index f695f99535a38..3af3c83ad10bd 100644 --- a/mobile/openapi/lib/model/update_partner_dto.dart +++ b/mobile/openapi/lib/model/update_partner_dto.dart @@ -40,6 +40,7 @@ class UpdatePartnerDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UpdatePartnerDto? fromJson(dynamic value) { + upgradeDto(value, "UpdatePartnerDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index 0bbbba00bbaab..e6f9216d74572 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -74,6 +74,7 @@ class UsageByUserDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UsageByUserDto? fromJson(dynamic value) { + upgradeDto(value, "UsageByUserDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index db514a1d571b6..f2709be57b640 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -105,6 +105,7 @@ class UserAdminCreateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminCreateDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminCreateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_delete_dto.dart b/mobile/openapi/lib/model/user_admin_delete_dto.dart index 7778b15775d0f..2cf68ad7b25ee 100644 --- a/mobile/openapi/lib/model/user_admin_delete_dto.dart +++ b/mobile/openapi/lib/model/user_admin_delete_dto.dart @@ -50,6 +50,7 @@ class UserAdminDeleteDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminDeleteDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminDeleteDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index af1ad3ad1c1bf..e5ae8e1d4ef27 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -22,6 +22,7 @@ class UserAdminResponseDto { required this.license, required this.name, required this.oauthId, + required this.profileChangedAt, required this.profileImagePath, required this.quotaSizeInBytes, required this.quotaUsageInBytes, @@ -49,6 +50,8 @@ class UserAdminResponseDto { String oauthId; + DateTime profileChangedAt; + String profileImagePath; int? quotaSizeInBytes; @@ -74,6 +77,7 @@ class UserAdminResponseDto { other.license == license && other.name == name && other.oauthId == oauthId && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath && other.quotaSizeInBytes == quotaSizeInBytes && other.quotaUsageInBytes == quotaUsageInBytes && @@ -94,6 +98,7 @@ class UserAdminResponseDto { (license == null ? 0 : license!.hashCode) + (name.hashCode) + (oauthId.hashCode) + + (profileChangedAt.hashCode) + (profileImagePath.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + (quotaUsageInBytes == null ? 0 : quotaUsageInBytes!.hashCode) + @@ -103,7 +108,7 @@ class UserAdminResponseDto { (updatedAt.hashCode); @override - String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; + String toString() => 'UserAdminResponseDto[avatarColor=$avatarColor, createdAt=$createdAt, deletedAt=$deletedAt, email=$email, id=$id, isAdmin=$isAdmin, license=$license, name=$name, oauthId=$oauthId, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath, quotaSizeInBytes=$quotaSizeInBytes, quotaUsageInBytes=$quotaUsageInBytes, shouldChangePassword=$shouldChangePassword, status=$status, storageLabel=$storageLabel, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -124,6 +129,7 @@ class UserAdminResponseDto { } json[r'name'] = this.name; json[r'oauthId'] = this.oauthId; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; @@ -150,6 +156,7 @@ class UserAdminResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminResponseDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminResponseDto"); if (value is Map) { final json = value.cast(); @@ -163,6 +170,7 @@ class UserAdminResponseDto { license: UserLicense.fromJson(json[r'license']), name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes'), @@ -226,6 +234,7 @@ class UserAdminResponseDto { 'license', 'name', 'oauthId', + 'profileChangedAt', 'profileImagePath', 'quotaSizeInBytes', 'quotaUsageInBytes', diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index dd0db767fe6b0..6c6f73ae8e9e1 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -119,6 +119,7 @@ class UserAdminUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserAdminUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "UserAdminUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index c7abb085f29c7..9bed8d5c43633 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -52,6 +52,7 @@ class UserLicense { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserLicense? fromJson(dynamic value) { + upgradeDto(value, "UserLicense"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index d3927df8d7ee3..23d9ea84ecd82 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -88,6 +88,7 @@ class UserPreferencesResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserPreferencesResponseDto? fromJson(dynamic value) { + upgradeDto(value, "UserPreferencesResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index 2841c2f572c11..208dbf686078a 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -178,6 +178,7 @@ class UserPreferencesUpdateDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserPreferencesUpdateDto? fromJson(dynamic value) { + upgradeDto(value, "UserPreferencesUpdateDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index 41c18998483b9..a02da299481b8 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -17,6 +17,7 @@ class UserResponseDto { required this.email, required this.id, required this.name, + required this.profileChangedAt, required this.profileImagePath, }); @@ -28,6 +29,8 @@ class UserResponseDto { String name; + DateTime profileChangedAt; + String profileImagePath; @override @@ -36,6 +39,7 @@ class UserResponseDto { other.email == email && other.id == id && other.name == name && + other.profileChangedAt == profileChangedAt && other.profileImagePath == profileImagePath; @override @@ -45,10 +49,11 @@ class UserResponseDto { (email.hashCode) + (id.hashCode) + (name.hashCode) + + (profileChangedAt.hashCode) + (profileImagePath.hashCode); @override - String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileImagePath=$profileImagePath]'; + String toString() => 'UserResponseDto[avatarColor=$avatarColor, email=$email, id=$id, name=$name, profileChangedAt=$profileChangedAt, profileImagePath=$profileImagePath]'; Map toJson() { final json = {}; @@ -56,6 +61,7 @@ class UserResponseDto { json[r'email'] = this.email; json[r'id'] = this.id; json[r'name'] = this.name; + json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; return json; } @@ -64,6 +70,7 @@ class UserResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserResponseDto? fromJson(dynamic value) { + upgradeDto(value, "UserResponseDto"); if (value is Map) { final json = value.cast(); @@ -72,6 +79,7 @@ class UserResponseDto { email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, ); } @@ -124,6 +132,7 @@ class UserResponseDto { 'email', 'id', 'name', + 'profileChangedAt', 'profileImagePath', }; } diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 2d665fc7847b8..8f3f4df37ad81 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -82,6 +82,7 @@ class UserUpdateMeDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static UserUpdateMeDto? fromJson(dynamic value) { + upgradeDto(value, "UserUpdateMeDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_access_token_response_dto.dart b/mobile/openapi/lib/model/validate_access_token_response_dto.dart index e970f7e840a80..5e36efcfedf5b 100644 --- a/mobile/openapi/lib/model/validate_access_token_response_dto.dart +++ b/mobile/openapi/lib/model/validate_access_token_response_dto.dart @@ -40,6 +40,7 @@ class ValidateAccessTokenResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateAccessTokenResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateAccessTokenResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 05e122b1a1170..08199e3aa66c8 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -46,6 +46,7 @@ class ValidateLibraryDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateLibraryDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateLibraryDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 23aac0b74255a..11fbbd74c2aaa 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -62,6 +62,7 @@ class ValidateLibraryImportPathResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateLibraryImportPathResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateLibraryImportPathResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/openapi/lib/model/validate_library_response_dto.dart b/mobile/openapi/lib/model/validate_library_response_dto.dart index b213f9ba98943..e0dc2a2d14233 100644 --- a/mobile/openapi/lib/model/validate_library_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_response_dto.dart @@ -40,6 +40,7 @@ class ValidateLibraryResponseDto { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static ValidateLibraryResponseDto? fromJson(dynamic value) { + upgradeDto(value, "ValidateLibraryResponseDto"); if (value is Map) { final json = value.cast(); diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index e6b18bb1f8df9..7537e8065c97b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -78,6 +78,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.0.0" + background_downloader: + dependency: "direct main" + description: + name: background_downloader + sha256: "6a945db1a1c7727a4bc9c1d7c882cfb1a819f873b77e01d5e5dd6a3fb231cb28" + url: "https://pub.dev" + source: hosted + version: "8.5.5" boolean_selector: dependency: transitive description: @@ -744,10 +752,10 @@ packages: dependency: "direct main" description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -836,6 +844,13 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.1+1" + immich_mobile_immich_lint: + dependency: "direct dev" + description: + path: immich_lint + relative: true + source: path + version: "0.0.0" integration_test: dependency: "direct dev" description: flutter @@ -1856,4 +1871,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.24.0" + flutter: ">=3.24.3" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 282b57a086c6b..060bb3d77f1f8 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,11 +2,11 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.114.0+158 +version: 1.116.2+161 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.24.0 + flutter: 3.24.3 dependencies: flutter: @@ -32,7 +32,7 @@ dependencies: flutter_svg: ^2.0.9 package_info_plus: ^8.0.1 url_launcher: ^6.2.4 - http: ^0.13.6 + http: ^1.1.0 cancellation_token_http: ^2.0.0 easy_localization: ^3.0.3 share_plus: ^10.0.0 @@ -56,10 +56,14 @@ dependencies: thumbhash: 0.1.0+1 async: ^2.11.0 dynamic_color: ^1.7.0 #package to apply system theme + native_video_player: git: url: https://github.com/immich-app/native_video_player ref: feat/headers + + background_downloader: ^8.5.5 + #image editing packages crop_image: ^1.0.13 @@ -98,10 +102,12 @@ dev_dependencies: isar_generator: ^3.1.0+1 integration_test: sdk: flutter - custom_lint: ^0.6.0 + custom_lint: ^0.6.4 riverpod_lint: ^2.3.7 riverpod_generator: ^2.3.9 mocktail: ^1.0.3 + immich_mobile_immich_lint: + path: './immich_lint' flutter: uses-material-design: true diff --git a/mobile/scripts/fdroid_build_isar.sh b/mobile/scripts/fdroid_build_isar.sh old mode 100644 new mode 100755 index 44f59c69aee0e..41517737c94f5 --- a/mobile/scripts/fdroid_build_isar.sh +++ b/mobile/scripts/fdroid_build_isar.sh @@ -1,6 +1,8 @@ #!/usr/bin/env sh -cd .isar || exit +test -d .isar || exit +cp .isar-cargo.lock .isar/Cargo.lock +(cd .isar || exit bash tool/build_android.sh x86 bash tool/build_android.sh x64 bash tool/build_android.sh armv7 @@ -13,4 +15,4 @@ mv libisar_android_x64.so libisar.so mv libisar.so ../.pub-cache/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/ mv libisar_android_x86.so libisar.so mv libisar.so ../.pub-cache/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/ -cd .. \ No newline at end of file +) \ No newline at end of file diff --git a/mobile/scripts/fdroid_update_isar.sh b/mobile/scripts/fdroid_update_isar.sh new file mode 100755 index 0000000000000..814f50a8a15bc --- /dev/null +++ b/mobile/scripts/fdroid_update_isar.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env sh + +isar_version="$(awk '/isar: /{gsub(/\^/, "", $2); print $2}' pubspec.yaml)" +checked_out_version="$(git -C .isar describe --tags)" + +if [ "$isar_version" = "$checked_out_version" ]; then + echo "isar is up-to-date." + exit 0 +fi +echo "Updating from version $checked_out_version to $isar_version." + +git -C .isar checkout "$isar_version" +cargo generate-lockfile --manifest-path .isar/Cargo.toml +mv .isar/Cargo.lock .isar-cargo.lock diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart index 9edabcc0d0b66..0216528ddd31f 100644 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ b/mobile/test/modules/activity/activity_statistics_provider_test.dart @@ -1,5 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/activities/activity.model.dart'; import 'package:immich_mobile/providers/activity_service.provider.dart'; import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:mocktail/mocktail.dart'; @@ -25,7 +26,7 @@ void main() { test('Returns the proper count family', () async { when( () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => 5); + ).thenAnswer((_) async => const ActivityStats(comments: 5)); // Read here to make the getStatistics call container.read(activityStatisticsProvider('test-album', 'test-asset')); @@ -50,7 +51,7 @@ void main() { test('Adds activity', () async { when( () => activityMock.getStatistics('test-album'), - ).thenAnswer((_) async => 10); + ).thenAnswer((_) async => const ActivityStats(comments: 10)); final provider = activityStatisticsProvider('test-album'); container.listen( @@ -71,7 +72,7 @@ void main() { test('Removes activity', () async { when( () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => 10); + ).thenAnswer((_) async => const ActivityStats(comments: 10)); final provider = activityStatisticsProvider('new-album', 'test-asset'); container.listen( diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart index a2aa7b2617b7f..013232da3ee82 100644 --- a/mobile/test/modules/shared/shared_mocks.dart +++ b/mobile/test/modules/shared/shared_mocks.dart @@ -1,11 +1,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/services/hash.service.dart'; import 'package:mocktail/mocktail.dart'; -class MockHashService extends Mock implements HashService {} - class MockCurrentUserProvider extends StateNotifier with Mock implements CurrentUserProvider { diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 07437289beeff..8520d89b435e5 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -7,8 +7,9 @@ import 'package:immich_mobile/services/immich_logger.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:isar/isar.dart'; +import '../../repository.mocks.dart'; +import '../../service.mocks.dart'; import '../../test_utils.dart'; -import 'shared_mocks.dart'; void main() { Asset makeAsset({ @@ -38,6 +39,10 @@ void main() { group('Test SyncService grouped', () { late final Isar db; final MockHashService hs = MockHashService(); + final MockEntityService entityService = MockEntityService(); + final MockAlbumMediaRepository albumMediaRepository = + MockAlbumMediaRepository(); + final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository(); final owner = User( id: "1", updatedAt: DateTime.now(), @@ -45,6 +50,7 @@ void main() { name: "first last", isAdmin: false, ); + late SyncService s; setUpAll(() async { WidgetsFlutterBinding.ensureInitialized(); db = await TestUtils.initIsar(); @@ -65,9 +71,15 @@ void main() { db.assets.clearSync(); db.assets.putAllSync(initialAssets); }); + s = SyncService( + db, + hs, + entityService, + albumMediaRepository, + albumApiRepository, + ); }); test('test inserting existing assets', () async { - SyncService s = SyncService(db, hs); final List remoteAssets = [ makeAsset(checksum: "a", remoteId: "0-1"), makeAsset(checksum: "b", remoteId: "2-1"), @@ -85,7 +97,6 @@ void main() { }); test('test inserting new assets', () async { - SyncService s = SyncService(db, hs); final List remoteAssets = [ makeAsset(checksum: "a", remoteId: "0-1"), makeAsset(checksum: "b", remoteId: "2-1"), @@ -106,7 +117,6 @@ void main() { }); test('test syncing duplicate assets', () async { - SyncService s = SyncService(db, hs); final List remoteAssets = [ makeAsset(checksum: "a", remoteId: "0-1"), makeAsset(checksum: "b", remoteId: "1-1"), @@ -154,7 +164,6 @@ void main() { }); test('test efficient sync', () async { - SyncService s = SyncService(db, hs); final List toUpsert = [ makeAsset(checksum: "a", remoteId: "0-1"), // changed makeAsset(checksum: "f", remoteId: "0-2"), // new diff --git a/mobile/test/repository.mocks.dart b/mobile/test/repository.mocks.dart new file mode 100644 index 0000000000000..6e220e85a2dc2 --- /dev/null +++ b/mobile/test/repository.mocks.dart @@ -0,0 +1,25 @@ +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/asset_media.interface.dart'; +import 'package:immich_mobile/interfaces/backup.interface.dart'; +import 'package:immich_mobile/interfaces/file_media.interface.dart'; +import 'package:immich_mobile/interfaces/user.interface.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAlbumRepository extends Mock implements IAlbumRepository {} + +class MockAssetRepository extends Mock implements IAssetRepository {} + +class MockUserRepository extends Mock implements IUserRepository {} + +class MockBackupRepository extends Mock implements IBackupRepository {} + +class MockAlbumMediaRepository extends Mock implements IAlbumMediaRepository {} + +class MockAssetMediaRepository extends Mock implements IAssetMediaRepository {} + +class MockFileMediaRepository extends Mock implements IFileMediaRepository {} + +class MockAlbumApiRepository extends Mock implements IAlbumApiRepository {} diff --git a/mobile/test/service.mocks.dart b/mobile/test/service.mocks.dart new file mode 100644 index 0000000000000..de49a98cc4e5a --- /dev/null +++ b/mobile/test/service.mocks.dart @@ -0,0 +1,16 @@ +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/entity.service.dart'; +import 'package:immich_mobile/services/hash.service.dart'; +import 'package:immich_mobile/services/sync.service.dart'; +import 'package:immich_mobile/services/user.service.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockApiService extends Mock implements ApiService {} + +class MockUserService extends Mock implements UserService {} + +class MockSyncService extends Mock implements SyncService {} + +class MockHashService extends Mock implements HashService {} + +class MockEntityService extends Mock implements EntityService {} diff --git a/mobile/test/services/album.service_test.dart b/mobile/test/services/album.service_test.dart new file mode 100644 index 0000000000000..b2c2ec4427ab4 --- /dev/null +++ b/mobile/test/services/album.service_test.dart @@ -0,0 +1,196 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:mocktail/mocktail.dart'; +import '../fixtures/album.stub.dart'; +import '../fixtures/asset.stub.dart'; +import '../fixtures/user.stub.dart'; +import '../repository.mocks.dart'; +import '../service.mocks.dart'; + +void main() { + late AlbumService sut; + late MockUserService userService; + late MockSyncService syncService; + late MockEntityService entityService; + late MockAlbumRepository albumRepository; + late MockAssetRepository assetRepository; + late MockBackupRepository backupRepository; + late MockAlbumMediaRepository albumMediaRepository; + late MockAlbumApiRepository albumApiRepository; + + setUp(() { + userService = MockUserService(); + syncService = MockSyncService(); + entityService = MockEntityService(); + albumRepository = MockAlbumRepository(); + assetRepository = MockAssetRepository(); + backupRepository = MockBackupRepository(); + albumMediaRepository = MockAlbumMediaRepository(); + albumApiRepository = MockAlbumApiRepository(); + + sut = AlbumService( + userService, + syncService, + entityService, + albumRepository, + assetRepository, + backupRepository, + albumMediaRepository, + albumApiRepository, + ); + }); + + group('refreshDeviceAlbums', () { + test('empty selection with one album in db', () async { + when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)) + .thenAnswer((_) async => []); + when(() => backupRepository.getIdsBySelection(BackupSelection.select)) + .thenAnswer((_) async => []); + when(() => albumRepository.count(local: true)).thenAnswer((_) async => 1); + when(() => syncService.removeAllLocalAlbumsAndAssets()) + .thenAnswer((_) async => true); + final result = await sut.refreshDeviceAlbums(); + expect(result, false); + verify(() => syncService.removeAllLocalAlbumsAndAssets()); + }); + + test('one selected albums, two on device', () async { + when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)) + .thenAnswer((_) async => []); + when(() => backupRepository.getIdsBySelection(BackupSelection.select)) + .thenAnswer((_) async => [AlbumStub.oneAsset.localId!]); + when(() => albumMediaRepository.getAll()) + .thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); + when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())) + .thenAnswer((_) async => true); + final result = await sut.refreshDeviceAlbums(); + expect(result, true); + verify( + () => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null), + ).called(1); + verifyNoMoreInteractions(syncService); + }); + }); + group('refreshRemoteAlbums', () { + test('isShared: false', () async { + when(() => userService.refreshUsers()).thenAnswer((_) async => true); + when(() => albumApiRepository.getAll(shared: null)) + .thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]); + when( + () => syncService.syncRemoteAlbumsToDb( + [AlbumStub.oneAsset, AlbumStub.twoAsset], + isShared: false, + ), + ).thenAnswer((_) async => true); + final result = await sut.refreshRemoteAlbums(isShared: false); + expect(result, true); + verify(() => userService.refreshUsers()).called(1); + verify(() => albumApiRepository.getAll(shared: null)).called(1); + verify( + () => syncService.syncRemoteAlbumsToDb( + [AlbumStub.oneAsset, AlbumStub.twoAsset], + isShared: false, + ), + ).called(1); + verifyNoMoreInteractions(userService); + verifyNoMoreInteractions(albumApiRepository); + verifyNoMoreInteractions(syncService); + }); + }); + + group('createAlbum', () { + test('shared with assets', () async { + when( + () => albumApiRepository.create( + "name", + assetIds: any(named: "assetIds"), + sharedUserIds: any(named: "sharedUserIds"), + ), + ).thenAnswer((_) async => AlbumStub.oneAsset); + + when( + () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), + ).thenAnswer((_) async => AlbumStub.oneAsset); + + when( + () => albumRepository.create(AlbumStub.oneAsset), + ).thenAnswer((_) async => AlbumStub.twoAsset); + + final result = + await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]); + expect(result, AlbumStub.twoAsset); + verify( + () => albumApiRepository.create( + "name", + assetIds: [AssetStub.image1.remoteId!], + sharedUserIds: [UserStub.user1.id], + ), + ).called(1); + verify( + () => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset), + ).called(1); + }); + }); + + group('addAdditionalAssetToAlbum', () { + test('one added, one duplicate', () async { + when( + () => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()), + ).thenAnswer( + (_) async => ( + added: [AssetStub.image2.remoteId!], + duplicates: [AssetStub.image1.remoteId!] + ), + ); + when( + () => albumRepository.getById(AlbumStub.oneAsset.id), + ).thenAnswer((_) async => AlbumStub.oneAsset); + when( + () => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2]), + ).thenAnswer((_) async {}); + when( + () => albumRepository.removeAssets(AlbumStub.oneAsset, []), + ).thenAnswer((_) async {}); + when( + () => albumRepository.recalculateMetadata(AlbumStub.oneAsset), + ).thenAnswer((_) async => AlbumStub.oneAsset); + when( + () => albumRepository.update(AlbumStub.oneAsset), + ).thenAnswer((_) async => AlbumStub.oneAsset); + + final result = await sut.addAdditionalAssetToAlbum( + [AssetStub.image1, AssetStub.image2], + AlbumStub.oneAsset, + ); + + expect(result != null, true); + expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]); + expect(result.successfullyAdded, 1); + }); + }); + + group('addAdditionalUserToAlbum', () { + test('one added', () async { + when( + () => + albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()), + ).thenAnswer( + (_) async => AlbumStub.sharedWithUser, + ); + when( + () => entityService + .fillAlbumWithDatabaseEntities(AlbumStub.sharedWithUser), + ).thenAnswer((_) async => AlbumStub.sharedWithUser); + when( + () => albumRepository.update(AlbumStub.sharedWithUser), + ).thenAnswer((_) async => AlbumStub.sharedWithUser); + + final result = await sut.addAdditionalUserToAlbum( + [UserStub.user2.id], + AlbumStub.emptyAlbum, + ); + expect(result, true); + }); + }); +} diff --git a/mobile/test/services/entity.service_test.dart b/mobile/test/services/entity.service_test.dart new file mode 100644 index 0000000000000..8c8b49a7e02a5 --- /dev/null +++ b/mobile/test/services/entity.service_test.dart @@ -0,0 +1,76 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/services/entity.service.dart'; +import 'package:mocktail/mocktail.dart'; +import '../fixtures/asset.stub.dart'; +import '../fixtures/user.stub.dart'; +import '../repository.mocks.dart'; + +void main() { + late EntityService sut; + late MockAssetRepository assetRepository; + late MockUserRepository userRepository; + + setUp(() { + assetRepository = MockAssetRepository(); + userRepository = MockUserRepository(); + sut = EntityService(assetRepository, userRepository); + }); + + group('fillAlbumWithDatabaseEntities', () { + test('remote album with owner, thumbnail, sharedUsers and assets', + () async { + final Album album = Album( + name: "album-with-two-assets-and-two-users", + localId: "album-with-two-assets-and-two-users-local", + remoteId: "album-with-two-assets-and-two-users-remote", + createdAt: DateTime(2001), + modifiedAt: DateTime(2010), + shared: true, + activityEnabled: true, + startDate: DateTime(2019), + endDate: DateTime(2020), + ) + ..remoteThumbnailAssetId = AssetStub.image1.remoteId + ..assets.addAll([AssetStub.image1, AssetStub.image1]) + ..owner.value = UserStub.user1 + ..sharedUsers.addAll([UserStub.admin, UserStub.admin]); + + when(() => userRepository.get(album.ownerId!)) + .thenAnswer((_) async => UserStub.admin); + + when(() => assetRepository.getByRemoteId(AssetStub.image1.remoteId!)) + .thenAnswer((_) async => AssetStub.image1); + + when(() => userRepository.getByIds(any())) + .thenAnswer((_) async => [UserStub.user1, UserStub.user2]); + + when(() => assetRepository.getAllByRemoteId(any())) + .thenAnswer((_) async => [AssetStub.image1, AssetStub.image2]); + + await sut.fillAlbumWithDatabaseEntities(album); + expect(album.owner.value, UserStub.admin); + expect(album.thumbnail.value, AssetStub.image1); + expect(album.remoteUsers.toSet(), {UserStub.user1, UserStub.user2}); + expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2}); + }); + + test('remote album without any info', () async { + makeEmptyAlbum() => Album( + name: "album-without-info", + localId: "album-without-info-local", + remoteId: "album-without-info-remote", + createdAt: DateTime(2001), + modifiedAt: DateTime(2010), + shared: false, + activityEnabled: false, + ); + + final album = makeEmptyAlbum(); + await sut.fillAlbumWithDatabaseEntities(album); + verifyNoMoreInteractions(assetRepository); + verifyNoMoreInteractions(userRepository); + expect(album, makeEmptyAlbum()); + }); + }); +} diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index 2ca04630468f9..bf8b24b55703b 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -9,11 +9,7 @@ function dart { wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache patch --no-backup-if-mismatch -u native_class.mustache header}} -{{>part_of}} -class ApiClient { - ApiClient({this.basePath = '{{{basePath}}}', this.authentication,}); - - final String basePath; - final Authentication? authentication; - - var _client = Client(); - final _defaultHeaderMap = {}; - - /// Returns the current HTTP [Client] instance to use in this class. - /// - /// The return value is guaranteed to never be null. - Client get client => _client; - - /// Requests to use a new HTTP [Client] in this class. - set client(Client newClient) { - _client = newClient; - } - - Map get defaultHeaderMap => _defaultHeaderMap; - - void addDefaultHeader(String key, String value) { - _defaultHeaderMap[key] = value; - } - - // We don't use a Map for queryParams. - // If collectionFormat is 'multi', a key might appear multiple times. - Future invokeAPI( - String path, - String method, - List queryParams, - Object? body, - Map headerParams, - Map formParams, - String? contentType, - ) async { - await authentication?.applyToParams(queryParams, headerParams); - - headerParams.addAll(_defaultHeaderMap); - if (contentType != null) { - headerParams['Content-Type'] = contentType; - } - - final urlEncodedQueryParams = queryParams.map((param) => '$param'); - final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : ''; - final uri = Uri.parse('$basePath$path$queryString'); - - try { - // Special case for uploading a single file which isn't a 'multipart/form-data'. - if ( - body is MultipartFile && (contentType == null || - !contentType.toLowerCase().startsWith('multipart/form-data')) - ) { - final request = StreamedRequest(method, uri); - request.headers.addAll(headerParams); - request.contentLength = body.length; - body.finalize().listen( - request.sink.add, - onDone: request.sink.close, - // ignore: avoid_types_on_closure_parameters - onError: (Object error, StackTrace trace) => request.sink.close(), - cancelOnError: true, - ); - final response = await _client.send(request); - return Response.fromStream(response); - } - - if (body is MultipartRequest) { - final request = MultipartRequest(method, uri); - request.fields.addAll(body.fields); - request.files.addAll(body.files); - request.headers.addAll(body.headers); - request.headers.addAll(headerParams); - final response = await _client.send(request); - return Response.fromStream(response); - } - - final msgBody = contentType == 'application/x-www-form-urlencoded' - ? formParams - : await serializeAsync(body); - final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; - - switch(method) { - case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); - case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); - case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); - case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); - case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); - } - } on SocketException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'Socket operation failed: $method $path', - error, - trace, - ); - } on TlsException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'TLS/SSL communication failed: $method $path', - error, - trace, - ); - } on IOException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'I/O operation failed: $method $path', - error, - trace, - ); - } on ClientException catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'HTTP connection failed: $method $path', - error, - trace, - ); - } on Exception catch (error, trace) { - throw ApiException.withInner( - HttpStatus.badRequest, - 'Exception occurred: $method $path', - error, - trace, - ); - } - - throw ApiException( - HttpStatus.badRequest, - 'Invalid HTTP operation: $method $path', - ); - } -{{#native_serialization}} - - Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => - // ignore: deprecated_member_use_from_same_package - deserialize(value, targetType, growable: growable); - - @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') - dynamic deserialize(String value, String targetType, {bool growable = false,}) { - // Remove all spaces. Necessary for regular expressions as well. - targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments - - // If the expected target type is String, nothing to do... - return targetType == 'String' - ? value - : fromJson(json.decode(value), targetType, growable: growable); - } -{{/native_serialization}} - - // ignore: deprecated_member_use_from_same_package - Future serializeAsync(Object? value) async => serialize(value); - - @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.') - String serialize(Object? value) => value == null ? '' : json.encode(value); - -{{#native_serialization}} - /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. - static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { - upgradeDto(value, targetType); - try { - switch (targetType) { - case 'String': - return value is String ? value : value.toString(); - case 'int': - return value is int ? value : int.parse('$value'); - case 'double': - return value is double ? value : double.parse('$value'); - case 'bool': - if (value is bool) { - return value; - } - final valueString = '$value'.toLowerCase(); - return valueString == 'true' || valueString == '1'; - case 'DateTime': - return value is DateTime ? value : DateTime.tryParse(value); - {{#models}} - {{#model}} - case '{{{classname}}}': - {{#isEnum}} - {{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}} - {{/isEnum}} - {{^isEnum}} - return {{{classname}}}.fromJson(value); - {{/isEnum}} - {{/model}} - {{/models}} - default: - dynamic match; - if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) { - return value - .map((dynamic v) => fromJson(v, match, growable: growable,)) - .toList(growable: growable); - } - if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) { - return value - .map((dynamic v) => fromJson(v, match, growable: growable,)) - .toSet(); - } - if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) { - return Map.fromIterables( - value.keys.cast(), - value.values.map((dynamic v) => fromJson(v, match, growable: growable,)), - ); - } - } - } on Exception catch (error, trace) { - throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,); - } - throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',); - } -{{/native_serialization}} -} -{{#native_serialization}} - -/// Primarily intended for use in an isolate. -class DeserializationMessage { - const DeserializationMessage({ - required this.json, - required this.targetType, - this.growable = false, - }); - - /// The JSON value to deserialize. - final String json; - - /// Target type to deserialize to. - final String targetType; - - /// Whether to make deserialized lists or maps growable. - final bool growable; -} - -/// Primarily intended for use in an isolate. -Future decodeAsync(DeserializationMessage message) async { - // Remove all spaces. Necessary for regular expressions as well. - final targetType = message.targetType.replaceAll(' ', ''); - - // If the expected target type is String, nothing to do... - return targetType == 'String' - ? message.json - : json.decode(message.json); -} - -/// Primarily intended for use in an isolate. -Future deserializeAsync(DeserializationMessage message) async { - // Remove all spaces. Necessary for regular expressions as well. - final targetType = message.targetType.replaceAll(' ', ''); - - // If the expected target type is String, nothing to do... - return targetType == 'String' - ? message.json - : ApiClient.fromJson( - json.decode(message.json), - targetType, - growable: message.growable, - ); -} -{{/native_serialization}} - -/// Primarily intended for use in an isolate. -Future serializeAsync(Object? value) async => value == null ? '' : json.encode(value); diff --git a/open-api/templates/mobile/api_client.mustache.patch b/open-api/templates/mobile/api_client.mustache.patch deleted file mode 100644 index 3805cd8f7934a..0000000000000 --- a/open-api/templates/mobile/api_client.mustache.patch +++ /dev/null @@ -1,10 +0,0 @@ ---- api_client.mustache 2024-08-13 14:29:04.056364916 -0500 -+++ api_client_new.mustache 2024-08-13 14:29:36.224410735 -0500 -@@ -159,6 +159,7 @@ - {{#native_serialization}} - /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. - static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { -+ upgradeDto(value, targetType); - try { - switch (targetType) { - case 'String': diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache b/open-api/templates/mobile/serialization/native/native_class.mustache index 254843e00eefa..9a7b1439b1fdf 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache +++ b/open-api/templates/mobile/serialization/native/native_class.mustache @@ -111,6 +111,7 @@ class {{{classname}}} { /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods static {{{classname}}}? fromJson(dynamic value) { + upgradeDto(value, "{{{classname}}}"); if (value is Map) { final json = value.cast(); diff --git a/open-api/templates/mobile/serialization/native/native_class.mustache.patch b/open-api/templates/mobile/serialization/native/native_class.mustache.patch index 02e07f933a30d..4ba65949665f8 100644 --- a/open-api/templates/mobile/serialization/native/native_class.mustache.patch +++ b/open-api/templates/mobile/serialization/native/native_class.mustache.patch @@ -1,5 +1,5 @@ ---- native_class.mustache 2023-08-31 23:09:59.584269162 +0200 -+++ native_class1.mustache 2023-08-31 22:59:53.633083270 +0200 +--- native_class.mustache 2024-09-19 11:41:07.855683995 -0400 ++++ native_class_temp.mustache 2024-09-19 11:41:57.113249395 -0400 @@ -91,14 +91,14 @@ {{/isDateTime}} {{#isNullable}} @@ -17,10 +17,14 @@ } {{/defaultValue}} {{/required}} -@@ -114,17 +114,6 @@ +@@ -111,20 +111,10 @@ + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static {{{classname}}}? fromJson(dynamic value) { ++ upgradeDto(value, "{{{classname}}}"); if (value is Map) { final json = value.cast(); - + - // Ensure that the map contains the required keys. - // Note 1: the values aren't checked for validity beyond being non-null. - // Note 2: this code is stripped in release mode! @@ -35,9 +39,9 @@ return {{{classname}}}( {{#vars}} {{#isDateTime}} -@@ -215,6 +204,10 @@ +@@ -215,6 +205,10 @@ ? {{#defaultValue}}{{{.}}}{{/defaultValue}}{{^defaultValue}}null{{/defaultValue}} - : {{{datatypeWithEnum}}}.parse(json[r'{{{baseName}}}'].toString()), + : {{/isNullable}}{{{datatypeWithEnum}}}.parse('${json[r'{{{baseName}}}']}'), {{/isNumber}} + {{#isDouble}} + {{{name}}}: (mapValueOfType(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}).toDouble(), @@ -46,7 +50,7 @@ {{^isNumber}} {{^isEnum}} {{{name}}}: mapValueOfType<{{{datatypeWithEnum}}}>(json, r'{{{baseName}}}'){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, -@@ -223,6 +216,7 @@ +@@ -223,6 +217,7 @@ {{{name}}}: {{{enumName}}}.fromJson(json[r'{{{baseName}}}']){{#required}}{{^isNullable}}!{{/isNullable}}{{/required}}{{^required}}{{#defaultValue}} ?? {{{.}}}{{/defaultValue}}{{/required}}, {{/isEnum}} {{/isNumber}} diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 6d5b78ee9a588..72d7a3ec546d8 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,18 +1,18 @@ { "name": "@immich/sdk", - "version": "1.114.0", + "version": "1.116.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.114.0", + "version": "1.116.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "typescript": "^5.3.3" } }, @@ -22,9 +22,9 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", - "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dev": true, "license": "MIT", "dependencies": { @@ -32,9 +32,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": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index afa5f4585810b..41bc3a3b16017 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.114.0", + "version": "1.116.2", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "typescript": "^5.3.3" }, "repository": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 7cf4d48eda66b..aa3501079bf86 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.114.0 + * 1.116.2 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -19,6 +19,7 @@ export type UserResponseDto = { email: string; id: string; name: string; + profileChangedAt: string; profileImagePath: string; }; export type ActivityResponseDto = { @@ -53,6 +54,7 @@ export type UserAdminResponseDto = { license: (UserLicense) | null; name: string; oauthId: string; + profileChangedAt: string; profileImagePath: string; quotaSizeInBytes: number | null; quotaUsageInBytes: number | null; @@ -364,7 +366,6 @@ export type AssetMediaCreateDto = { fileModifiedAt: string; isArchived?: boolean; isFavorite?: boolean; - isOffline?: boolean; isVisible?: boolean; livePhotoVideoId?: string; sidecarData?: Blob; @@ -427,7 +428,7 @@ export type UpdateAssetDto = { isArchived?: boolean; isFavorite?: boolean; latitude?: number; - livePhotoVideoId?: string; + livePhotoVideoId?: string | null; longitude?: number; rating?: number; }; @@ -548,6 +549,9 @@ export type AllJobStatusResponseDto = { thumbnailGeneration: JobStatusDto; videoConversion: JobStatusDto; }; +export type JobCreateDto = { + name: ManualJobName; +}; export type JobCommandDto = { command: JobCommand; force: boolean; @@ -574,10 +578,6 @@ export type UpdateLibraryDto = { importPaths?: string[]; name?: string; }; -export type ScanLibraryDto = { - refreshAllFiles?: boolean; - refreshModifiedFiles?: boolean; -}; export type LibraryStatsResponseDto = { photos: number; total: number; @@ -651,6 +651,9 @@ export type SystemConfigSmtpDto = { replyTo: string; transport: SystemConfigSmtpTransportDto; }; +export type TestEmailResponseDto = { + messageId: string; +}; export type OAuthConfigDto = { redirectUri: string; }; @@ -666,6 +669,7 @@ export type PartnerResponseDto = { id: string; inTimeline?: boolean; name: string; + profileChangedAt: string; profileImagePath: string; }; export type UpdatePartnerDto = { @@ -831,6 +835,39 @@ export type PlacesResponseDto = { longitude: number; name: string; }; +export type RandomSearchDto = { + city?: string | null; + country?: string | null; + createdAfter?: string; + createdBefore?: string; + deviceId?: string; + isArchived?: boolean; + isEncoded?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isNotInAlbum?: boolean; + isOffline?: boolean; + isVisible?: boolean; + lensModel?: string | null; + libraryId?: string | null; + make?: string; + model?: string | null; + personIds?: string[]; + size?: number; + state?: string | null; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + "type"?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + withArchived?: boolean; + withDeleted?: boolean; + withExif?: boolean; + withPeople?: boolean; + withStacked?: boolean; +}; export type SmartSearchDto = { city?: string | null; country?: string | null; @@ -888,6 +925,8 @@ export type ServerConfigDto = { isInitialized: boolean; isOnboarded: boolean; loginPageMessage: string; + mapDarkStyleUrl: string; + mapLightStyleUrl: string; oauthButtonText: string; trashDays: number; userDeleteDelay: number; @@ -1060,14 +1099,16 @@ export type SystemConfigFFmpegDto = { transcode: TranscodePolicy; twoPass: boolean; }; +export type SystemConfigGeneratedImageDto = { + format: ImageFormat; + quality: number; + size: number; +}; export type SystemConfigImageDto = { colorspace: Colorspace; extractEmbedded: boolean; - previewFormat: ImageFormat; - previewSize: number; - quality: number; - thumbnailFormat: ImageFormat; - thumbnailSize: number; + preview: SystemConfigGeneratedImageDto; + thumbnail: SystemConfigGeneratedImageDto; }; export type JobSettingsDto = { concurrency: number; @@ -1240,6 +1281,9 @@ export type TimeBucketResponseDto = { count: number; timeBucket: string; }; +export type TrashResponseDto = { + count: number; +}; export type UserUpdateMeDto = { email?: string; name?: string; @@ -1249,6 +1293,7 @@ export type CreateProfileImageDto = { file: Blob; }; export type CreateProfileImageResponseDto = { + profileChangedAt: string; profileImagePath: string; userId: string; }; @@ -1686,6 +1731,9 @@ export function getMemoryLane({ day, month }: { ...opts })); } +/** + * This property was deprecated in v1.116.0 + */ export function getRandom({ count }: { count?: number; }, opts?: Oazapfts.RequestOpts) { @@ -1941,6 +1989,15 @@ export function getAllJobsStatus(opts?: Oazapfts.RequestOpts) { ...opts })); } +export function createJob({ jobCreateDto }: { + jobCreateDto: JobCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/jobs", oazapfts.json({ + ...opts, + method: "POST", + body: jobCreateDto + }))); +} export function sendJobCommand({ id, jobCommandDto }: { id: JobName; jobCommandDto: JobCommandDto; @@ -2005,24 +2062,14 @@ export function updateLibrary({ id, updateLibraryDto }: { body: updateLibraryDto }))); } -export function removeOfflineFiles({ id }: { +export function scanLibrary({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/removeOffline`, { + return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/scan`, { ...opts, method: "POST" })); } -export function scanLibrary({ id, scanLibraryDto }: { - id: string; - scanLibraryDto: ScanLibraryDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText(`/libraries/${encodeURIComponent(id)}/scan`, oazapfts.json({ - ...opts, - method: "POST", - body: scanLibraryDto - }))); -} export function getLibraryStatistics({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { @@ -2082,20 +2129,6 @@ export function reverseGeocode({ lat, lon }: { ...opts })); } -export function getMapStyle({ key, theme }: { - key?: string; - theme: MapTheme; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: object; - }>(`/map/style.json${QS.query(QS.explode({ - key, - theme - }))}`, { - ...opts - })); -} export function searchMemories(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; @@ -2176,7 +2209,10 @@ export function addMemoryAssets({ id, bulkIdsDto }: { export function sendTestEmail({ systemConfigSmtpDto }: { systemConfigSmtpDto: SystemConfigSmtpDto; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/notifications/test-email", oazapfts.json({ + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TestEmailResponseDto; + }>("/notifications/test-email", oazapfts.json({ ...opts, method: "POST", body: systemConfigSmtpDto @@ -2481,6 +2517,18 @@ export function searchPlaces({ name }: { ...opts })); } +export function searchRandom({ randomSearchDto }: { + randomSearchDto: RandomSearchDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: AssetResponseDto[]; + }>("/search/random", oazapfts.json({ + ...opts, + method: "POST", + body: randomSearchDto + }))); +} export function searchSmart({ smartSearchDto }: { smartSearchDto: SmartSearchDto; }, opts?: Oazapfts.RequestOpts) { @@ -3057,13 +3105,19 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key })); } export function emptyTrash(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/trash/empty", { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TrashResponseDto; + }>("/trash/empty", { ...opts, method: "POST" })); } export function restoreTrash(opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/trash/restore", { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TrashResponseDto; + }>("/trash/restore", { ...opts, method: "POST" })); @@ -3071,7 +3125,10 @@ export function restoreTrash(opts?: Oazapfts.RequestOpts) { export function restoreAssets({ bulkIdsDto }: { bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/trash/restore/assets", oazapfts.json({ + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TrashResponseDto; + }>("/trash/restore/assets", oazapfts.json({ ...opts, method: "POST", body: bulkIdsDto @@ -3364,6 +3421,11 @@ export enum EntityType { Asset = "ASSET", Album = "ALBUM" } +export enum ManualJobName { + PersonCleanup = "person-cleanup", + TagCleanup = "tag-cleanup", + UserCleanup = "user-cleanup" +} export enum JobName { ThumbnailGeneration = "thumbnailGeneration", MetadataExtraction = "metadataExtraction", @@ -3387,10 +3449,6 @@ export enum JobCommand { Empty = "empty", ClearFailed = "clear-failed" } -export enum MapTheme { - Light = "light", - Dark = "dark" -} export enum MemoryType { OnThisDay = "on_this_day" } diff --git a/readme_i18n/README_pt_BR.md b/readme_i18n/README_pt_BR.md index d872b8435b058..51ea8238dabb0 100644 --- a/readme_i18n/README_pt_BR.md +++ b/readme_i18n/README_pt_BR.md @@ -1,84 +1,90 @@ -

-
+

+
Licença: AGPLv3 Discord -
-
+
+

- +

-

Immich - Solução self-hosted de alta performance para backup de fotos e vídeos

+

Solução self-hosted de alta performance para backup de fotos e vídeos


- +

- English - Català - Español - Français - Italiano - 日本語 - 한국어 - Deutsch - Nederlands - Türkçe - 中文 - Русский - Svenska - العربية + +English +Català +Español +Français +Italiano +日本語 +한국어 +Deutsch +Nederlands +Türkçe +中文 +Русский +Svenska +العربية +Tiếng Việt +

## Avisos - ⚠️ Este projeto está sob **desenvolvimento constante**. -- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a compatibilidade com versões anteriores). -- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e vídeos.** -- ⚠️ Sempre siga o plano [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup para as suas mídias preciosas! +- ⚠️ Podem ocorrer bugs e _breaking changes_ (alterações que quebram a + compatibilidade com versões anteriores). +- ⚠️ **Não use esta solução como a única forma de fazer backup das suas fotos e + vídeos.** +- ⚠️ Sempre siga o plano + [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) de backup + para as suas mídias preciosas! -## Conteúdo +> [!NOTE] +> Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/. -- [Documentação Oficial](https://immich.app/docs) -- [Roadmap](https://github.com/orgs/immich-app/projects/1) -- [Demonstração](#demo) -- [Recursos](#features) -- [Introdução](https://immich.app/docs/overview/introduction) +## Links + +- [Documentação](https://immich.app/docs) +- [Sobre](https://immich.app/docs/overview/introduction) - [Instalação](https://immich.app/docs/install/requirements) +- [Roadmap](https://github.com/orgs/immich-app/projects/1) +- [Demonstração](#demonstração) +- [Funcionalidades](#funcionalidades) +- [Traduções](https://immich.app/docs/developer/translations) - [Diretrizes de Contribuição](https://immich.app/docs/overview/support-the-project) -## Documentação - -Você pode encontrar a documentação principal, incluindo guias de instalação, em https://immich.app/. - ## Demonstração -Você pode acessar a demonstração web em https://demo.immich.app +Acesse a demonstração [aqui](https://demo.immich.app). A demonstração está +hospedada no Nível Gratuito da Oracle VM em Amsterdam com um processador 2.4Ghz +quad-core ARM64 e 24GB de RAM. -No aplicativo para dispositivos móveis, você pode usar `https://demo.immich.app/api` no campo `Server Endpoint URL` +No aplicativo para dispositivos móveis, você pode usar +`https://demo.immich.app/api` no campo `Server Endpoint URL` -```bash title="Credenciais de Demonstração" -Credenciais de Demonstração -email: demo@immich.app -senha: demo -``` +### Credenciais de login -``` -Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM -``` +| Email | Senha | +| --------------- | ----- | +| demo@immich.app | demo | ## Atividades + ![Atividades](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de Analytics do Repobeats") -## Recursos +## Funcionalidades - -| Recursos | Aplicativo Móvel | Web | -|:----------------------------------------------------|------------------|-----| +| Funcionalidades | Aplicativo Móvel | Web | +| :-------------------------------------------------- | ---------------- | --- | | Fazer upload e visualizar fotos e vídeos | Sim | Sim | | Backup automático ao abrir o aplicativo | Sim | N/A | | Prevenir a duplicação de arquivos | Sim | Sim | @@ -88,17 +94,17 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR | Criação de álbuns e álbuns compartilhados | Sim | Sim | | Barra de rolagem arrastável | Sim | Sim | | Suporta formatos RAW | Sim | Sim | -| Visualização de metadados (EXIF, map) | Sim | Sim | -| Pesquisar por metadados, objetos, rostos, and CLIP | Sim | Sim | +| Visualização de metadados (EXIF, mapa) | Sim | Sim | +| Pesquisar por metadados, objetos, rostos, e CLIP | Sim | Sim | | Funções administrativas (gerenciamento de usuários) | Não | Sim | | Backup em segundo plano | Sim | N/A | -| Virtual scroll | Sim | Sim | +| Rolagem virtual | Sim | Sim | | Suporte OAuth | Sim | Sim | | Chaves de API | N/A | Sim | -| Backup e visualização de LivePhoto/MotionPhoto | Sim | Sim | +| Backup e reprodução de LivePhoto/MotionPhoto | Sim | Sim | | Visualização de imagens 360º | Não | Sim | | Estrutura de armazenamento definida pelo usuário | Sim | Sim | -| Compartilhar com o público | Não | Sim | +| Compartilhar com o público | Sim | Sim | | Arquivo e Favoritos | Sim | Sim | | Mapa Global | Sim | Sim | | Compartilhamento com parceiro | Sim | Sim | @@ -108,6 +114,29 @@ Especificações: Nível Gratuito da Oracle VM - Amsterdam - 2.4Ghz quad-core AR | Galeria em modo apenas leitura | Sim | Sim | | Empilhamento de fotos | Sim | Sim | +## Traduções + +Leia mais sobre as traduções +[aqui](https://immich.app/docs/developer/translations). + + +Status da tradução + + +## Atividade do repositório + +![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Imagem de análise de atividade Repobeats") + +## Histórico de estrelas + + + + + + Gráfico de histórico de estrelas + + + ## Contribuidores diff --git a/readme_i18n/README_vi_VN.md b/readme_i18n/README_vi_VN.md new file mode 100644 index 0000000000000..7ec4b9c948a2a --- /dev/null +++ b/readme_i18n/README_vi_VN.md @@ -0,0 +1,133 @@ +

+
+
Giấy phép: AGPLv3 + + Discord + +
+
+

+ +

+ +

+

Giải pháp quản lý ảnh và video tự lưu trữ hiệu suất cao

+
+ + + +
+

+ +English +Català +Español +Français +Italiano +日本語 +한국어 +Deutsch +Nederlands +Türkçe +中文 +Русский +Português Brasileiro +Svenska +العربية +Tiếng Việt + +

+ +## Tuyên bố miễn trừ trách nhiệm + +- ⚠️ Dự án đang được phát triển **rất tích cực**. +- ⚠️ Dự kiến ​​sẽ có lỗi và thay đổi đột ngột. +- ⚠️ **Không sử dụng ứng dụng như là cách duy nhất để lưu trữ ảnh và video của bạn.** +- ⚠️ Luôn tuân thủ kế hoạch sao lưu [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) cho những bức ảnh và video quý giá của bạn! + +> [!NOTE] +> Bạn có thể tìm thấy tài liệu chính, bao gồm hướng dẫn cài đặt, tại https://immich.app/. + +## Liên kết + +- [Tài liệu](https://immich.app/docs) +- [Giới thiệu](https://immich.app/docs/overview/introduction) +- [Cài đặt](https://immich.app/docs/install/requirements) +- [Lộ trình](https://immich.app/roadmap) +- [Demo](#demo) +- [Tính năng](#Tính-năng) +- [Dịch thuật](https://immich.app/docs/developer/translations) +- [Đóng góp](https://immich.app/docs/overview/support-the-project) + +## Demo + +Truy cập bản demo [tại đây](https://demo.immich.app). Bản demo đang chạy trên máy ảo Oracle Free-tier ở Amsterdam với CPU ARM64 lõi tứ 2,4 GHz và RAM 24 GB. + +Đối với ứng dụng di động, bạn có thể sử dụng `https://demo.immich.app/api` cho `Server Endpoint URL` + +### Thông tin đăng nhập + +| Email | Mật khẩu | +| --------------- | -------- | +| demo@immich.app | demo | + +## Tính năng + +| Tính năng | Mobile | Web | +| :--------------------------------------------------- | ------ | ----- | +| Tải lên và xem video, ảnh | Có | Có | +| Tự động sao lưu khi ứng dụng được mở | Có | N/A | +| Ngăn chặn sự trùng lặp nội dung | Có | Có | +| Album được chọn để sao lưu | Có | N/A | +| Tải ảnh và video xuống thiết bị cục bộ | Có | Có | +| Hỗ trợ nhiều người dùng | Có | Có | +| Album và Album được chia sẻ | Có | Có | +| Thanh cuộn có thể chà / kéo | Có | Có | +| Hỗ trợ định dạng raw | Có | Có | +| Xem metadata (EXIF, bản đồ) | Có | Có | +| Tìm kiếm theo metadata, đối tượng, khuôn mặt và CLIP | Có | Có | +| Chức năng quản trị (quản lý người dùng) | Không | Có | +| Sao lưu trong nền | Có | N/A | +| Cuộn ảo | Có | Có | +| Hỗ trợ OAuth | Có | Có | +| API Keys | N/A | Có | +| Sao lưu và phát lại Live Photo/Motion Photo | Có | Có | +| Hỗ trợ hiển thị hình ảnh 360 độ | Không | Có | +| Cấu trúc lưu trữ do người dùng xác định | Có | Có | +| Chia sẻ công khai | Có | Có | +| Lưu trữ và Yêu thích | Có | Có | +| Bản đồ toàn cầu | Có | Có | +| Chia sẻ đối tác | Có | Có | +| Nhận dạng khuôn mặt và phân cụm | Có | Có | +| Kỷ niệm (x năm trước) | Có | Có | +| Hỗ trợ ngoại tuyến | Có | Không | +| Thư viện chỉ đọc | Có | Có | +| Ảnh xếp chồng | Có | Có | + +## Dịch thuật + +Đọc thêm về dịch thuật [tại đây](https://immich.app/docs/developer/translations). + + +Tình trạng dịch thuật + + +## Hoạt động của repository + +![Hoạt động](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Hình ảnh phân tích Repobeats") + +## Lịch sử Đánh dấu sao + + + + + + Biểu đồ Lịch sử Đánh dấu + + + +## Người đóng góp + + + + diff --git a/server/Dockerfile b/server/Dockerfile index 04a495e090a53..66965c0edb73e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240903@sha256:ca18e2805ec8ddcf0ac7734a6eaf6d9a08bd3a14218bf0dbdbe865d83117190f AS dev +FROM ghcr.io/immich-app/base-server-dev:20240924@sha256:fff4358d435065a626c64a4c015cbfce6ee714b05fabe39aa0d83d8cff3951f2 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS web +FROM node:20.17.0-alpine3.20@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240903@sha256:d0d170ceeee7ef6c7b62b5d927820d74c14a9893f3e6285c1b9df45b33951b09 +FROM ghcr.io/immich-app/base-server-prod:20240924@sha256:af3089fe48d7ff162594bd7edfffa56ba4e7014ad10ad69c4ebfd428e39b06ff WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/package-lock.json b/server/package-lock.json index 51f038dfa0301..646a26b1ee9d0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.114.0", + "version": "1.116.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.114.0", + "version": "1.116.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", @@ -20,11 +20,11 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.49.0", + "@opentelemetry/auto-instrumentations-node": "^0.50.0", "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.53.0", "@opentelemetry/sdk-node": "^0.53.0", - "@react-email/components": "^0.0.24", + "@react-email/components": "^0.0.25", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -83,7 +83,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/react": "^18.3.4", @@ -733,9 +733,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -749,9 +749,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -765,9 +765,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -781,9 +781,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -797,9 +797,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -813,9 +813,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -829,9 +829,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -845,9 +845,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -861,9 +861,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -877,9 +877,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -893,9 +893,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -909,9 +909,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -925,9 +925,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -941,9 +941,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -957,9 +957,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -973,9 +973,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -989,9 +989,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -1005,9 +1005,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -1021,9 +1021,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -1037,9 +1037,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -1053,9 +1053,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -1069,9 +1069,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -1085,9 +1085,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -1198,9 +1198,9 @@ "dev": true }, "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, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1215,6 +1215,18 @@ "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, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -1973,9 +1985,9 @@ } }, "node_modules/@nestjs/cli": { - "version": "10.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", - "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", + "integrity": "sha512-FP7Rh13u8aJbHe+zZ7hM0CC4785g9Pw4lz4r2TTgRtf0zTxSWMkJaPEwyjX8SK9oWK2GsYxl+fKpwVZNbmnj9A==", "dev": true, "dependencies": { "@angular-devkit/core": "17.3.8", @@ -1995,7 +2007,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.93.0", + "webpack": "5.94.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2031,12 +2043,12 @@ } }, "node_modules/@nestjs/common": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", - "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.3.tgz", + "integrity": "sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==", "dependencies": { "iterare": "1.2.1", - "tslib": "2.6.3", + "tslib": "2.7.0", "uid": "2.0.2" }, "funding": { @@ -2058,6 +2070,11 @@ } } }, + "node_modules/@nestjs/common/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/config": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", @@ -2073,16 +2090,16 @@ } }, "node_modules/@nestjs/core": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", - "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", + "integrity": "sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.3", + "path-to-regexp": "3.3.0", + "tslib": "2.7.0", "uid": "2.0.2" }, "funding": { @@ -2109,6 +2126,11 @@ } } }, + "node_modules/@nestjs/core/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/event-emitter": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", @@ -2141,15 +2163,15 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", - "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.4.tgz", + "integrity": "sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==", "dependencies": { - "body-parser": "1.20.2", + "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.19.2", + "express": "4.21.0", "multer": "1.4.4-lts.1", - "tslib": "2.6.3" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -2160,13 +2182,18 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-express/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", - "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.3.tgz", + "integrity": "sha512-jTatT8q15LB5CFWsaIez3IigMixt7tNGJ4QLlRJ5NggPOPKRZssJnloODyEadFNHJjZiyufp5/NoPKBtNMf+lg==", "dependencies": { "socket.io": "4.7.5", - "tslib": "2.6.3" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -2178,10 +2205,15 @@ "rxjs": "^7.1.0" } }, + "node_modules/@nestjs/platform-socket.io/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@nestjs/schedule": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", - "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.1.tgz", + "integrity": "sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==", "dependencies": { "cron": "3.1.7", "uuid": "10.0.0" @@ -2226,15 +2258,15 @@ "dev": true }, "node_modules/@nestjs/swagger": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", - "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", "dependencies": { "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", - "path-to-regexp": "3.2.0", + "path-to-regexp": "3.3.0", "swagger-ui-dist": "5.17.14" }, "peerDependencies": { @@ -2258,12 +2290,12 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", - "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.3.tgz", + "integrity": "sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==", "dev": true, "dependencies": { - "tslib": "2.6.3" + "tslib": "2.7.0" }, "funding": { "type": "opencollective", @@ -2284,6 +2316,12 @@ } } }, + "node_modules/@nestjs/testing/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + }, "node_modules/@nestjs/typeorm": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", @@ -2300,13 +2338,13 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", - "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.3.tgz", + "integrity": "sha512-EW5/GR0jImJwrb8+YpHPoFN2tlhYQzVE2yAN5Se5sygUr/ZFMNAG84sd79NmWGd4RxoxR0aFH9nRycQ/0Ebe5w==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", - "tslib": "2.6.3" + "tslib": "2.7.0" }, "peerDependencies": { "@nestjs/common": "^10.0.0", @@ -2321,6 +2359,11 @@ } } }, + "node_modules/@nestjs/websockets/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -2524,9 +2567,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.0.tgz", - "integrity": "sha512-HxjD7xH9iAE4OyhNaaSec65i1H6QZYBWSwWkowFfsc5YAcDvJG30/J1sRKXEQqdmUcKTXEAnA66UciqZha/4+Q==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", "dependencies": { "@opentelemetry/api": "^1.0.0" }, @@ -2535,57 +2578,57 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.49.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz", - "integrity": "sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.50.0.tgz", + "integrity": "sha512-LqoSiWrOM4Cnr395frDHL4R/o5c2fuqqrqW8sZwhxvkasImmVlyL66YMPHllM2O5xVj2nP2ANUKHZSd293meZA==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/instrumentation-amqplib": "^0.41.0", - "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", - "@opentelemetry/instrumentation-bunyan": "^0.40.0", - "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", - "@opentelemetry/instrumentation-connect": "^0.38.0", - "@opentelemetry/instrumentation-cucumber": "^0.8.0", - "@opentelemetry/instrumentation-dataloader": "^0.11.0", - "@opentelemetry/instrumentation-dns": "^0.38.0", - "@opentelemetry/instrumentation-express": "^0.41.1", - "@opentelemetry/instrumentation-fastify": "^0.38.0", - "@opentelemetry/instrumentation-fs": "^0.14.0", - "@opentelemetry/instrumentation-generic-pool": "^0.38.1", - "@opentelemetry/instrumentation-graphql": "^0.42.0", - "@opentelemetry/instrumentation-grpc": "^0.52.0", - "@opentelemetry/instrumentation-hapi": "^0.40.0", - "@opentelemetry/instrumentation-http": "^0.52.0", - "@opentelemetry/instrumentation-ioredis": "^0.42.0", - "@opentelemetry/instrumentation-kafkajs": "^0.2.0", - "@opentelemetry/instrumentation-knex": "^0.39.0", - "@opentelemetry/instrumentation-koa": "^0.42.0", - "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", - "@opentelemetry/instrumentation-memcached": "^0.38.0", - "@opentelemetry/instrumentation-mongodb": "^0.46.0", - "@opentelemetry/instrumentation-mongoose": "^0.41.0", - "@opentelemetry/instrumentation-mysql": "^0.40.0", - "@opentelemetry/instrumentation-mysql2": "^0.40.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.39.0", - "@opentelemetry/instrumentation-net": "^0.38.0", - "@opentelemetry/instrumentation-pg": "^0.43.0", - "@opentelemetry/instrumentation-pino": "^0.41.0", - "@opentelemetry/instrumentation-redis": "^0.41.0", - "@opentelemetry/instrumentation-redis-4": "^0.41.1", - "@opentelemetry/instrumentation-restify": "^0.40.0", - "@opentelemetry/instrumentation-router": "^0.39.0", - "@opentelemetry/instrumentation-socket.io": "^0.41.0", - "@opentelemetry/instrumentation-tedious": "^0.13.0", - "@opentelemetry/instrumentation-undici": "^0.5.0", - "@opentelemetry/instrumentation-winston": "^0.39.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0", - "@opentelemetry/resource-detector-aws": "^1.6.0", - "@opentelemetry/resource-detector-azure": "^0.2.10", - "@opentelemetry/resource-detector-container": "^0.4.0", - "@opentelemetry/resource-detector-gcp": "^0.29.10", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/instrumentation-amqplib": "^0.42.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.44.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.44.0", + "@opentelemetry/instrumentation-bunyan": "^0.41.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.41.0", + "@opentelemetry/instrumentation-connect": "^0.39.0", + "@opentelemetry/instrumentation-cucumber": "^0.9.0", + "@opentelemetry/instrumentation-dataloader": "^0.12.0", + "@opentelemetry/instrumentation-dns": "^0.39.0", + "@opentelemetry/instrumentation-express": "^0.42.0", + "@opentelemetry/instrumentation-fastify": "^0.39.0", + "@opentelemetry/instrumentation-fs": "^0.15.0", + "@opentelemetry/instrumentation-generic-pool": "^0.39.0", + "@opentelemetry/instrumentation-graphql": "^0.43.0", + "@opentelemetry/instrumentation-grpc": "^0.53.0", + "@opentelemetry/instrumentation-hapi": "^0.41.0", + "@opentelemetry/instrumentation-http": "^0.53.0", + "@opentelemetry/instrumentation-ioredis": "^0.43.0", + "@opentelemetry/instrumentation-kafkajs": "^0.3.0", + "@opentelemetry/instrumentation-knex": "^0.40.0", + "@opentelemetry/instrumentation-koa": "^0.43.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.40.0", + "@opentelemetry/instrumentation-memcached": "^0.39.0", + "@opentelemetry/instrumentation-mongodb": "^0.47.0", + "@opentelemetry/instrumentation-mongoose": "^0.42.0", + "@opentelemetry/instrumentation-mysql": "^0.41.0", + "@opentelemetry/instrumentation-mysql2": "^0.41.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.40.0", + "@opentelemetry/instrumentation-net": "^0.39.0", + "@opentelemetry/instrumentation-pg": "^0.44.0", + "@opentelemetry/instrumentation-pino": "^0.42.0", + "@opentelemetry/instrumentation-redis": "^0.42.0", + "@opentelemetry/instrumentation-redis-4": "^0.42.0", + "@opentelemetry/instrumentation-restify": "^0.41.0", + "@opentelemetry/instrumentation-router": "^0.40.0", + "@opentelemetry/instrumentation-socket.io": "^0.42.0", + "@opentelemetry/instrumentation-tedious": "^0.14.0", + "@opentelemetry/instrumentation-undici": "^0.6.0", + "@opentelemetry/instrumentation-winston": "^0.40.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.1", + "@opentelemetry/resource-detector-aws": "^1.6.1", + "@opentelemetry/resource-detector-azure": "^0.2.11", + "@opentelemetry/resource-detector-container": "^0.4.1", + "@opentelemetry/resource-detector-gcp": "^0.29.11", "@opentelemetry/resources": "^1.24.0", - "@opentelemetry/sdk-node": "^0.52.0" + "@opentelemetry/sdk-node": "^0.53.0" }, "engines": { "node": ">=14" @@ -2594,95 +2637,6 @@ "@opentelemetry/api": "^1.4.1" } }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", - "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", - "dependencies": { - "@opentelemetry/api-logs": "0.52.1", - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/sdk-node": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", - "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", - "dependencies": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", - "@opentelemetry/exporter-trace-otlp-http": "0.52.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", - "@opentelemetry/exporter-zipkin": "1.25.1", - "@opentelemetry/instrumentation": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-logs": "0.52.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/sdk-trace-node": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/auto-instrumentations-node/node_modules/import-in-the-middle": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", - "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", - "dependencies": { - "acorn": "^8.8.2", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, "node_modules/@opentelemetry/context-async-hooks": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.26.0.tgz", @@ -2695,11 +2649,11 @@ } }, "node_modules/@opentelemetry/core": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.0.tgz", - "integrity": "sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.0" + "@opentelemetry/semantic-conventions": "1.27.0" }, "engines": { "node": ">=14" @@ -2726,31 +2680,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -2865,14 +2794,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-grpc/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-http": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.53.0.tgz", @@ -2891,31 +2812,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -3013,14 +2909,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-proto": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-proto/-/exporter-logs-otlp-proto-0.53.0.tgz", @@ -3041,31 +2929,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -3163,14 +3026,6 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-logs-otlp-proto/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/exporter-prometheus": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-prometheus/-/exporter-prometheus-0.53.0.tgz", @@ -3187,20 +3042,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/resources": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", @@ -3231,174 +3072,6 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/exporter-prometheus/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz", - "integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-grpc/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz", - "integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==", - "dependencies": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-http/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz", - "integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==", - "dependencies": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-trace-otlp-proto/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/exporter-zipkin": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz", - "integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==", - "dependencies": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/exporter-zipkin/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/host-metrics": { "version": "0.35.1", "resolved": "https://registry.npmjs.org/@opentelemetry/host-metrics/-/host-metrics-0.35.1.tgz", @@ -3415,13 +3088,13 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz", - "integrity": "sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", "dependencies": { - "@opentelemetry/api-logs": "0.52.0", - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "1.8.0", + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" @@ -3434,13 +3107,13 @@ } }, "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz", - "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.42.0.tgz", + "integrity": "sha512-fiuU6OKsqHJiydHWgTRQ7MnIrJ2lEqsdgFtNIH4LbAUJl/5XmrIeoDzDnox+hfkgWK65jsleFuQDtYb5hW1koQ==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3450,15 +3123,15 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-lambda": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.43.0.tgz", - "integrity": "sha512-pSxcWlsE/pCWQRIw92sV2C+LmKXelYkjkA7C5s39iPUi4pZ2lA1nIiw+1R/y2pDEhUHcaKkNyljQr3cx9ZpVlQ==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.44.0.tgz", + "integrity": "sha512-6vmr7FJIuopZzsQjEQTp4xWtF6kBp7DhD7pPIN8FN0dKUKyuVraABIpgWjMfelaUPy4rTYUGkYqPluhG0wx8Dw==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/propagator-aws-xray": "^1.3.1", "@opentelemetry/resources": "^1.8.0", - "@opentelemetry/semantic-conventions": "^1.22.0", - "@types/aws-lambda": "8.10.122" + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "8.10.143" }, "engines": { "node": ">=14" @@ -3468,14 +3141,14 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.43.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz", - "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.44.0.tgz", + "integrity": "sha512-HIWFg4TDQsayceiikOnruMmyQ0SZYW6WiR+wknWwWVLHC3lHTCpAnqzp5V42ckArOdlwHZu2Jvq2GMSM4Myx3w==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/propagation-utils": "^0.30.10", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/propagation-utils": "^0.30.11", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3485,12 +3158,12 @@ } }, "node_modules/@opentelemetry/instrumentation-bunyan": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.40.0.tgz", - "integrity": "sha512-aZ4cXaGWwj79ZXSYrgFVsrDlE4mmf2wfvP9bViwRc0j75A6eN6GaHYHqufFGMTCqASQn5pIjjP+Bx+PWTGiofw==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.41.0.tgz", + "integrity": "sha512-NoQS+gcwQ7pzb2PZFyra6bAxDAVXBMmpKxBblEuXJWirGrAksQllg9XTdmqhrwT/KxUYrbVca/lMams7e51ysg==", "dependencies": { - "@opentelemetry/api-logs": "^0.52.0", - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/instrumentation": "^0.53.0", "@types/bunyan": "1.8.9" }, "engines": { @@ -3501,12 +3174,12 @@ } }, "node_modules/@opentelemetry/instrumentation-cassandra-driver": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.40.0.tgz", - "integrity": "sha512-JxbM39JU7HxE9MTKKwi6y5Z3mokjZB2BjwfqYi4B3Y29YO3I42Z7eopG6qq06yWZc+nQli386UDQe0d9xKmw0A==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.41.0.tgz", + "integrity": "sha512-hvTNcC8qjCQEHZTLAlTmDptjsEGqCKpN+90hHH8Nn/GwilGr5TMSwGrlfstdJuZWyw8HAnRUed6bcjvmHHk2Xw==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3516,13 +3189,13 @@ } }, "node_modules/@opentelemetry/instrumentation-connect": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.38.0.tgz", - "integrity": "sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.39.0.tgz", + "integrity": "sha512-pGBiKevLq7NNglMgqzmeKczF4XQMTOUOTkK8afRHMZMnrK3fcETyTH7lVaSozwiOM3Ws+SuEmXZT7DYrrhxGlg==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.36" }, "engines": { @@ -3533,12 +3206,12 @@ } }, "node_modules/@opentelemetry/instrumentation-cucumber": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.8.0.tgz", - "integrity": "sha512-ieTm4RBIlZt2brPwtX5aEZYtYnkyqhAVXJI9RIohiBVMe5DxiwCwt+2Exep/nDVqGPX8zRBZUl4AEw423OxJig==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.9.0.tgz", + "integrity": "sha512-4PQNFnIqnA2WM3ZHpr0xhZpHSqJ5xJ6ppTIzZC7wPqe+ZBpj41vG8B6ieqiPfq+im4QdqbYnzLb3rj48GDEN9g==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3548,11 +3221,11 @@ } }, "node_modules/@opentelemetry/instrumentation-dataloader": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.11.0.tgz", - "integrity": "sha512-27urJmwkH4KDaMJtEv1uy2S7Apk4XbN4AgWMdfMJbi7DnOduJmeuA+DpJCwXB72tEWXo89z5T3hUVJIDiSNmNw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.12.0.tgz", + "integrity": "sha512-pnPxatoFE0OXIZDQhL2okF//dmbiWFzcSc8pUg9TqofCLYZySSxDCgQc69CJBo5JnI3Gz1KP+mOjS4WAeRIH4g==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -3562,11 +3235,11 @@ } }, "node_modules/@opentelemetry/instrumentation-dns": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.38.0.tgz", - "integrity": "sha512-Um07I0TQXDWa+ZbEAKDFUxFH40dLtejtExDOMLNJ1CL8VmOmA71qx93Qi/QG4tGkiI1XWqr7gF/oiMCJ4m8buQ==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.39.0.tgz", + "integrity": "sha512-+iPzvXqVdJa67QBuz2tuP0UI3LS1/cMMo6dS7360DDtOQX+sQzkiN+mo3Omn4T6ZRhkTDw6c7uwsHBcmL31+1g==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "semver": "^7.5.4" }, "engines": { @@ -3577,13 +3250,13 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz", - "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.42.0.tgz", + "integrity": "sha512-YNcy7ZfGnLsVEqGXQPT+S0G1AE46N21ORY7i7yUQyfhGAL4RBjnZUqefMI0NwqIl6nGbr1IpF0rZGoN8Q7x12Q==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3593,13 +3266,13 @@ } }, "node_modules/@opentelemetry/instrumentation-fastify": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.38.0.tgz", - "integrity": "sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.39.0.tgz", + "integrity": "sha512-SS9uSlKcsWZabhBp2szErkeuuBDgxOUlllwkS92dVaWRnMmwysPhcEgHKB8rUe3BHg/GnZC1eo1hbTZv4YhfoA==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3609,12 +3282,12 @@ } }, "node_modules/@opentelemetry/instrumentation-fs": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.14.0.tgz", - "integrity": "sha512-pVc8P5AgliC1DphyyBUgsxXlm2XaPH4BpYvt7rAZDMIqUpRk8gs19SioABtKqqxvFzg5jPtgJfJsdxq0Y+maLw==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.15.0.tgz", + "integrity": "sha512-JWVKdNLpu1skqZQA//jKOcKdJC66TWKqa2FUFq70rKohvaSq47pmXlnabNO+B/BvLfmidfiaN35XakT5RyMl2Q==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -3624,11 +3297,11 @@ } }, "node_modules/@opentelemetry/instrumentation-generic-pool": { - "version": "0.38.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.1.tgz", - "integrity": "sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz", + "integrity": "sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -3638,11 +3311,11 @@ } }, "node_modules/@opentelemetry/instrumentation-graphql": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.42.0.tgz", - "integrity": "sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.43.0.tgz", + "integrity": "sha512-aI3YMmC2McGd8KW5du1a2gBA0iOMOGLqg4s9YjzwbjFwjlmMNFSK1P3AIg374GWg823RPUGfVTIgZ/juk9CVOA==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -3652,12 +3325,12 @@ } }, "node_modules/@opentelemetry/instrumentation-grpc": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.52.0.tgz", - "integrity": "sha512-YYhA2pbhMWgF5Hp6eR7AHp1utzZQ3Y0VB8GIwd8zJoLtAuQRZa1N29DUtZ+t/pGRJF+xGPVI+vP+7ugHgeN0zQ==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.53.0.tgz", + "integrity": "sha512-Ss338T92yE1UCgr9zXSY3cPuaAy27uQw+wAC5IwsQKCXL5wwkiOgkd+2Ngksa9EGsgUEMwGeHi76bDdHFJ5Rrw==", "dependencies": { - "@opentelemetry/instrumentation": "0.52.0", - "@opentelemetry/semantic-conventions": "1.25.0" + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/semantic-conventions": "1.27.0" }, "engines": { "node": ">=14" @@ -3667,13 +3340,13 @@ } }, "node_modules/@opentelemetry/instrumentation-hapi": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.40.0.tgz", - "integrity": "sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz", + "integrity": "sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3683,13 +3356,13 @@ } }, "node_modules/@opentelemetry/instrumentation-http": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.0.tgz", - "integrity": "sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz", + "integrity": "sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ==", "dependencies": { - "@opentelemetry/core": "1.25.0", - "@opentelemetry/instrumentation": "0.52.0", - "@opentelemetry/semantic-conventions": "1.25.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/semantic-conventions": "1.27.0", "semver": "^7.5.2" }, "engines": { @@ -3700,13 +3373,13 @@ } }, "node_modules/@opentelemetry/instrumentation-ioredis": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz", - "integrity": "sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz", + "integrity": "sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.23.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3716,12 +3389,12 @@ } }, "node_modules/@opentelemetry/instrumentation-kafkajs": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz", - "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz", + "integrity": "sha512-UnkZueYK1ise8FXQeKlpBd7YYUtC7mM8J0wzUSccEfc/G8UqHQqAzIyYCUOUPUKp8GsjLnWOOK/3hJc4owb7Jg==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.24.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3731,12 +3404,12 @@ } }, "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz", - "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.40.0.tgz", + "integrity": "sha512-6jka2jfX8+fqjEbCn6hKWHVWe++mHrIkLQtaJqUkBt3ZBs2xn1+y0khxiDS0v/mNb0bIKDJWwtpKFfsQDM1Geg==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3746,13 +3419,13 @@ } }, "node_modules/@opentelemetry/instrumentation-koa": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.42.0.tgz", - "integrity": "sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz", + "integrity": "sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3762,11 +3435,11 @@ } }, "node_modules/@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.39.0.tgz", - "integrity": "sha512-eU1Wx1RRTR/2wYXFzH9gcpB8EPmhYlNDIUHzUXjyUE0CAXEJhBLkYNlzdaVCoQDw2neDqS+Woshiia6+emWK9A==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.40.0.tgz", + "integrity": "sha512-21xRwZsEdMPnROu/QsaOIODmzw59IYpGFmuC4aFWvMj6stA8+Ei1tX67nkarJttlNjoM94um0N4X26AD7ff54A==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -3776,12 +3449,12 @@ } }, "node_modules/@opentelemetry/instrumentation-memcached": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.38.0.tgz", - "integrity": "sha512-tPmyqQEZNyrvg6G+iItdlguQEcGzfE+bJkpQifmBXmWBnoS5oU3UxqtyYuXGL2zI9qQM5yMBHH4nRXWALzy7WA==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.39.0.tgz", + "integrity": "sha512-WfwvKAZ9I1qILRP5EUd88HQjwAAL+trXpCpozjBi4U6a0A07gB3fZ5PFAxbXemSjF5tHk9KVoROnqHvQ+zzFSQ==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.23.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/memcached": "^2.2.6" }, "engines": { @@ -3792,13 +3465,13 @@ } }, "node_modules/@opentelemetry/instrumentation-mongodb": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.46.0.tgz", - "integrity": "sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz", + "integrity": "sha512-yqyXRx2SulEURjgOQyJzhCECSh5i1uM49NUaq9TqLd6fA7g26OahyJfsr9NE38HFqGRHpi4loyrnfYGdrsoVjQ==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/sdk-metrics": "^1.9.1", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3808,13 +3481,13 @@ } }, "node_modules/@opentelemetry/instrumentation-mongoose": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz", - "integrity": "sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz", + "integrity": "sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3824,13 +3497,13 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.40.0.tgz", - "integrity": "sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz", + "integrity": "sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", - "@types/mysql": "2.15.22" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" }, "engines": { "node": ">=14" @@ -3840,12 +3513,12 @@ } }, "node_modules/@opentelemetry/instrumentation-mysql2": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.40.0.tgz", - "integrity": "sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz", + "integrity": "sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1" }, "engines": { @@ -3856,12 +3529,12 @@ } }, "node_modules/@opentelemetry/instrumentation-nestjs-core": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.39.0.tgz", - "integrity": "sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz", + "integrity": "sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.23.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3871,12 +3544,12 @@ } }, "node_modules/@opentelemetry/instrumentation-net": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.38.0.tgz", - "integrity": "sha512-stjow1PijcmUquSmRD/fSihm/H61DbjPlJuJhWUe7P22LFPjFhsrSeiB5vGj3vn+QGceNAs+kioUTzMGPbNxtg==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.39.0.tgz", + "integrity": "sha512-rixHoODfI/Cx1B0mH1BpxCT0bRSxktuBDrt9IvpT2KSEutK5hR0RsRdgdz/GKk+BQ4u+IG6godgMSGwNQCueEA==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.23.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3886,15 +3559,15 @@ } }, "node_modules/@opentelemetry/instrumentation-pg": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.43.0.tgz", - "integrity": "sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz", + "integrity": "sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1", "@types/pg": "8.6.1", - "@types/pg-pool": "2.0.4" + "@types/pg-pool": "2.0.6" }, "engines": { "node": ">=14" @@ -3914,13 +3587,13 @@ } }, "node_modules/@opentelemetry/instrumentation-pino": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.41.0.tgz", - "integrity": "sha512-Kpv0fJRk/8iMzMk5Ue5BsUJfHkBJ2wQoIi/qduU1a1Wjx9GLj6J2G17PHjPK5mnZjPNzkFOXFADZMfgDioliQw==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.42.0.tgz", + "integrity": "sha512-SoX6FzucBfTuFNMZjdurJhcYWq2ve8/LkhmyVLUW31HpIB45RF1JNum0u4MkGisosDmXlK4njomcgUovShI+WA==", "dependencies": { - "@opentelemetry/api-logs": "^0.52.0", + "@opentelemetry/api-logs": "^0.53.0", "@opentelemetry/core": "^1.25.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -3930,13 +3603,13 @@ } }, "node_modules/@opentelemetry/instrumentation-redis": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.41.0.tgz", - "integrity": "sha512-RJ1pwI3btykp67ts+5qZbaFSAAzacucwBet5/5EsKYtWBpHbWwV/qbGN/kIBzXg5WEZBhXLrR/RUq0EpEUpL3A==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.42.0.tgz", + "integrity": "sha512-jZBoqve0rEC51q0HuhjtZVq1DtUvJHzEJ3YKGvzGar2MU1J4Yt5+pQAQYh1W4jSoDyKeaI4hyeUdWM5N0c2lqA==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3946,13 +3619,13 @@ } }, "node_modules/@opentelemetry/instrumentation-redis-4": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.1.tgz", - "integrity": "sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz", + "integrity": "sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3962,13 +3635,13 @@ } }, "node_modules/@opentelemetry/instrumentation-restify": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.40.0.tgz", - "integrity": "sha512-sm/rH/GysY/KOEvZqYBZSLYFeXlBkHCgqPDgWc07tz+bHCN6mPs9P3otGOSTe7o3KAIM8Nc6ncCO59vL+jb2cA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.41.0.tgz", + "integrity": "sha512-gKEo+X/wVKUBuD2WDDlF7SlDNBHMWjSQoLxFCsGqeKgHR0MGtwMel8uaDGg9LJ83nKqYy+7Vl/cDFxjba6H+/w==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3978,12 +3651,12 @@ } }, "node_modules/@opentelemetry/instrumentation-router": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.39.0.tgz", - "integrity": "sha512-LaXnVmD69WPC4hNeLzKexCCS19hRLrUw3xicneAMkzJSzNJvPyk7G6I7lz7VjQh1cooObPBt9gNyd3hhTCUrag==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.40.0.tgz", + "integrity": "sha512-bRo4RaclGFiKtmv/N1D0MuzO7DuxbeqMkMCbPPng6mDwzpHAMpHz/K/IxJmF+H1Hi/NYXVjCKvHGClageLe9eA==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -3993,12 +3666,12 @@ } }, "node_modules/@opentelemetry/instrumentation-socket.io": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.41.0.tgz", - "integrity": "sha512-7fzDe9/FpO6NFizC/wnzXXX7bF9oRchsD//wFqy5g5hVEgXZCQ70IhxjrKdBvgjyIejR9T9zTvfQ6PfVKfkCAw==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.42.0.tgz", + "integrity": "sha512-xB5tdsBzuZyicQTO3hDzJIpHQ7V1BYJ6vWPWgl19gWZDBdjEGc3HOupjkd3BUJyDoDhbMEHGk2nNlkUU99EfkA==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -4008,12 +3681,12 @@ } }, "node_modules/@opentelemetry/instrumentation-tedious": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.13.0.tgz", - "integrity": "sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.14.0.tgz", + "integrity": "sha512-ofq7pPhSqvRDvD2FVx3RIWPj76wj4QubfrbqJtEx0A+fWoaYxJOCIQ92tYJh28elAmjMmgF/XaYuJuBhBv5J3A==", "dependencies": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" }, "engines": { @@ -4024,12 +3697,12 @@ } }, "node_modules/@opentelemetry/instrumentation-undici": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz", - "integrity": "sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz", + "integrity": "sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ==", "dependencies": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -4039,12 +3712,12 @@ } }, "node_modules/@opentelemetry/instrumentation-winston": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.39.0.tgz", - "integrity": "sha512-v/1xziLJ9CyB3YDjBSBzbB70Qd0JwWTo36EqWK5m3AR0CzsyMQQmf3ZIZM6sgx7hXMcRQ0pnEYhg6nhrUQPm9A==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.40.0.tgz", + "integrity": "sha512-eMk2tKl86YJ8/yHvtDbyhrE35/R0InhO9zuHTflPx8T0+IvKVUhPV71MsJr32sImftqeOww92QHt4Jd+a5db4g==", "dependencies": { - "@opentelemetry/api-logs": "^0.52.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/instrumentation": "^0.53.0" }, "engines": { "node": ">=14" @@ -4053,139 +3726,10 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-exporter-base": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz", - "integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==", - "dependencies": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-transformer": "0.52.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz", - "integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==", - "dependencies": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.0.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-grpc-exporter-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/otlp-transformer": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz", - "integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==", - "dependencies": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-logs": "0.52.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "protobufjs": "^7.3.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/propagation-utils": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.10.tgz", - "integrity": "sha512-hhTW8pFp9PSyosYzzuUL9rdm7HF97w3OCyElufFHyUnYnKkCBbu8ne2LyF/KSdI/xZ81ubxWZs78hX4S7pLq5g==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.11.tgz", + "integrity": "sha512-rY4L/2LWNk5p/22zdunpqVmgz6uN419DsRTw5KFMa6u21tWhXS8devlMy4h8m8nnS20wM7r6yYweCNNKjgLYJw==", "engines": { "node": ">=14" }, @@ -4207,78 +3751,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/propagator-b3": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz", - "integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==", - "dependencies": { - "@opentelemetry/core": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-b3/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/propagator-jaeger": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz", - "integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==", - "dependencies": { - "@opentelemetry/core": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/propagator-jaeger/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/redis-common": { "version": "0.36.2", "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", @@ -4288,12 +3760,12 @@ } }, "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz", - "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==", + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.1.tgz", + "integrity": "sha512-Qshebw6azBuKUqGkVgambZlLS6Xh+LCoLXep1oqW1RSzSOHQxGYDsPs99v8NzO65eJHHOu8wc2yKsWZQAgYsSw==", "dependencies": { "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" }, "engines": { "node": ">=14" @@ -4318,14 +3790,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/resource-detector-aws/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/resource-detector-azure": { "version": "0.2.11", "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-azure/-/resource-detector-azure-0.2.11.tgz", @@ -4342,28 +3806,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/resource-detector-azure/node_modules/@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/resource-detector-azure/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/resource-detector-container": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-container/-/resource-detector-container-0.4.1.tgz", @@ -4379,22 +3821,14 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/resource-detector-container/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/resource-detector-gcp": { - "version": "0.29.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.10.tgz", - "integrity": "sha512-rm2HKJ9lsdoVvrbmkr9dkOzg3Uk0FksXNxvNBgrCprM1XhMoJwThI5i0h/5sJypISUAJlEeJS6gn6nROj/NpkQ==", + "version": "0.29.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.11.tgz", + "integrity": "sha512-07wJx4nyxD/c2z3n70OQOg8fmoO/baTsq8uU+f7tZaehRNQx76MPkRbV2L902N40Z21SPIG8biUZ30OXE9tOIg==", "dependencies": { "@opentelemetry/core": "^1.0.0", "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "gcp-metadata": "^6.0.0" }, "engines": { @@ -4441,55 +3875,6 @@ "node": ">=14" } }, - "node_modules/@opentelemetry/sdk-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz", - "integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==", - "dependencies": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.4.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/sdk-metrics": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", @@ -4557,31 +3942,6 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "dependencies": { - "@opentelemetry/api": "^1.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.27.0" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/exporter-trace-otlp-grpc": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz", @@ -4654,25 +4014,6 @@ "@opentelemetry/api": "^1.0.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/instrumentation": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", - "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", - "dependencies": { - "@opentelemetry/api-logs": "0.53.0", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": "^1.3.0" - } - }, "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -4834,7 +4175,7 @@ "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/@opentelemetry/semantic-conventions": { + "node_modules/@opentelemetry/semantic-conventions": { "version": "1.27.0", "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==", @@ -4842,115 +4183,6 @@ "node": ">=14" } }, - "node_modules/@opentelemetry/sdk-node/node_modules/import-in-the-middle": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", - "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", - "dependencies": { - "acorn": "^8.8.2", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - }, - "node_modules/@opentelemetry/sdk-trace-base": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz", - "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==", - "dependencies": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/sdk-trace-node": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz", - "integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==", - "dependencies": { - "@opentelemetry/context-async-hooks": "1.25.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/propagator-b3": "1.25.1", - "@opentelemetry/propagator-jaeger": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "semver": "^7.5.2" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/context-async-hooks": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", - "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "dependencies": { - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "engines": { - "node": ">=14" - }, - "peerDependencies": { - "@opentelemetry/api": ">=1.0.0 <1.10.0" - } - }, - "node_modules/@opentelemetry/sdk-trace-node/node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==", - "engines": { - "node": ">=14" - } - }, - "node_modules/@opentelemetry/semantic-conventions": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz", - "integrity": "sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==", - "engines": { - "node": ">=14" - } - }, "node_modules/@opentelemetry/sql-common": { "version": "0.40.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sql-common/-/sql-common-0.40.1.tgz", @@ -5070,9 +4302,9 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz", - "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.9.tgz", + "integrity": "sha512-Zrhc71VYrSC1fVXJuaViKoB/dBjxLw6nbE53Bm/eUuZPdnnZ1+ZUIh8jfaRKC5MzMjgnLGQTweGXVnfIrhyxtQ==", "dependencies": { "prismjs": "1.29.0" }, @@ -5106,13 +4338,13 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz", - "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==", + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.25.tgz", + "integrity": "sha512-lnfVVrThEcET5NPoeaXvrz9UxtWpGRcut2a07dLbyKgNbP7vj/cXTI5TuHtanCvhCddFpMDnElNRghDOfPzwUg==", "dependencies": { "@react-email/body": "0.0.10", "@react-email/button": "0.0.17", - "@react-email/code-block": "0.0.8", + "@react-email/code-block": "0.0.9", "@react-email/code-inline": "0.0.4", "@react-email/column": "0.0.12", "@react-email/container": "0.0.14", @@ -5350,9 +4582,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", - "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "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" ], @@ -5363,9 +4595,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", - "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "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" ], @@ -5376,9 +4608,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", - "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "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" ], @@ -5389,9 +4621,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", - "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "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" ], @@ -5402,9 +4634,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", - "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "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" ], @@ -5415,9 +4647,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", - "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "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" ], @@ -5428,9 +4660,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", - "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "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" ], @@ -5441,9 +4673,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", - "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "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" ], @@ -5454,9 +4686,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", - "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "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" ], @@ -5467,9 +4699,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", - "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "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" ], @@ -5480,9 +4712,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", - "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "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" ], @@ -5493,9 +4725,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", - "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "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" ], @@ -5506,9 +4738,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", - "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "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" ], @@ -5519,9 +4751,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", - "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "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" ], @@ -5532,9 +4764,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", - "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "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" ], @@ -5545,9 +4777,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", - "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "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" ], @@ -5614,9 +4846,9 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.21.tgz", - "integrity": "sha512-7/cN0SZ+y2V6e0hsDD8koGR0QVh7Jl3r756bwaHLLSN+kReoUb/yVcLsA8iTn90JLME3DkQK4CPjxDCQiyMXNg==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", "devOptional": true, "hasInstallScript": true, "dependencies": { @@ -5631,16 +4863,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.21", - "@swc/core-darwin-x64": "1.7.21", - "@swc/core-linux-arm-gnueabihf": "1.7.21", - "@swc/core-linux-arm64-gnu": "1.7.21", - "@swc/core-linux-arm64-musl": "1.7.21", - "@swc/core-linux-x64-gnu": "1.7.21", - "@swc/core-linux-x64-musl": "1.7.21", - "@swc/core-win32-arm64-msvc": "1.7.21", - "@swc/core-win32-ia32-msvc": "1.7.21", - "@swc/core-win32-x64-msvc": "1.7.21" + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26" }, "peerDependencies": { "@swc/helpers": "*" @@ -5652,9 +4884,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.21.tgz", - "integrity": "sha512-hh5uOZ7jWF66z2TRMhhXtWMQkssuPCSIZPy9VHf5KvZ46cX+5UeECDthchYklEVZQyy4Qr6oxfh4qff/5spoMA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", "cpu": [ "arm64" ], @@ -5668,9 +4900,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.21.tgz", - "integrity": "sha512-lTsPquqSierQ6jWiWM7NnYXXZGk9zx3NGkPLHjPbcH5BmyiauX0CC/YJYJx7YmS2InRLyALlGmidHkaF4JY28A==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", "cpu": [ "x64" ], @@ -5684,9 +4916,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.21.tgz", - "integrity": "sha512-AgSd0fnSzAqCvWpzzZCq75z62JVGUkkXEOpfdi99jj/tryPy38KdXJtkVWJmufPXlRHokGTBitalk33WDJwsbA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", "cpu": [ "arm" ], @@ -5700,9 +4932,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.21.tgz", - "integrity": "sha512-l+jw6RQ4Y43/8dIst0c73uQE+W3kCWrCFqMqC/xIuE/iqHOnvYK6YbA1ffOct2dImkHzNiKuoehGqtQAc6cNaQ==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", "cpu": [ "arm64" ], @@ -5716,9 +4948,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.21.tgz", - "integrity": "sha512-29KKZXrTo/c9F1JFL9WsNvCa6UCdIVhHP5EfuYhlKbn5/YmSsNFkuHdUtZFEd5U4+jiShXDmgGCtLW2d08LIwg==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", "cpu": [ "arm64" ], @@ -5732,9 +4964,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.21.tgz", - "integrity": "sha512-HsP3JwddvQj5HvnjmOr+Bd5plEm6ccpfP5wUlm3hywzvdVkj+yR29bmD7UwpV/1zCQ60Ry35a7mXhKI6HQxFgw==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", "cpu": [ "x64" ], @@ -5748,9 +4980,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.21.tgz", - "integrity": "sha512-hYKLVeUTHqvFK628DFJEwxoX6p42T3HaQ4QjNtf3oKhiJWFh9iTRUrN/oCB5YI3R9WMkFkKh+99gZ/Dd0T5lsg==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", "cpu": [ "x64" ], @@ -5764,9 +4996,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.21.tgz", - "integrity": "sha512-qyWAKW10aMBe6iUqeZ7NAJIswjfggVTUpDINpQGUJhz+pR71YZDidXgZXpaDB84YyDB2JAlRqd1YrLkl7CMiIw==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", "cpu": [ "arm64" ], @@ -5780,9 +5012,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.21.tgz", - "integrity": "sha512-cy61wS3wgH5mEwBiQ5w6/FnQrchBDAdPsSh0dKSzNmI+4K8hDxS8uzdBycWqJXO0cc+mA77SIlwZC3hP3Kum2g==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", "cpu": [ "ia32" ], @@ -5796,9 +5028,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.21.tgz", - "integrity": "sha512-/rexGItJURNJOdae+a48M+loT74nsEU+PyRRVAkZMKNRtLoYFAr0cpDlS5FodIgGunp/nqM0bst4H2w6Y05IKA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", "cpu": [ "x64" ], @@ -5835,12 +5067,12 @@ } }, "node_modules/@testcontainers/postgresql": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz", - "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==", + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.13.1.tgz", + "integrity": "sha512-HAh/3uLAzAhOmzXsOE6hVxkvetczPnX/Zoyt+SgK7QotW98Npr1MDx8OKiaLGTJ8XkIvVvS4Ch6bl+frt4pnkQ==", "dev": true, "dependencies": { - "testcontainers": "^10.12.0" + "testcontainers": "^10.13.1" } }, "node_modules/@tsconfig/node10": { @@ -5872,31 +5104,40 @@ "peer": true }, "node_modules/@turf/boolean-point-in-polygon": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", - "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.1.0.tgz", + "integrity": "sha512-mprVsyIQ+ijWTZwbnO4Jhxu94ZW2M2CheqLiRTsGJy0Ooay9v6Av5/Nl3/Gst7ZVXxPqMeMaFYkSzcTc87AKew==", "dependencies": { - "@turf/helpers": "^6.5.0", - "@turf/invariant": "^6.5.0" + "@turf/helpers": "^7.1.0", + "@turf/invariant": "^7.1.0", + "@types/geojson": "^7946.0.10", + "point-in-polygon-hao": "^1.1.0", + "tslib": "^2.6.2" }, "funding": { "url": "https://opencollective.com/turf" } }, "node_modules/@turf/helpers": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", - "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.1.0.tgz", + "integrity": "sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.6.2" + }, "funding": { "url": "https://opencollective.com/turf" } }, "node_modules/@turf/invariant": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", - "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.1.0.tgz", + "integrity": "sha512-OCLNqkItBYIP1nE9lJGuIUatWGtQ4rhBKAyTfFu0z8npVzGEYzvguEeof8/6LkKmTTEHW53tCjoEhSSzdRh08Q==", "dependencies": { - "@turf/helpers": "^6.5.0" + "@turf/helpers": "^7.1.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.6.2" }, "funding": { "url": "https://opencollective.com/turf" @@ -5918,9 +5159,9 @@ "dev": true }, "node_modules/@types/aws-lambda": { - "version": "8.10.122", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.122.tgz", - "integrity": "sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw==" + "version": "8.10.143", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.143.tgz", + "integrity": "sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==" }, "node_modules/@types/bcrypt": { "version": "5.0.2", @@ -6011,21 +5252,13 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", "dev": true, + "optional": true, + "peer": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, - "node_modules/@types/eslint-scope": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", - "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", - "dev": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6065,6 +5298,11 @@ "@types/node": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, "node_modules/@types/http-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", @@ -6143,25 +5381,25 @@ } }, "node_modules/@types/mysql": { - "version": "2.15.22", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.22.tgz", - "integrity": "sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==", + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "20.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", - "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "dependencies": { "undici-types": "~6.19.2" } }, "node_modules/@types/nodemailer": { - "version": "6.4.15", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", - "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", "dev": true, "dependencies": { "@types/node": "*" @@ -6174,9 +5412,9 @@ "dev": true }, "node_modules/@types/pg": { - "version": "8.10.9", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", - "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==", + "version": "8.11.10", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", + "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -6184,23 +5422,23 @@ } }, "node_modules/@types/pg-pool": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.4.tgz", - "integrity": "sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", "dependencies": { "@types/pg": "*" } }, "node_modules/@types/pg/node_modules/pg-types": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", - "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", - "postgres-date": "~2.0.1", + "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" }, @@ -6228,9 +5466,9 @@ } }, "node_modules/@types/pg/node_modules/postgres-date": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", - "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==", "engines": { "node": ">=12" } @@ -6268,9 +5506,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", - "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "version": "18.3.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", + "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -6387,16 +5625,16 @@ "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "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, "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", @@ -6420,15 +5658,15 @@ } }, "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, "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": { @@ -6448,13 +5686,13 @@ } }, "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, "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" @@ -6465,13 +5703,13 @@ } }, "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, "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" }, @@ -6489,9 +5727,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, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6502,13 +5740,13 @@ } }, "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, "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", @@ -6554,15 +5792,15 @@ } }, "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, "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" @@ -6576,12 +5814,12 @@ } }, "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, "dependencies": { - "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -6593,19 +5831,19 @@ } }, "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, "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", @@ -6615,7 +5853,13 @@ "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/coverage-v8/node_modules/magic-string": { @@ -6628,13 +5872,13 @@ } }, "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, "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" }, @@ -6642,10 +5886,46 @@ "url": "https://opencollective.com/vitest" } }, + "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, + "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/mocker/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "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==", + "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, "dependencies": { "tinyrainbow": "^1.2.0" @@ -6655,12 +5935,12 @@ } }, "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, "dependencies": { - "@vitest/utils": "2.0.5", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" }, "funding": { @@ -6668,13 +5948,13 @@ } }, "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, "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": { @@ -6691,9 +5971,9 @@ } }, "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, "dependencies": { "tinyspy": "^3.0.0" @@ -6703,13 +5983,12 @@ } }, "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, "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" }, @@ -7417,9 +6696,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -7429,7 +6708,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -7798,9 +7077,9 @@ } }, "node_modules/cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==" }, "node_modules/class-transformer": { "version": "0.5.1", @@ -8373,11 +7652,11 @@ } }, "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==", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -8723,9 +8002,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "engines": { "node": ">= 0.8" } @@ -8740,9 +8019,9 @@ } }, "node_modules/engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -8753,7 +8032,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" }, "engines": { "node": ">=10.2.0" @@ -8768,9 +8047,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -8825,9 +8104,9 @@ "dev": true }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -8837,29 +8116,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -8888,16 +8167,17 @@ } }, "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, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@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", @@ -8920,7 +8200,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", @@ -9217,68 +8496,6 @@ "node": ">=0.8.x" } }, - "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/execa/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/execa/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/execa/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/exiftool-vendored": { "version": "28.2.1", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", @@ -9314,36 +8531,36 @@ ] }, "node_modules/express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -9376,9 +8593,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" }, "node_modules/extend": { "version": "3.0.2", @@ -9509,12 +8726,12 @@ } }, "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "dependencies": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -9821,17 +9038,17 @@ } }, "node_modules/geo-tz": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.2.tgz", - "integrity": "sha512-NjEzJBzaMhO9C7lFZIsWDkVED7aLxcES3iEZOWJ97dhnDUGhEB8vhW7MaWR+2y4aWvtFV/VyuDi8Y0rUHvm4tw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.1.1.tgz", + "integrity": "sha512-V6FEJ9UQOHnBD7eAOkJG3gZlc0LjKckRjt56cit6MLKMPF2qQIUBDYb0iUj4kw8l+WUeAu9ytPdSPpcLEELWtw==", "dependencies": { - "@turf/boolean-point-in-polygon": "^6.5.0", - "@turf/helpers": "^6.5.0", + "@turf/boolean-point-in-polygon": "^7.1.0", + "@turf/helpers": "^7.1.0", "geobuf": "^3.0.2", "pbf": "^3.2.1" }, "engines": { - "node": ">=12" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/evansiroky" @@ -9911,18 +9128,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "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.2", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", @@ -10235,19 +9440,10 @@ "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/i18n-iso-countries": { - "version": "7.11.3", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz", - "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz", + "integrity": "sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw==", "dependencies": { "diacritics": "1.3.0" }, @@ -10310,9 +9506,9 @@ } }, "node_modules/import-in-the-middle": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz", - "integrity": "sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", "dependencies": { "acorn": "^8.8.2", "acorn-import-attributes": "^1.9.5", @@ -10651,9 +9847,9 @@ } }, "node_modules/jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -11092,9 +10288,12 @@ } }, "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/merge-stream": { "version": "2.0.0", @@ -11119,11 +10318,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -11289,9 +10488,9 @@ } }, "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==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/msgpackr": { "version": "1.10.1", @@ -11656,9 +10855,9 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "node_modules/nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==", + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==", "engines": { "node": ">=6.0.0" } @@ -11711,33 +10910,6 @@ "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==" }, - "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", @@ -11766,9 +10938,12 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11828,11 +11003,11 @@ } }, "node_modules/openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", "dependencies": { - "jose": "^4.15.5", + "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -12085,9 +11260,9 @@ } }, "node_modules/path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" }, "node_modules/path-type": { "version": "4.0.0", @@ -12133,13 +11308,13 @@ } }, "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==", "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" }, @@ -12165,9 +11340,9 @@ "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==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" }, "node_modules/pg-int8": { "version": "1.0.1", @@ -12186,17 +11361,17 @@ } }, "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==", "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==" + "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==" }, "node_modules/pg-types": { "version": "2.2.0", @@ -12222,9 +11397,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "4.0.2", @@ -12265,10 +11440,15 @@ "node": ">=4" } }, + "node_modules/point-in-polygon-hao": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.1.0.tgz", + "integrity": "sha512-3hTIM2j/v9Lio+wOyur3kckD4NxruZhpowUbEgmyikW+a2Kppjtu1eN+AhnMQtoHW46zld88JiYWv6fxpsDrTQ==" + }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -12285,8 +11465,8 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -12449,9 +11629,9 @@ } }, "node_modules/postgres-range": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", - "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==" }, "node_modules/prelude-ls": { "version": "1.2.1", @@ -12639,11 +11819,11 @@ } }, "node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "dependencies": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" }, "engines": { "node": ">=0.6" @@ -13695,9 +12875,9 @@ } }, "node_modules/rollup": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", - "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -13710,22 +12890,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.3", - "@rollup/rollup-android-arm64": "4.14.3", - "@rollup/rollup-darwin-arm64": "4.14.3", - "@rollup/rollup-darwin-x64": "4.14.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", - "@rollup/rollup-linux-arm-musleabihf": "4.14.3", - "@rollup/rollup-linux-arm64-gnu": "4.14.3", - "@rollup/rollup-linux-arm64-musl": "4.14.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", - "@rollup/rollup-linux-riscv64-gnu": "4.14.3", - "@rollup/rollup-linux-s390x-gnu": "4.14.3", - "@rollup/rollup-linux-x64-gnu": "4.14.3", - "@rollup/rollup-linux-x64-musl": "4.14.3", - "@rollup/rollup-win32-arm64-msvc": "4.14.3", - "@rollup/rollup-win32-ia32-msvc": "4.14.3", - "@rollup/rollup-win32-x64-msvc": "4.14.3", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "fsevents": "~2.3.2" } }, @@ -13880,9 +13060,9 @@ } }, "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dependencies": { "debug": "2.6.9", "depd": "2.0.0", @@ -13915,10 +13095,13 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } }, "node_modules/serialize-javascript": { "version": "6.0.2", @@ -13930,14 +13113,14 @@ } }, "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "dependencies": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" }, "engines": { "node": ">= 0.8.0" @@ -14066,13 +13249,17 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14152,26 +13339,6 @@ "ws": "~8.17.1" } }, - "node_modules/socket.io-adapter/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -14194,9 +13361,9 @@ } }, "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==", "engines": { "node": ">=0.10.0" } @@ -14267,9 +13434,9 @@ } }, "node_modules/sql-formatter": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.1.tgz", - "integrity": "sha512-lw/G/emIJ+tVspOtOFzfD2YFFMN3MFPxGnbWl1DlJLB+fsX7X7zMqSRM1SLSn2YuaRJ0lTe7AMknHDqmIW1Y8w==", + "version": "15.4.2", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.2.tgz", + "integrity": "sha512-Pw4aAgfuyml/SHMlhbJhyOv+GR+Z1HNb9sgX3CVBVdN5YNM+v2VWkYJ3NNbYS7cu37GY3vP/PgnwoVynCuXRxg==", "dev": true, "dependencies": { "argparse": "^2.0.1", @@ -14417,18 +13584,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", @@ -14670,9 +13825,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -14859,9 +14014,9 @@ } }, "node_modules/testcontainers": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.0.tgz", - "integrity": "sha512-SDblQvirbJw1ZpenxaAairGtAesw5XMOCHLbRhTTUBJtBkZJGce8Vx/I8lXQxWIM8HRXsg3HILTHGQvYo4x7wQ==", + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.1.tgz", + "integrity": "sha512-JBbOhxmygj/ouH/47GnoVNt+c55Telh/45IjVxEbDoswsLchVmJiuKiw/eF6lE5i7LN+/99xsrSCttI3YRtirg==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", @@ -14940,15 +14095,21 @@ "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==" }, "node_modules/tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", "dev": true }, "node_modules/tinypool": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", - "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" @@ -14964,9 +14125,9 @@ } }, "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, "engines": { "node": ">=14.0.0" @@ -15388,9 +14549,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==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -15401,9 +14562,9 @@ } }, "node_modules/ua-parser-js": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", - "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==", + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==", "funding": [ { "type": "opencollective", @@ -15418,6 +14579,9 @@ "url": "https://github.com/sponsors/faisalman" } ], + "bin": { + "ua-parser-js": "script/cli.js" + }, "engines": { "node": "*" } @@ -15638,14 +14802,14 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -15664,6 +14828,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -15681,6 +14846,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -15693,15 +14861,14 @@ } }, "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, "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": { @@ -15734,29 +14901,29 @@ } }, "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, "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": { @@ -15771,8 +14938,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": "*" }, @@ -15833,12 +15000,11 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "dependencies": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -15847,7 +15013,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -16013,15 +15179,15 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "engines": { "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" + "utf-8-validate": ">=5.0.2" }, "peerDependenciesMeta": { "bufferutil": { @@ -16625,163 +15791,163 @@ } }, "@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "dev": true, "optional": true }, @@ -16855,9 +16021,9 @@ } }, "@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 }, "@eslint/object-schema": { @@ -16866,6 +16032,15 @@ "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true }, + "@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, + "requires": { + "levn": "^0.4.1" + } + }, "@fastify/busboy": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", @@ -17302,9 +16477,9 @@ } }, "@nestjs/cli": { - "version": "10.4.4", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", - "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.5.tgz", + "integrity": "sha512-FP7Rh13u8aJbHe+zZ7hM0CC4785g9Pw4lz4r2TTgRtf0zTxSWMkJaPEwyjX8SK9oWK2GsYxl+fKpwVZNbmnj9A==", "dev": true, "requires": { "@angular-devkit/core": "17.3.8", @@ -17324,7 +16499,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.93.0", + "webpack": "5.94.0", "webpack-node-externals": "3.0.0" }, "dependencies": { @@ -17337,13 +16512,20 @@ } }, "@nestjs/common": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", - "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.3.tgz", + "integrity": "sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==", "requires": { "iterare": "1.2.1", - "tslib": "2.6.3", + "tslib": "2.7.0", "uid": "2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/config": { @@ -17357,16 +16539,23 @@ } }, "@nestjs/core": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", - "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.4.tgz", + "integrity": "sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "path-to-regexp": "3.2.0", - "tslib": "2.6.3", + "path-to-regexp": "3.3.0", + "tslib": "2.7.0", "uid": "2.0.2" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/event-emitter": { @@ -17384,30 +16573,44 @@ "requires": {} }, "@nestjs/platform-express": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", - "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.4.tgz", + "integrity": "sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==", "requires": { - "body-parser": "1.20.2", + "body-parser": "1.20.3", "cors": "2.8.5", - "express": "4.19.2", + "express": "4.21.0", "multer": "1.4.4-lts.1", - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/platform-socket.io": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", - "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.3.tgz", + "integrity": "sha512-jTatT8q15LB5CFWsaIez3IigMixt7tNGJ4QLlRJ5NggPOPKRZssJnloODyEadFNHJjZiyufp5/NoPKBtNMf+lg==", "requires": { "socket.io": "4.7.5", - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@nestjs/schedule": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", - "integrity": "sha512-WEc96WTXZW+VI/Ng+uBpiBUwm6TWtAbQ4RKWkfbmzKvmbRGzA/9k/UyAWDS9k0pp+ZcbC+MaZQtt7TjQHrwX6g==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.1.tgz", + "integrity": "sha512-VxAnCiU4HP0wWw8IdWAVfsGC/FGjyToNjjUtXDEQL6oj+w/N5QDd2VT9k6d7Jbr8PlZuBZNdWtDKSkH5bZ+RXQ==", "requires": { "cron": "3.1.7", "uuid": "10.0.0" @@ -17442,25 +16645,33 @@ } }, "@nestjs/swagger": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.0.tgz", - "integrity": "sha512-dCiwKkRxcR7dZs5jtrGspBAe/nqJd1AYzOBTzw9iCdbq3BGrLpwokelk6lFZPe4twpTsPQqzNKBwKzVbI6AR/g==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.4.2.tgz", + "integrity": "sha512-Mu6TEn1M/owIvAx2B4DUQObQXqo2028R2s9rSZ/hJEgBK95+doTwS0DjmVA2wTeZTyVtXOoN7CsoM5pONBzvKQ==", "requires": { "@microsoft/tsdoc": "^0.15.0", "@nestjs/mapped-types": "2.0.5", "js-yaml": "4.1.0", "lodash": "4.17.21", - "path-to-regexp": "3.2.0", + "path-to-regexp": "3.3.0", "swagger-ui-dist": "5.17.14" } }, "@nestjs/testing": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", - "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.3.tgz", + "integrity": "sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==", "dev": true, "requires": { - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "dev": true + } } }, "@nestjs/typeorm": { @@ -17472,13 +16683,20 @@ } }, "@nestjs/websockets": { - "version": "10.4.1", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", - "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.3.tgz", + "integrity": "sha512-EW5/GR0jImJwrb8+YpHPoFN2tlhYQzVE2yAN5Se5sygUr/ZFMNAG84sd79NmWGd4RxoxR0aFH9nRycQ/0Ebe5w==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", - "tslib": "2.6.3" + "tslib": "2.7.0" + }, + "dependencies": { + "tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" + } } }, "@next/env": { @@ -17584,132 +16802,65 @@ "integrity": "sha512-I/s6F7yKUDdtMsoBWXJe8Qz40Tui5vsuKCWJEWVL+5q9sSWRzzx6v2KeNsOBEwd94j0eWkpWCH4yB6rZg9Mf0w==" }, "@opentelemetry/api-logs": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.0.tgz", - "integrity": "sha512-HxjD7xH9iAE4OyhNaaSec65i1H6QZYBWSwWkowFfsc5YAcDvJG30/J1sRKXEQqdmUcKTXEAnA66UciqZha/4+Q==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", + "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", "requires": { "@opentelemetry/api": "^1.0.0" } }, "@opentelemetry/auto-instrumentations-node": { - "version": "0.49.2", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz", - "integrity": "sha512-xtETEPmAby/3MMmedv8Z/873sdLTWg+Vq98rtm4wbwvAiXBB/ao8qRyzRlvR2MR6puEr+vIB/CXeyJnzNA3cyw==", + "version": "0.50.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.50.0.tgz", + "integrity": "sha512-LqoSiWrOM4Cnr395frDHL4R/o5c2fuqqrqW8sZwhxvkasImmVlyL66YMPHllM2O5xVj2nP2ANUKHZSd293meZA==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/instrumentation-amqplib": "^0.41.0", - "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", - "@opentelemetry/instrumentation-bunyan": "^0.40.0", - "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", - "@opentelemetry/instrumentation-connect": "^0.38.0", - "@opentelemetry/instrumentation-cucumber": "^0.8.0", - "@opentelemetry/instrumentation-dataloader": "^0.11.0", - "@opentelemetry/instrumentation-dns": "^0.38.0", - "@opentelemetry/instrumentation-express": "^0.41.1", - "@opentelemetry/instrumentation-fastify": "^0.38.0", - "@opentelemetry/instrumentation-fs": "^0.14.0", - "@opentelemetry/instrumentation-generic-pool": "^0.38.1", - "@opentelemetry/instrumentation-graphql": "^0.42.0", - "@opentelemetry/instrumentation-grpc": "^0.52.0", - "@opentelemetry/instrumentation-hapi": "^0.40.0", - "@opentelemetry/instrumentation-http": "^0.52.0", - "@opentelemetry/instrumentation-ioredis": "^0.42.0", - "@opentelemetry/instrumentation-kafkajs": "^0.2.0", - "@opentelemetry/instrumentation-knex": "^0.39.0", - "@opentelemetry/instrumentation-koa": "^0.42.0", - "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", - "@opentelemetry/instrumentation-memcached": "^0.38.0", - "@opentelemetry/instrumentation-mongodb": "^0.46.0", - "@opentelemetry/instrumentation-mongoose": "^0.41.0", - "@opentelemetry/instrumentation-mysql": "^0.40.0", - "@opentelemetry/instrumentation-mysql2": "^0.40.0", - "@opentelemetry/instrumentation-nestjs-core": "^0.39.0", - "@opentelemetry/instrumentation-net": "^0.38.0", - "@opentelemetry/instrumentation-pg": "^0.43.0", - "@opentelemetry/instrumentation-pino": "^0.41.0", - "@opentelemetry/instrumentation-redis": "^0.41.0", - "@opentelemetry/instrumentation-redis-4": "^0.41.1", - "@opentelemetry/instrumentation-restify": "^0.40.0", - "@opentelemetry/instrumentation-router": "^0.39.0", - "@opentelemetry/instrumentation-socket.io": "^0.41.0", - "@opentelemetry/instrumentation-tedious": "^0.13.0", - "@opentelemetry/instrumentation-undici": "^0.5.0", - "@opentelemetry/instrumentation-winston": "^0.39.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0", - "@opentelemetry/resource-detector-aws": "^1.6.0", - "@opentelemetry/resource-detector-azure": "^0.2.10", - "@opentelemetry/resource-detector-container": "^0.4.0", - "@opentelemetry/resource-detector-gcp": "^0.29.10", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/instrumentation-amqplib": "^0.42.0", + "@opentelemetry/instrumentation-aws-lambda": "^0.44.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.44.0", + "@opentelemetry/instrumentation-bunyan": "^0.41.0", + "@opentelemetry/instrumentation-cassandra-driver": "^0.41.0", + "@opentelemetry/instrumentation-connect": "^0.39.0", + "@opentelemetry/instrumentation-cucumber": "^0.9.0", + "@opentelemetry/instrumentation-dataloader": "^0.12.0", + "@opentelemetry/instrumentation-dns": "^0.39.0", + "@opentelemetry/instrumentation-express": "^0.42.0", + "@opentelemetry/instrumentation-fastify": "^0.39.0", + "@opentelemetry/instrumentation-fs": "^0.15.0", + "@opentelemetry/instrumentation-generic-pool": "^0.39.0", + "@opentelemetry/instrumentation-graphql": "^0.43.0", + "@opentelemetry/instrumentation-grpc": "^0.53.0", + "@opentelemetry/instrumentation-hapi": "^0.41.0", + "@opentelemetry/instrumentation-http": "^0.53.0", + "@opentelemetry/instrumentation-ioredis": "^0.43.0", + "@opentelemetry/instrumentation-kafkajs": "^0.3.0", + "@opentelemetry/instrumentation-knex": "^0.40.0", + "@opentelemetry/instrumentation-koa": "^0.43.0", + "@opentelemetry/instrumentation-lru-memoizer": "^0.40.0", + "@opentelemetry/instrumentation-memcached": "^0.39.0", + "@opentelemetry/instrumentation-mongodb": "^0.47.0", + "@opentelemetry/instrumentation-mongoose": "^0.42.0", + "@opentelemetry/instrumentation-mysql": "^0.41.0", + "@opentelemetry/instrumentation-mysql2": "^0.41.0", + "@opentelemetry/instrumentation-nestjs-core": "^0.40.0", + "@opentelemetry/instrumentation-net": "^0.39.0", + "@opentelemetry/instrumentation-pg": "^0.44.0", + "@opentelemetry/instrumentation-pino": "^0.42.0", + "@opentelemetry/instrumentation-redis": "^0.42.0", + "@opentelemetry/instrumentation-redis-4": "^0.42.0", + "@opentelemetry/instrumentation-restify": "^0.41.0", + "@opentelemetry/instrumentation-router": "^0.40.0", + "@opentelemetry/instrumentation-socket.io": "^0.42.0", + "@opentelemetry/instrumentation-tedious": "^0.14.0", + "@opentelemetry/instrumentation-undici": "^0.6.0", + "@opentelemetry/instrumentation-winston": "^0.40.0", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.1", + "@opentelemetry/resource-detector-aws": "^1.6.1", + "@opentelemetry/resource-detector-azure": "^0.2.11", + "@opentelemetry/resource-detector-container": "^0.4.1", + "@opentelemetry/resource-detector-gcp": "^0.29.11", "@opentelemetry/resources": "^1.24.0", - "@opentelemetry/sdk-node": "^0.52.0" - }, - "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/instrumentation": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.1.tgz", - "integrity": "sha512-uXJbYU/5/MBHjMp1FqrILLRuiJCs3Ofk0MeRDk8g1S1gD47U8X3JnSwcMO1rtRo1x1a7zKaQHaoYu49p/4eSKw==", - "requires": { - "@opentelemetry/api-logs": "0.52.1", - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - } - }, - "@opentelemetry/sdk-node": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-node/-/sdk-node-0.52.1.tgz", - "integrity": "sha512-uEG+gtEr6eKd8CVWeKMhH2olcCHM9dEK68pe0qE0be32BcCRsvYURhHaD1Srngh1SQcnQzZ4TP324euxqtBOJA==", - "requires": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/exporter-trace-otlp-grpc": "0.52.1", - "@opentelemetry/exporter-trace-otlp-http": "0.52.1", - "@opentelemetry/exporter-trace-otlp-proto": "0.52.1", - "@opentelemetry/exporter-zipkin": "1.25.1", - "@opentelemetry/instrumentation": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-logs": "0.52.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/sdk-trace-node": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - }, - "import-in-the-middle": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", - "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", - "requires": { - "acorn": "^8.8.2", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - } + "@opentelemetry/sdk-node": "^0.53.0" } }, "@opentelemetry/context-async-hooks": { @@ -17719,11 +16870,11 @@ "requires": {} }, "@opentelemetry/core": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.0.tgz", - "integrity": "sha512-n0B3s8rrqGrasTgNkXLKXzN0fXo+6IYP7M5b7AMsrZM33f/y6DS6kJ0Btd7SespASWq8bgL3taLo0oe0vB52IQ==", + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", + "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", "requires": { - "@opentelemetry/semantic-conventions": "1.25.0" + "@opentelemetry/semantic-conventions": "1.27.0" } }, "@opentelemetry/exporter-logs-otlp-grpc": { @@ -17738,22 +16889,6 @@ "@opentelemetry/sdk-logs": "0.53.0" }, "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.27.0" - } - }, "@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -17825,11 +16960,6 @@ "@opentelemetry/resources": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" } } }, @@ -17845,22 +16975,6 @@ "@opentelemetry/sdk-logs": "0.53.0" }, "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.27.0" - } - }, "@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -17921,11 +17035,6 @@ "@opentelemetry/resources": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" } } }, @@ -17943,22 +17052,6 @@ "@opentelemetry/sdk-trace-base": "1.26.0" }, "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.27.0" - } - }, "@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -18019,11 +17112,6 @@ "@opentelemetry/resources": "1.26.0", "@opentelemetry/semantic-conventions": "1.27.0" } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" } } }, @@ -18037,14 +17125,6 @@ "@opentelemetry/sdk-metrics": "1.26.0" }, "dependencies": { - "@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.27.0" - } - }, "@opentelemetry/resources": { "version": "1.26.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-1.26.0.tgz", @@ -18062,119 +17142,6 @@ "@opentelemetry/core": "1.26.0", "@opentelemetry/resources": "1.26.0" } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" - } - } - }, - "@opentelemetry/exporter-trace-otlp-grpc": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz", - "integrity": "sha512-pVkSH20crBwMTqB3nIN4jpQKUEoB0Z94drIHpYyEqs7UBr+I0cpYyOR3bqjA/UasQUMROb3GX8ZX4/9cVRqGBQ==", - "requires": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-grpc-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/exporter-trace-otlp-http": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-http/-/exporter-trace-otlp-http-0.52.1.tgz", - "integrity": "sha512-05HcNizx0BxcFKKnS5rwOV+2GevLTVIRA0tRgWYyw4yCgR53Ic/xk83toYKts7kbzcI+dswInUg/4s8oyA+tqg==", - "requires": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/exporter-trace-otlp-proto": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-proto/-/exporter-trace-otlp-proto-0.52.1.tgz", - "integrity": "sha512-pt6uX0noTQReHXNeEslQv7x311/F1gJzMnp1HD2qgypLRPbXDeMzzeTngRTUaUbP6hqWNtPxuLr4DEoZG+TcEQ==", - "requires": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/exporter-zipkin": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-zipkin/-/exporter-zipkin-1.25.1.tgz", - "integrity": "sha512-RmOwSvkimg7ETwJbUOPTMhJm9A9bG1U8s7Zo3ajDh4zM7eYcycQ0dM7FbLD6NXWbI2yj7UY4q8BKinKYBQksyw==", - "requires": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" } } }, @@ -18188,306 +17155,306 @@ } }, "@opentelemetry/instrumentation": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.52.0.tgz", - "integrity": "sha512-LPwSIrw+60cheWaXsfGL8stBap/AppKQJFE+qqRvzYrgttXFH2ofoIMxWadeqPTq4BYOXM/C7Bdh/T+B60xnlQ==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", + "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", "requires": { - "@opentelemetry/api-logs": "0.52.0", - "@types/shimmer": "^1.0.2", - "import-in-the-middle": "1.8.0", + "@opentelemetry/api-logs": "0.53.0", + "@types/shimmer": "^1.2.0", + "import-in-the-middle": "^1.8.1", "require-in-the-middle": "^7.1.1", "semver": "^7.5.2", "shimmer": "^1.2.1" } }, "@opentelemetry/instrumentation-amqplib": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz", - "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.42.0.tgz", + "integrity": "sha512-fiuU6OKsqHJiydHWgTRQ7MnIrJ2lEqsdgFtNIH4LbAUJl/5XmrIeoDzDnox+hfkgWK65jsleFuQDtYb5hW1koQ==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-aws-lambda": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.43.0.tgz", - "integrity": "sha512-pSxcWlsE/pCWQRIw92sV2C+LmKXelYkjkA7C5s39iPUi4pZ2lA1nIiw+1R/y2pDEhUHcaKkNyljQr3cx9ZpVlQ==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-lambda/-/instrumentation-aws-lambda-0.44.0.tgz", + "integrity": "sha512-6vmr7FJIuopZzsQjEQTp4xWtF6kBp7DhD7pPIN8FN0dKUKyuVraABIpgWjMfelaUPy4rTYUGkYqPluhG0wx8Dw==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/propagator-aws-xray": "^1.3.1", "@opentelemetry/resources": "^1.8.0", - "@opentelemetry/semantic-conventions": "^1.22.0", - "@types/aws-lambda": "8.10.122" + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/aws-lambda": "8.10.143" } }, "@opentelemetry/instrumentation-aws-sdk": { - "version": "0.43.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz", - "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.44.0.tgz", + "integrity": "sha512-HIWFg4TDQsayceiikOnruMmyQ0SZYW6WiR+wknWwWVLHC3lHTCpAnqzp5V42ckArOdlwHZu2Jvq2GMSM4Myx3w==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/propagation-utils": "^0.30.10", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/propagation-utils": "^0.30.11", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-bunyan": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.40.0.tgz", - "integrity": "sha512-aZ4cXaGWwj79ZXSYrgFVsrDlE4mmf2wfvP9bViwRc0j75A6eN6GaHYHqufFGMTCqASQn5pIjjP+Bx+PWTGiofw==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-bunyan/-/instrumentation-bunyan-0.41.0.tgz", + "integrity": "sha512-NoQS+gcwQ7pzb2PZFyra6bAxDAVXBMmpKxBblEuXJWirGrAksQllg9XTdmqhrwT/KxUYrbVca/lMams7e51ysg==", "requires": { - "@opentelemetry/api-logs": "^0.52.0", - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/instrumentation": "^0.53.0", "@types/bunyan": "1.8.9" } }, "@opentelemetry/instrumentation-cassandra-driver": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.40.0.tgz", - "integrity": "sha512-JxbM39JU7HxE9MTKKwi6y5Z3mokjZB2BjwfqYi4B3Y29YO3I42Z7eopG6qq06yWZc+nQli386UDQe0d9xKmw0A==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cassandra-driver/-/instrumentation-cassandra-driver-0.41.0.tgz", + "integrity": "sha512-hvTNcC8qjCQEHZTLAlTmDptjsEGqCKpN+90hHH8Nn/GwilGr5TMSwGrlfstdJuZWyw8HAnRUed6bcjvmHHk2Xw==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-connect": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.38.0.tgz", - "integrity": "sha512-2/nRnx3pjYEmdPIaBwtgtSviTKHWnDZN3R+TkRUnhIVrvBKVcq+I5B2rtd6mr6Fe9cHlZ9Ojcuh7pkNh/xdWWg==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-connect/-/instrumentation-connect-0.39.0.tgz", + "integrity": "sha512-pGBiKevLq7NNglMgqzmeKczF4XQMTOUOTkK8afRHMZMnrK3fcETyTH7lVaSozwiOM3Ws+SuEmXZT7DYrrhxGlg==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/connect": "3.4.36" } }, "@opentelemetry/instrumentation-cucumber": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.8.0.tgz", - "integrity": "sha512-ieTm4RBIlZt2brPwtX5aEZYtYnkyqhAVXJI9RIohiBVMe5DxiwCwt+2Exep/nDVqGPX8zRBZUl4AEw423OxJig==", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-cucumber/-/instrumentation-cucumber-0.9.0.tgz", + "integrity": "sha512-4PQNFnIqnA2WM3ZHpr0xhZpHSqJ5xJ6ppTIzZC7wPqe+ZBpj41vG8B6ieqiPfq+im4QdqbYnzLb3rj48GDEN9g==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-dataloader": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.11.0.tgz", - "integrity": "sha512-27urJmwkH4KDaMJtEv1uy2S7Apk4XbN4AgWMdfMJbi7DnOduJmeuA+DpJCwXB72tEWXo89z5T3hUVJIDiSNmNw==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dataloader/-/instrumentation-dataloader-0.12.0.tgz", + "integrity": "sha512-pnPxatoFE0OXIZDQhL2okF//dmbiWFzcSc8pUg9TqofCLYZySSxDCgQc69CJBo5JnI3Gz1KP+mOjS4WAeRIH4g==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-dns": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.38.0.tgz", - "integrity": "sha512-Um07I0TQXDWa+ZbEAKDFUxFH40dLtejtExDOMLNJ1CL8VmOmA71qx93Qi/QG4tGkiI1XWqr7gF/oiMCJ4m8buQ==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-dns/-/instrumentation-dns-0.39.0.tgz", + "integrity": "sha512-+iPzvXqVdJa67QBuz2tuP0UI3LS1/cMMo6dS7360DDtOQX+sQzkiN+mo3Omn4T6ZRhkTDw6c7uwsHBcmL31+1g==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "semver": "^7.5.4" } }, "@opentelemetry/instrumentation-express": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz", - "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.42.0.tgz", + "integrity": "sha512-YNcy7ZfGnLsVEqGXQPT+S0G1AE46N21ORY7i7yUQyfhGAL4RBjnZUqefMI0NwqIl6nGbr1IpF0rZGoN8Q7x12Q==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-fastify": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.38.0.tgz", - "integrity": "sha512-HBVLpTSYpkQZ87/Df3N0gAw7VzYZV3n28THIBrJWfuqw3Or7UqdhnjeuMIPQ04BKk3aZc0cWn2naSQObbh5vXw==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fastify/-/instrumentation-fastify-0.39.0.tgz", + "integrity": "sha512-SS9uSlKcsWZabhBp2szErkeuuBDgxOUlllwkS92dVaWRnMmwysPhcEgHKB8rUe3BHg/GnZC1eo1hbTZv4YhfoA==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-fs": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.14.0.tgz", - "integrity": "sha512-pVc8P5AgliC1DphyyBUgsxXlm2XaPH4BpYvt7rAZDMIqUpRk8gs19SioABtKqqxvFzg5jPtgJfJsdxq0Y+maLw==", + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-fs/-/instrumentation-fs-0.15.0.tgz", + "integrity": "sha512-JWVKdNLpu1skqZQA//jKOcKdJC66TWKqa2FUFq70rKohvaSq47pmXlnabNO+B/BvLfmidfiaN35XakT5RyMl2Q==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-generic-pool": { - "version": "0.38.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.38.1.tgz", - "integrity": "sha512-WvssuKCuavu/hlq661u82UWkc248cyI/sT+c2dEIj6yCk0BUkErY1D+9XOO+PmHdJNE+76i2NdcvQX5rJrOe/w==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-generic-pool/-/instrumentation-generic-pool-0.39.0.tgz", + "integrity": "sha512-y4v8Y+tSfRB3NNBvHjbjrn7rX/7sdARG7FuK6zR8PGb28CTa0kHpEGCJqvL9L8xkTNvTXo+lM36ajFGUaK1aNw==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-graphql": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.42.0.tgz", - "integrity": "sha512-N8SOwoKL9KQSX7z3gOaw5UaTeVQcfDO1c21csVHnmnmGUoqsXbArK2B8VuwPWcv6/BC/i3io+xTo7QGRZ/z28Q==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-graphql/-/instrumentation-graphql-0.43.0.tgz", + "integrity": "sha512-aI3YMmC2McGd8KW5du1a2gBA0iOMOGLqg4s9YjzwbjFwjlmMNFSK1P3AIg374GWg823RPUGfVTIgZ/juk9CVOA==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-grpc": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.52.0.tgz", - "integrity": "sha512-YYhA2pbhMWgF5Hp6eR7AHp1utzZQ3Y0VB8GIwd8zJoLtAuQRZa1N29DUtZ+t/pGRJF+xGPVI+vP+7ugHgeN0zQ==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-grpc/-/instrumentation-grpc-0.53.0.tgz", + "integrity": "sha512-Ss338T92yE1UCgr9zXSY3cPuaAy27uQw+wAC5IwsQKCXL5wwkiOgkd+2Ngksa9EGsgUEMwGeHi76bDdHFJ5Rrw==", "requires": { - "@opentelemetry/instrumentation": "0.52.0", - "@opentelemetry/semantic-conventions": "1.25.0" + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/semantic-conventions": "1.27.0" } }, "@opentelemetry/instrumentation-hapi": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.40.0.tgz", - "integrity": "sha512-8U/w7Ifumtd2bSN1OLaSwAAFhb9FyqWUki3lMMB0ds+1+HdSxYBe9aspEJEgvxAqOkrQnVniAPTEGf1pGM7SOw==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-hapi/-/instrumentation-hapi-0.41.0.tgz", + "integrity": "sha512-jKDrxPNXDByPlYcMdZjNPYCvw0SQJjN+B1A+QH+sx+sAHsKSAf9hwFiJSrI6C4XdOls43V/f/fkp9ITkHhKFbQ==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-http": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.52.0.tgz", - "integrity": "sha512-E6ywZuxTa4LnVXZGwL1oj3e2Eog1yIaNqa8KjKXoGkDNKte9/SjQnePXOmhQYI0A9nf0UyFbP9aKd+yHrkJXUA==", + "version": "0.53.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-http/-/instrumentation-http-0.53.0.tgz", + "integrity": "sha512-H74ErMeDuZfj7KgYCTOFGWF5W9AfaPnqLQQxeFq85+D29wwV2yqHbz2IKLYpkOh7EI6QwDEl7rZCIxjJLyc/CQ==", "requires": { - "@opentelemetry/core": "1.25.0", - "@opentelemetry/instrumentation": "0.52.0", - "@opentelemetry/semantic-conventions": "1.25.0", + "@opentelemetry/core": "1.26.0", + "@opentelemetry/instrumentation": "0.53.0", + "@opentelemetry/semantic-conventions": "1.27.0", "semver": "^7.5.2" } }, "@opentelemetry/instrumentation-ioredis": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.42.0.tgz", - "integrity": "sha512-P11H168EKvBB9TUSasNDOGJCSkpT44XgoM6d3gRIWAa9ghLpYhl0uRkS8//MqPzcJVHr3h3RmfXIpiYLjyIZTw==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-ioredis/-/instrumentation-ioredis-0.43.0.tgz", + "integrity": "sha512-i3Dke/LdhZbiUAEImmRG3i7Dimm/BD7t8pDDzwepSvIQ6s2X6FPia7561gw+64w+nx0+G9X14D7rEfaMEmmjig==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.23.0" + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-kafkajs": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz", - "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.3.0.tgz", + "integrity": "sha512-UnkZueYK1ise8FXQeKlpBd7YYUtC7mM8J0wzUSccEfc/G8UqHQqAzIyYCUOUPUKp8GsjLnWOOK/3hJc4owb7Jg==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.24.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-knex": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz", - "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.40.0.tgz", + "integrity": "sha512-6jka2jfX8+fqjEbCn6hKWHVWe++mHrIkLQtaJqUkBt3ZBs2xn1+y0khxiDS0v/mNb0bIKDJWwtpKFfsQDM1Geg==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-koa": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.42.0.tgz", - "integrity": "sha512-H1BEmnMhho8o8HuNRq5zEI4+SIHDIglNB7BPKohZyWG4fWNuR7yM4GTlR01Syq21vODAS7z5omblScJD/eZdKw==", + "version": "0.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-koa/-/instrumentation-koa-0.43.0.tgz", + "integrity": "sha512-lDAhSnmoTIN6ELKmLJBplXzT/Jqs5jGZehuG22EdSMaTwgjMpxMDI1YtlKEhiWPWkrz5LUsd0aOO0ZRc9vn3AQ==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-lru-memoizer": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.39.0.tgz", - "integrity": "sha512-eU1Wx1RRTR/2wYXFzH9gcpB8EPmhYlNDIUHzUXjyUE0CAXEJhBLkYNlzdaVCoQDw2neDqS+Woshiia6+emWK9A==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-lru-memoizer/-/instrumentation-lru-memoizer-0.40.0.tgz", + "integrity": "sha512-21xRwZsEdMPnROu/QsaOIODmzw59IYpGFmuC4aFWvMj6stA8+Ei1tX67nkarJttlNjoM94um0N4X26AD7ff54A==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-memcached": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.38.0.tgz", - "integrity": "sha512-tPmyqQEZNyrvg6G+iItdlguQEcGzfE+bJkpQifmBXmWBnoS5oU3UxqtyYuXGL2zI9qQM5yMBHH4nRXWALzy7WA==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-memcached/-/instrumentation-memcached-0.39.0.tgz", + "integrity": "sha512-WfwvKAZ9I1qILRP5EUd88HQjwAAL+trXpCpozjBi4U6a0A07gB3fZ5PFAxbXemSjF5tHk9KVoROnqHvQ+zzFSQ==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.23.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/memcached": "^2.2.6" } }, "@opentelemetry/instrumentation-mongodb": { - "version": "0.46.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.46.0.tgz", - "integrity": "sha512-VF/MicZ5UOBiXrqBslzwxhN7TVqzu1/LN/QDpkskqM0Zm0aZ4CVRbUygL8d7lrjLn15x5kGIe8VsSphMfPJzlA==", + "version": "0.47.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongodb/-/instrumentation-mongodb-0.47.0.tgz", + "integrity": "sha512-yqyXRx2SulEURjgOQyJzhCECSh5i1uM49NUaq9TqLd6fA7g26OahyJfsr9NE38HFqGRHpi4loyrnfYGdrsoVjQ==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/sdk-metrics": "^1.9.1", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-mongoose": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.41.0.tgz", - "integrity": "sha512-ivJg4QnnabFxxoI7K8D+in7hfikjte38sYzJB9v1641xJk9Esa7jM3hmbPB7lxwcgWJLVEDvfPwobt1if0tXxA==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mongoose/-/instrumentation-mongoose-0.42.0.tgz", + "integrity": "sha512-AnWv+RaR86uG3qNEMwt3plKX1ueRM7AspfszJYVkvkehiicC3bHQA6vWdb6Zvy5HAE14RyFbu9+2hUUjR2NSyg==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-mysql": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.40.0.tgz", - "integrity": "sha512-d7ja8yizsOCNMYIJt5PH/fKZXjb/mS48zLROO4BzZTtDfhNCl2UM/9VIomP2qkGIFVouSJrGr/T00EzY7bPtKA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql/-/instrumentation-mysql-0.41.0.tgz", + "integrity": "sha512-jnvrV6BsQWyHS2qb2fkfbfSb1R/lmYwqEZITwufuRl37apTopswu9izc0b1CYRp/34tUG/4k/V39PND6eyiNvw==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", - "@types/mysql": "2.15.22" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", + "@types/mysql": "2.15.26" } }, "@opentelemetry/instrumentation-mysql2": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.40.0.tgz", - "integrity": "sha512-0xfS1xcqUmY7WE1uWjlmI67Xg3QsSUlNT+AcXHeA4BDUPwZtWqF4ezIwLgpVZfHOnkAEheqGfNSWd1PIu3Wnfg==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-mysql2/-/instrumentation-mysql2-0.41.0.tgz", + "integrity": "sha512-REQB0x+IzVTpoNgVmy5b+UnH1/mDByrneimP6sbDHkp1j8QOl1HyWOrBH/6YWR0nrbU3l825Em5PlybjT3232g==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1" } }, "@opentelemetry/instrumentation-nestjs-core": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.39.0.tgz", - "integrity": "sha512-mewVhEXdikyvIZoMIUry8eb8l3HUjuQjSjVbmLVTt4NQi35tkpnHQrG9bTRBrl3403LoWZ2njMPJyg4l6HfKvA==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-nestjs-core/-/instrumentation-nestjs-core-0.40.0.tgz", + "integrity": "sha512-WF1hCUed07vKmf5BzEkL0wSPinqJgH7kGzOjjMAiTGacofNXjb/y4KQ8loj2sNsh5C/NN7s1zxQuCgbWbVTGKg==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.23.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-net": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.38.0.tgz", - "integrity": "sha512-stjow1PijcmUquSmRD/fSihm/H61DbjPlJuJhWUe7P22LFPjFhsrSeiB5vGj3vn+QGceNAs+kioUTzMGPbNxtg==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-net/-/instrumentation-net-0.39.0.tgz", + "integrity": "sha512-rixHoODfI/Cx1B0mH1BpxCT0bRSxktuBDrt9IvpT2KSEutK5hR0RsRdgdz/GKk+BQ4u+IG6godgMSGwNQCueEA==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.23.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-pg": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.43.0.tgz", - "integrity": "sha512-og23KLyoxdnAeFs1UWqzSonuCkePUzCX30keSYigIzJe/6WSYA8rnEI5lobcxPEzg+GcU06J7jzokuEHbjVJNw==", + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pg/-/instrumentation-pg-0.44.0.tgz", + "integrity": "sha512-oTWVyzKqXud1BYEGX1loo2o4k4vaU1elr3vPO8NZolrBtFvQ34nx4HgUaexUDuEog00qQt+MLR5gws/p+JXMLQ==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@opentelemetry/sql-common": "^0.40.1", "@types/pg": "8.6.1", - "@types/pg-pool": "2.0.4" + "@types/pg-pool": "2.0.6" }, "dependencies": { "@types/pg": { @@ -18503,182 +17470,95 @@ } }, "@opentelemetry/instrumentation-pino": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.41.0.tgz", - "integrity": "sha512-Kpv0fJRk/8iMzMk5Ue5BsUJfHkBJ2wQoIi/qduU1a1Wjx9GLj6J2G17PHjPK5mnZjPNzkFOXFADZMfgDioliQw==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-pino/-/instrumentation-pino-0.42.0.tgz", + "integrity": "sha512-SoX6FzucBfTuFNMZjdurJhcYWq2ve8/LkhmyVLUW31HpIB45RF1JNum0u4MkGisosDmXlK4njomcgUovShI+WA==", "requires": { - "@opentelemetry/api-logs": "^0.52.0", + "@opentelemetry/api-logs": "^0.53.0", "@opentelemetry/core": "^1.25.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-redis": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.41.0.tgz", - "integrity": "sha512-RJ1pwI3btykp67ts+5qZbaFSAAzacucwBet5/5EsKYtWBpHbWwV/qbGN/kIBzXg5WEZBhXLrR/RUq0EpEUpL3A==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis/-/instrumentation-redis-0.42.0.tgz", + "integrity": "sha512-jZBoqve0rEC51q0HuhjtZVq1DtUvJHzEJ3YKGvzGar2MU1J4Yt5+pQAQYh1W4jSoDyKeaI4hyeUdWM5N0c2lqA==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-redis-4": { - "version": "0.41.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.41.1.tgz", - "integrity": "sha512-UqJAbxraBk7s7pQTlFi5ND4sAUs4r/Ai7gsAVZTQDbHl2kSsOp7gpHcpIuN5dpcI2xnuhM2tkH4SmEhbrv2S6Q==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-redis-4/-/instrumentation-redis-4-0.42.0.tgz", + "integrity": "sha512-NaD+t2JNcOzX/Qa7kMy68JbmoVIV37fT/fJYzLKu2Wwd+0NCxt+K2OOsOakA8GVg8lSpFdbx4V/suzZZ2Pvdjg==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/instrumentation": "^0.53.0", "@opentelemetry/redis-common": "^0.36.2", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-restify": { - "version": "0.40.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.40.0.tgz", - "integrity": "sha512-sm/rH/GysY/KOEvZqYBZSLYFeXlBkHCgqPDgWc07tz+bHCN6mPs9P3otGOSTe7o3KAIM8Nc6ncCO59vL+jb2cA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-restify/-/instrumentation-restify-0.41.0.tgz", + "integrity": "sha512-gKEo+X/wVKUBuD2WDDlF7SlDNBHMWjSQoLxFCsGqeKgHR0MGtwMel8uaDGg9LJ83nKqYy+7Vl/cDFxjba6H+/w==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-router": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.39.0.tgz", - "integrity": "sha512-LaXnVmD69WPC4hNeLzKexCCS19hRLrUw3xicneAMkzJSzNJvPyk7G6I7lz7VjQh1cooObPBt9gNyd3hhTCUrag==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-router/-/instrumentation-router-0.40.0.tgz", + "integrity": "sha512-bRo4RaclGFiKtmv/N1D0MuzO7DuxbeqMkMCbPPng6mDwzpHAMpHz/K/IxJmF+H1Hi/NYXVjCKvHGClageLe9eA==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-socket.io": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.41.0.tgz", - "integrity": "sha512-7fzDe9/FpO6NFizC/wnzXXX7bF9oRchsD//wFqy5g5hVEgXZCQ70IhxjrKdBvgjyIejR9T9zTvfQ6PfVKfkCAw==", + "version": "0.42.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-socket.io/-/instrumentation-socket.io-0.42.0.tgz", + "integrity": "sha512-xB5tdsBzuZyicQTO3hDzJIpHQ7V1BYJ6vWPWgl19gWZDBdjEGc3HOupjkd3BUJyDoDhbMEHGk2nNlkUU99EfkA==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/instrumentation-tedious": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.13.0.tgz", - "integrity": "sha512-Pob0+0R62AqXH50pjazTeGBy/1+SK4CYpFUBV5t7xpbpeuQezkkgVGvLca84QqjBqQizcXedjpUJLgHQDixPQg==", + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-tedious/-/instrumentation-tedious-0.14.0.tgz", + "integrity": "sha512-ofq7pPhSqvRDvD2FVx3RIWPj76wj4QubfrbqJtEx0A+fWoaYxJOCIQ92tYJh28elAmjMmgF/XaYuJuBhBv5J3A==", "requires": { - "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/instrumentation": "^0.53.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@types/tedious": "^4.0.14" } }, "@opentelemetry/instrumentation-undici": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.5.0.tgz", - "integrity": "sha512-aNTeSrFAVcM9qco5DfZ9DNXu6hpMRe8Kt8nCDHfMWDB3pwgGVUE76jTdohc+H/7eLRqh4L7jqs5NSQoHw7S6ww==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-undici/-/instrumentation-undici-0.6.0.tgz", + "integrity": "sha512-ABJBhm5OdhGmbh0S/fOTE4N69IZ00CsHC5ijMYfzbw3E5NwLgpQk5xsljaECrJ8wz1SfXbO03FiSuu5AyRAkvQ==", "requires": { "@opentelemetry/core": "^1.8.0", - "@opentelemetry/instrumentation": "^0.52.0" + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/instrumentation-winston": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.39.0.tgz", - "integrity": "sha512-v/1xziLJ9CyB3YDjBSBzbB70Qd0JwWTo36EqWK5m3AR0CzsyMQQmf3ZIZM6sgx7hXMcRQ0pnEYhg6nhrUQPm9A==", + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-winston/-/instrumentation-winston-0.40.0.tgz", + "integrity": "sha512-eMk2tKl86YJ8/yHvtDbyhrE35/R0InhO9zuHTflPx8T0+IvKVUhPV71MsJr32sImftqeOww92QHt4Jd+a5db4g==", "requires": { - "@opentelemetry/api-logs": "^0.52.0", - "@opentelemetry/instrumentation": "^0.52.0" - } - }, - "@opentelemetry/otlp-exporter-base": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.52.1.tgz", - "integrity": "sha512-z175NXOtX5ihdlshtYBe5RpGeBoTXVCKPPLiQlD6FHvpM4Ch+p2B0yWKYSrBfLH24H9zjJiBdTrtD+hLlfnXEQ==", - "requires": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-transformer": "0.52.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/otlp-grpc-exporter-base": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz", - "integrity": "sha512-zo/YrSDmKMjG+vPeA9aBBrsQM9Q/f2zo6N04WMB3yNldJRsgpRBeLLwvAt/Ba7dpehDLOEFBd1i2JCoaFtpCoQ==", - "requires": { - "@grpc/grpc-js": "^1.7.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/otlp-exporter-base": "0.52.1", - "@opentelemetry/otlp-transformer": "0.52.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/otlp-transformer": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.52.1.tgz", - "integrity": "sha512-I88uCZSZZtVa0XniRqQWKbjAUm73I8tpEy/uJYPPYw5d7BRdVk0RfTBQw8kSUl01oVWEuqxLDa802222MYyWHg==", - "requires": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/sdk-logs": "0.52.1", - "@opentelemetry/sdk-metrics": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "protobufjs": "^7.3.0" - }, - "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } + "@opentelemetry/api-logs": "^0.53.0", + "@opentelemetry/instrumentation": "^0.53.0" } }, "@opentelemetry/propagation-utils": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.10.tgz", - "integrity": "sha512-hhTW8pFp9PSyosYzzuUL9rdm7HF97w3OCyElufFHyUnYnKkCBbu8ne2LyF/KSdI/xZ81ubxWZs78hX4S7pLq5g==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.11.tgz", + "integrity": "sha512-rY4L/2LWNk5p/22zdunpqVmgz6uN419DsRTw5KFMa6u21tWhXS8devlMy4h8m8nnS20wM7r6yYweCNNKjgLYJw==", "requires": {} }, "@opentelemetry/propagator-aws-xray": { @@ -18689,64 +17569,18 @@ "@opentelemetry/core": "^1.0.0" } }, - "@opentelemetry/propagator-b3": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-b3/-/propagator-b3-1.25.1.tgz", - "integrity": "sha512-p6HFscpjrv7//kE+7L+3Vn00VEDUJB0n6ZrjkTYHrJ58QZ8B3ajSJhRbCcY6guQ3PDjTbxWklyvIN2ojVbIb1A==", - "requires": { - "@opentelemetry/core": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/propagator-jaeger": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/propagator-jaeger/-/propagator-jaeger-1.25.1.tgz", - "integrity": "sha512-nBprRf0+jlgxks78G/xq72PipVK+4or9Ypntw0gVZYNTCSK8rg5SeaGV19tV920CMqBD/9UIOiFr23Li/Q8tiA==", - "requires": { - "@opentelemetry/core": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, "@opentelemetry/redis-common": { "version": "0.36.2", "resolved": "https://registry.npmjs.org/@opentelemetry/redis-common/-/redis-common-0.36.2.tgz", "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==" }, "@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz", - "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==", + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.1.tgz", + "integrity": "sha512-Qshebw6azBuKUqGkVgambZlLS6Xh+LCoLXep1oqW1RSzSOHQxGYDsPs99v8NzO65eJHHOu8wc2yKsWZQAgYsSw==", "requires": { "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0" + "@opentelemetry/semantic-conventions": "^1.27.0" } }, "@opentelemetry/resource-detector-aws": { @@ -18757,13 +17591,6 @@ "@opentelemetry/core": "^1.0.0", "@opentelemetry/resources": "^1.10.0", "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "dependencies": { - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" - } } }, "@opentelemetry/resource-detector-azure": { @@ -18774,21 +17601,6 @@ "@opentelemetry/core": "^1.25.1", "@opentelemetry/resources": "^1.10.1", "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.27.0" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" - } } }, "@opentelemetry/resource-detector-container": { @@ -18798,23 +17610,16 @@ "requires": { "@opentelemetry/resources": "^1.10.0", "@opentelemetry/semantic-conventions": "^1.27.0" - }, - "dependencies": { - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" - } } }, "@opentelemetry/resource-detector-gcp": { - "version": "0.29.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.10.tgz", - "integrity": "sha512-rm2HKJ9lsdoVvrbmkr9dkOzg3Uk0FksXNxvNBgrCprM1XhMoJwThI5i0h/5sJypISUAJlEeJS6gn6nROj/NpkQ==", + "version": "0.29.11", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-gcp/-/resource-detector-gcp-0.29.11.tgz", + "integrity": "sha512-07wJx4nyxD/c2z3n70OQOg8fmoO/baTsq8uU+f7tZaehRNQx76MPkRbV2L902N40Z21SPIG8biUZ30OXE9tOIg==", "requires": { "@opentelemetry/core": "^1.0.0", "@opentelemetry/resources": "^1.0.0", - "@opentelemetry/semantic-conventions": "^1.22.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "gcp-metadata": "^6.0.0" } }, @@ -18842,39 +17647,6 @@ } } }, - "@opentelemetry/sdk-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.52.1.tgz", - "integrity": "sha512-MBYh+WcPPsN8YpRHRmK1Hsca9pVlyyKd4BxOC4SsgHACnl/bPp4Cri9hWhVm5+2tiQ9Zf4qSc1Jshw9tOLGWQA==", - "requires": { - "@opentelemetry/api-logs": "0.52.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1" - }, - "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.52.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.52.1.tgz", - "integrity": "sha512-qnSqB2DQ9TPP96dl8cDubDvrUyWc0/sK81xHTK8eSUspzDM3bsewX903qclQFvVhgStjRWdC5bLb3kQqMkfV5A==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, "@opentelemetry/sdk-metrics": { "version": "1.25.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", @@ -18923,22 +17695,6 @@ "@opentelemetry/semantic-conventions": "1.27.0" }, "dependencies": { - "@opentelemetry/api-logs": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.53.0.tgz", - "integrity": "sha512-8HArjKx+RaAI8uEIgcORbZIPklyh1YLjPSBus8hjRmvLi6DeFzgOcdZ7KwPabKj8mXF8dX0hyfAyGfycz0DbFw==", - "requires": { - "@opentelemetry/api": "^1.0.0" - } - }, - "@opentelemetry/core": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.26.0.tgz", - "integrity": "sha512-1iKxXXE8415Cdv0yjG3G6hQnB5eVEsJce3QaawX8SjDn0mAS0ZM8fAbZZJD4ajvhC15cePvosSCut404KrIIvQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.27.0" - } - }, "@opentelemetry/exporter-trace-otlp-grpc": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.53.0.tgz", @@ -18987,19 +17743,6 @@ "@opentelemetry/semantic-conventions": "1.27.0" } }, - "@opentelemetry/instrumentation": { - "version": "0.53.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.53.0.tgz", - "integrity": "sha512-DMwg0hy4wzf7K73JJtl95m/e0boSoWhH07rfvHvYzQtBD3Bmv0Wc1x733vyZBqmFm8OjJD0/pfiUg1W3JjFX0A==", - "requires": { - "@opentelemetry/api-logs": "0.53.0", - "@types/shimmer": "^1.2.0", - "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "semver": "^7.5.2", - "shimmer": "^1.2.1" - } - }, "@opentelemetry/otlp-exporter-base": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.53.0.tgz", @@ -19100,88 +17843,13 @@ "@opentelemetry/sdk-trace-base": "1.26.0", "semver": "^7.5.2" } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.27.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", - "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" - }, - "import-in-the-middle": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", - "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", - "requires": { - "acorn": "^8.8.2", - "acorn-import-attributes": "^1.9.5", - "cjs-module-lexer": "^1.2.2", - "module-details-from-path": "^1.0.3" - } - } - } - }, - "@opentelemetry/sdk-trace-base": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.25.1.tgz", - "integrity": "sha512-C8k4hnEbc5FamuZQ92nTOp8X/diCY56XUTnMiv9UTuJitCzaNNHAVsdm5+HLCdI8SLQsLWIrG38tddMxLVoftw==", - "requires": { - "@opentelemetry/core": "1.25.1", - "@opentelemetry/resources": "1.25.1", - "@opentelemetry/semantic-conventions": "1.25.1" - }, - "dependencies": { - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" - } - } - }, - "@opentelemetry/sdk-trace-node": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-node/-/sdk-trace-node-1.25.1.tgz", - "integrity": "sha512-nMcjFIKxnFqoez4gUmihdBrbpsEnAX/Xj16sGvZm+guceYE0NE00vLhpDVK6f3q8Q4VFI5xG8JjlXKMB/SkTTQ==", - "requires": { - "@opentelemetry/context-async-hooks": "1.25.1", - "@opentelemetry/core": "1.25.1", - "@opentelemetry/propagator-b3": "1.25.1", - "@opentelemetry/propagator-jaeger": "1.25.1", - "@opentelemetry/sdk-trace-base": "1.25.1", - "semver": "^7.5.2" - }, - "dependencies": { - "@opentelemetry/context-async-hooks": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-1.25.1.tgz", - "integrity": "sha512-UW/ge9zjvAEmRWVapOP0qyCvPulWU6cQxGxDbWEFfGOj1VBBZAuOqTo3X6yWmDTD3Xe15ysCZChHncr2xFMIfQ==", - "requires": {} - }, - "@opentelemetry/core": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.25.1.tgz", - "integrity": "sha512-GeT/l6rBYWVQ4XArluLVB6WWQ8flHbdb6r2FCHC3smtdOAbrJBIv35tpV/yp9bmYUJf+xmZpu9DRTIeJVhFbEQ==", - "requires": { - "@opentelemetry/semantic-conventions": "1.25.1" - } - }, - "@opentelemetry/semantic-conventions": { - "version": "1.25.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.1.tgz", - "integrity": "sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==" } } }, "@opentelemetry/semantic-conventions": { - "version": "1.25.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.25.0.tgz", - "integrity": "sha512-M+kkXKRAIAiAP6qYyesfrC5TOmDpDVtsxuGfPcqd9B/iBrac+E14jYwrgm0yZBUIbIP2OnqC3j+UgkXLm1vxUQ==" + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.27.0.tgz", + "integrity": "sha512-sAay1RrB+ONOem0OZanAR1ZI/k7yDpnOQSQmTMuGImUQb2y8EbSaCJ94FQluM74xoU03vlb2d2U90hZluL6nQg==" }, "@opentelemetry/sql-common": { "version": "0.40.1", @@ -19280,9 +17948,9 @@ "requires": {} }, "@react-email/code-block": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz", - "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.9.tgz", + "integrity": "sha512-Zrhc71VYrSC1fVXJuaViKoB/dBjxLw6nbE53Bm/eUuZPdnnZ1+ZUIh8jfaRKC5MzMjgnLGQTweGXVnfIrhyxtQ==", "requires": { "prismjs": "1.29.0" } @@ -19300,13 +17968,13 @@ "requires": {} }, "@react-email/components": { - "version": "0.0.24", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz", - "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==", + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.25.tgz", + "integrity": "sha512-lnfVVrThEcET5NPoeaXvrz9UxtWpGRcut2a07dLbyKgNbP7vj/cXTI5TuHtanCvhCddFpMDnElNRghDOfPzwUg==", "requires": { "@react-email/body": "0.0.10", "@react-email/button": "0.0.17", - "@react-email/code-block": "0.0.8", + "@react-email/code-block": "0.0.9", "@react-email/code-inline": "0.0.4", "@react-email/column": "0.0.12", "@react-email/container": "0.0.14", @@ -19448,114 +18116,114 @@ } }, "@rollup/rollup-android-arm-eabi": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", - "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", - "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", - "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", - "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", - "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", - "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", - "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", - "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", - "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", - "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", - "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", - "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", - "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", - "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", - "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "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==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", - "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "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==", "dev": true, "optional": true }, @@ -19607,92 +18275,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.21.tgz", - "integrity": "sha512-7/cN0SZ+y2V6e0hsDD8koGR0QVh7Jl3r756bwaHLLSN+kReoUb/yVcLsA8iTn90JLME3DkQK4CPjxDCQiyMXNg==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.26.tgz", + "integrity": "sha512-f5uYFf+TmMQyYIoxkn/evWhNGuUzC730dFwAKGwBVHHVoPyak1/GvJUm6i1SKl+2Hrj9oN0i3WSoWWZ4pgI8lw==", "devOptional": true, "requires": { - "@swc/core-darwin-arm64": "1.7.21", - "@swc/core-darwin-x64": "1.7.21", - "@swc/core-linux-arm-gnueabihf": "1.7.21", - "@swc/core-linux-arm64-gnu": "1.7.21", - "@swc/core-linux-arm64-musl": "1.7.21", - "@swc/core-linux-x64-gnu": "1.7.21", - "@swc/core-linux-x64-musl": "1.7.21", - "@swc/core-win32-arm64-msvc": "1.7.21", - "@swc/core-win32-ia32-msvc": "1.7.21", - "@swc/core-win32-x64-msvc": "1.7.21", + "@swc/core-darwin-arm64": "1.7.26", + "@swc/core-darwin-x64": "1.7.26", + "@swc/core-linux-arm-gnueabihf": "1.7.26", + "@swc/core-linux-arm64-gnu": "1.7.26", + "@swc/core-linux-arm64-musl": "1.7.26", + "@swc/core-linux-x64-gnu": "1.7.26", + "@swc/core-linux-x64-musl": "1.7.26", + "@swc/core-win32-arm64-msvc": "1.7.26", + "@swc/core-win32-ia32-msvc": "1.7.26", + "@swc/core-win32-x64-msvc": "1.7.26", "@swc/counter": "^0.1.3", "@swc/types": "^0.1.12" } }, "@swc/core-darwin-arm64": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.21.tgz", - "integrity": "sha512-hh5uOZ7jWF66z2TRMhhXtWMQkssuPCSIZPy9VHf5KvZ46cX+5UeECDthchYklEVZQyy4Qr6oxfh4qff/5spoMA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.26.tgz", + "integrity": "sha512-FF3CRYTg6a7ZVW4yT9mesxoVVZTrcSWtmZhxKCYJX9brH4CS/7PRPjAKNk6kzWgWuRoglP7hkjQcd6EpMcZEAw==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.21.tgz", - "integrity": "sha512-lTsPquqSierQ6jWiWM7NnYXXZGk9zx3NGkPLHjPbcH5BmyiauX0CC/YJYJx7YmS2InRLyALlGmidHkaF4JY28A==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.26.tgz", + "integrity": "sha512-az3cibZdsay2HNKmc4bjf62QVukuiMRh5sfM5kHR/JMTrLyS6vSw7Ihs3UTkZjUxkLTT8ro54LI6sV6sUQUbLQ==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.21.tgz", - "integrity": "sha512-AgSd0fnSzAqCvWpzzZCq75z62JVGUkkXEOpfdi99jj/tryPy38KdXJtkVWJmufPXlRHokGTBitalk33WDJwsbA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.26.tgz", + "integrity": "sha512-VYPFVJDO5zT5U3RpCdHE5v1gz4mmR8BfHecUZTmD2v1JeFY6fv9KArJUpjrHEEsjK/ucXkQFmJ0jaiWXmpOV9Q==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.21.tgz", - "integrity": "sha512-l+jw6RQ4Y43/8dIst0c73uQE+W3kCWrCFqMqC/xIuE/iqHOnvYK6YbA1ffOct2dImkHzNiKuoehGqtQAc6cNaQ==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.26.tgz", + "integrity": "sha512-YKevOV7abpjcAzXrhsl+W48Z9mZvgoVs2eP5nY+uoMAdP2b3GxC0Df1Co0I90o2lkzO4jYBpTMcZlmUXLdXn+Q==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.21.tgz", - "integrity": "sha512-29KKZXrTo/c9F1JFL9WsNvCa6UCdIVhHP5EfuYhlKbn5/YmSsNFkuHdUtZFEd5U4+jiShXDmgGCtLW2d08LIwg==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.26.tgz", + "integrity": "sha512-3w8iZICMkQQON0uIcvz7+Q1MPOW6hJ4O5ETjA0LSP/tuKqx30hIniCGOgPDnv3UTMruLUnQbtBwVCZTBKR3Rkg==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.21.tgz", - "integrity": "sha512-HsP3JwddvQj5HvnjmOr+Bd5plEm6ccpfP5wUlm3hywzvdVkj+yR29bmD7UwpV/1zCQ60Ry35a7mXhKI6HQxFgw==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.26.tgz", + "integrity": "sha512-c+pp9Zkk2lqb06bNGkR2Looxrs7FtGDMA4/aHjZcCqATgp348hOKH5WPvNLBl+yPrISuWjbKDVn3NgAvfvpH4w==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.21.tgz", - "integrity": "sha512-hYKLVeUTHqvFK628DFJEwxoX6p42T3HaQ4QjNtf3oKhiJWFh9iTRUrN/oCB5YI3R9WMkFkKh+99gZ/Dd0T5lsg==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.26.tgz", + "integrity": "sha512-PgtyfHBF6xG87dUSSdTJHwZ3/8vWZfNIXQV2GlwEpslrOkGqy+WaiiyE7Of7z9AvDILfBBBcJvJ/r8u980wAfQ==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.21.tgz", - "integrity": "sha512-qyWAKW10aMBe6iUqeZ7NAJIswjfggVTUpDINpQGUJhz+pR71YZDidXgZXpaDB84YyDB2JAlRqd1YrLkl7CMiIw==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.26.tgz", + "integrity": "sha512-9TNXPIJqFynlAOrRD6tUQjMq7KApSklK3R/tXgIxc7Qx+lWu8hlDQ/kVPLpU7PWvMMwC/3hKBW+p5f+Tms1hmA==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.21.tgz", - "integrity": "sha512-cy61wS3wgH5mEwBiQ5w6/FnQrchBDAdPsSh0dKSzNmI+4K8hDxS8uzdBycWqJXO0cc+mA77SIlwZC3hP3Kum2g==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.26.tgz", + "integrity": "sha512-9YngxNcG3177GYdsTum4V98Re+TlCeJEP4kEwEg9EagT5s3YejYdKwVAkAsJszzkXuyRDdnHUpYbTrPG6FiXrQ==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.7.21", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.21.tgz", - "integrity": "sha512-/rexGItJURNJOdae+a48M+loT74nsEU+PyRRVAkZMKNRtLoYFAr0cpDlS5FodIgGunp/nqM0bst4H2w6Y05IKA==", + "version": "1.7.26", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.26.tgz", + "integrity": "sha512-VR+hzg9XqucgLjXxA13MtV5O3C0bK0ywtLIBw/+a+O+Oc6mxFWHtdUeXDbIi5AiPbn0fjgVJMqYnyjGyyX8u0w==", "dev": true, "optional": true }, @@ -19720,12 +18388,12 @@ } }, "@testcontainers/postgresql": { - "version": "10.12.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.12.0.tgz", - "integrity": "sha512-n0Q0Btx0R923CDgm6KBXbesPH10CewpuuCPcnmEZzon3IneMzdk4UqVhhNNOeJFDGhtFrZBOoJ1o7CUI4J0vQw==", + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.13.1.tgz", + "integrity": "sha512-HAh/3uLAzAhOmzXsOE6hVxkvetczPnX/Zoyt+SgK7QotW98Npr1MDx8OKiaLGTJ8XkIvVvS4Ch6bl+frt4pnkQ==", "dev": true, "requires": { - "testcontainers": "^10.12.0" + "testcontainers": "^10.13.1" } }, "@tsconfig/node10": { @@ -19757,25 +18425,34 @@ "peer": true }, "@turf/boolean-point-in-polygon": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-6.5.0.tgz", - "integrity": "sha512-DtSuVFB26SI+hj0SjrvXowGTUCHlgevPAIsukssW6BG5MlNSBQAo70wpICBNJL6RjukXg8d2eXaAWuD/CqL00A==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.1.0.tgz", + "integrity": "sha512-mprVsyIQ+ijWTZwbnO4Jhxu94ZW2M2CheqLiRTsGJy0Ooay9v6Av5/Nl3/Gst7ZVXxPqMeMaFYkSzcTc87AKew==", "requires": { - "@turf/helpers": "^6.5.0", - "@turf/invariant": "^6.5.0" + "@turf/helpers": "^7.1.0", + "@turf/invariant": "^7.1.0", + "@types/geojson": "^7946.0.10", + "point-in-polygon-hao": "^1.1.0", + "tslib": "^2.6.2" } }, "@turf/helpers": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-6.5.0.tgz", - "integrity": "sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==" + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.1.0.tgz", + "integrity": "sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==", + "requires": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.6.2" + } }, "@turf/invariant": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-6.5.0.tgz", - "integrity": "sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.1.0.tgz", + "integrity": "sha512-OCLNqkItBYIP1nE9lJGuIUatWGtQ4rhBKAyTfFu0z8npVzGEYzvguEeof8/6LkKmTTEHW53tCjoEhSSzdRh08Q==", "requires": { - "@turf/helpers": "^6.5.0" + "@turf/helpers": "^7.1.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.6.2" } }, "@types/archiver": { @@ -19794,9 +18471,9 @@ "dev": true }, "@types/aws-lambda": { - "version": "8.10.122", - "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.122.tgz", - "integrity": "sha512-vBkIh9AY22kVOCEKo5CJlyCgmSWvasC+SWUxL/x/vOwRobMpI/HG1xp/Ae3AqmSiZeLUbOhW0FCD3ZjqqUxmXw==" + "version": "8.10.143", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.143.tgz", + "integrity": "sha512-u5vzlcR14ge/4pMTTMDQr3MF0wEe38B2F9o84uC4F43vN5DGTy63npRrB6jQhyt+C0lGv4ZfiRcRkqJoZuPnmg==" }, "@types/bcrypt": { "version": "5.0.2", @@ -19887,21 +18564,13 @@ "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", "dev": true, + "optional": true, + "peer": true, "requires": { "@types/estree": "*", "@types/json-schema": "*" } }, - "@types/eslint-scope": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", - "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", - "dev": true, - "requires": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -19941,6 +18610,11 @@ "@types/node": "*" } }, + "@types/geojson": { + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" + }, "@types/http-errors": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz", @@ -20019,25 +18693,25 @@ } }, "@types/mysql": { - "version": "2.15.22", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.22.tgz", - "integrity": "sha512-wK1pzsJVVAjYCSZWQoWHziQZbNggXFDUEIGf54g4ZM/ERuP86uGdWeKZWMYlqTPMZfHJJvLPyogXGvCOg87yLQ==", + "version": "2.15.26", + "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.26.tgz", + "integrity": "sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==", "requires": { "@types/node": "*" } }, "@types/node": { - "version": "20.16.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz", - "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==", + "version": "20.16.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.5.tgz", + "integrity": "sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==", "requires": { "undici-types": "~6.19.2" } }, "@types/nodemailer": { - "version": "6.4.15", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.15.tgz", - "integrity": "sha512-0EBJxawVNjPkng1zm2vopRctuWVCxk34JcIlRuXSf54habUWdz1FB7wHDqOqvDa8Mtpt0Q3LTXQkAs2LNyK5jQ==", + "version": "6.4.16", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", + "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", "dev": true, "requires": { "@types/node": "*" @@ -20050,9 +18724,9 @@ "dev": true }, "@types/pg": { - "version": "8.10.9", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz", - "integrity": "sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==", + "version": "8.11.10", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.10.tgz", + "integrity": "sha512-LczQUW4dbOQzsH2RQ5qoeJ6qJPdrcM/DcMLoqWQkMLMsq83J5lAX3LXjdkWdpscFy67JSOWDnh7Ny/sPFykmkg==", "requires": { "@types/node": "*", "pg-protocol": "*", @@ -20060,15 +18734,15 @@ }, "dependencies": { "pg-types": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.1.tgz", - "integrity": "sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz", + "integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==", "requires": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", - "postgres-date": "~2.0.1", + "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } @@ -20087,9 +18761,9 @@ } }, "postgres-date": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.0.1.tgz", - "integrity": "sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz", + "integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==" }, "postgres-interval": { "version": "3.0.0", @@ -20099,9 +18773,9 @@ } }, "@types/pg-pool": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.4.tgz", - "integrity": "sha512-qZAvkv1K3QbmHHFYSNRYPkRjOWRLBYrL4B9c+wG0GSVGBw0NtJwPcgx/DSddeDJvRGMHCEQ4VMEVfuJ/0gZ3XQ==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/pg-pool/-/pg-pool-2.0.6.tgz", + "integrity": "sha512-TaAUE5rq2VQYxab5Ts7WZhKNmuN78Q6PiFonTDdpbx8a1H0M1vhy3rhiMjl+e2iHmogyMw7jZF4FrE6eJUy5HQ==", "requires": { "@types/pg": "*" } @@ -20131,9 +18805,9 @@ "dev": true }, "@types/react": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", - "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "version": "18.3.8", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.8.tgz", + "integrity": "sha512-syBUrW3/XpnW4WJ41Pft+I+aPoDVbrBVQGEnbD7NijDGlVC+8gV/XKRY+7vMDlfPpbwYt0l1vd/Sj8bJGMbs9Q==", "dev": true, "requires": { "@types/prop-types": "*", @@ -20250,16 +18924,16 @@ "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "@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, "requires": { "@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", @@ -20267,54 +18941,54 @@ } }, "@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, "requires": { - "@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" } }, "@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, "requires": { - "@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" } }, "@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, "requires": { - "@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" } }, "@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 }, "@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, "requires": { - "@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", @@ -20344,41 +19018,41 @@ } }, "@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, "requires": { "@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" } }, "@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, "requires": { - "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/types": "8.6.0", "eslint-visitor-keys": "^3.4.3" } }, "@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, "requires": { "@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", @@ -20397,44 +19071,66 @@ } }, "@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, "requires": { - "@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" } }, + "@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, + "requires": { + "@vitest/spy": "^2.1.0-beta.1", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.11" + }, + "dependencies": { + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + } + } + }, "@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==", + "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, "requires": { "tinyrainbow": "^1.2.0" } }, "@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, "requires": { - "@vitest/utils": "2.0.5", + "@vitest/utils": "2.1.1", "pathe": "^1.1.2" } }, "@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, "requires": { - "@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" }, "dependencies": { @@ -20450,22 +19146,21 @@ } }, "@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, "requires": { "tinyspy": "^3.0.0" } }, "@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, "requires": { - "@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" } @@ -21031,9 +19726,9 @@ } }, "body-parser": { - "version": "1.20.2", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", - "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "requires": { "bytes": "3.1.2", "content-type": "~1.0.5", @@ -21043,7 +19738,7 @@ "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.11.0", + "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" @@ -21284,9 +19979,9 @@ "dev": true }, "cjs-module-lexer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", - "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==" + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz", + "integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==" }, "class-transformer": { "version": "0.5.1", @@ -21705,11 +20400,11 @@ "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==" }, "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==", "requires": { - "ms": "2.1.2" + "ms": "^2.1.3" } }, "deep-eql": { @@ -21966,9 +20661,9 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" }, "end-of-stream": { "version": "1.4.4", @@ -21980,9 +20675,9 @@ } }, "engine.io": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.2.tgz", - "integrity": "sha512-IXsMcGpw/xRfjra46sVZVHiSWo/nJ/3g1337q9KNXtS6YRzbW5yIzTCb9DjhrBe7r3GZQR0I4+nq+4ODk5g/cA==", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", "requires": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", @@ -21993,7 +20688,7 @@ "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", - "ws": "~8.11.0" + "ws": "~8.17.1" } }, "engine.io-parser": { @@ -22002,9 +20697,9 @@ "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==" }, "enhanced-resolve": { - "version": "5.17.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", - "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -22044,34 +20739,34 @@ "dev": true }, "esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "requires": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "escalade": { @@ -22091,16 +20786,17 @@ "dev": true }, "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, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@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", @@ -22123,7 +20819,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", @@ -22308,46 +21003,6 @@ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, - "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, - "requires": { - "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" - }, - "dependencies": { - "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 - }, - "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 - }, - "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, - "requires": { - "mimic-fn": "^4.0.0" - } - } - } - }, "exiftool-vendored": { "version": "28.2.1", "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", @@ -22375,36 +21030,36 @@ "optional": true }, "express": { - "version": "4.19.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", - "integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==", + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.0.tgz", + "integrity": "sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==", "requires": { "accepts": "~1.3.8", "array-flatten": "1.1.1", - "body-parser": "1.20.2", + "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.6.0", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", - "finalhandler": "1.2.0", + "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", + "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", + "path-to-regexp": "0.1.10", "proxy-addr": "~2.0.7", - "qs": "6.11.0", + "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", + "send": "0.19.0", + "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", @@ -22431,9 +21086,9 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", + "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" } } }, @@ -22547,12 +21202,12 @@ } }, "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", "requires": { "debug": "2.6.9", - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", @@ -22794,12 +21449,12 @@ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "geo-tz": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.0.2.tgz", - "integrity": "sha512-NjEzJBzaMhO9C7lFZIsWDkVED7aLxcES3iEZOWJ97dhnDUGhEB8vhW7MaWR+2y4aWvtFV/VyuDi8Y0rUHvm4tw==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.1.1.tgz", + "integrity": "sha512-V6FEJ9UQOHnBD7eAOkJG3gZlc0LjKckRjt56cit6MLKMPF2qQIUBDYb0iUj4kw8l+WUeAu9ytPdSPpcLEELWtw==", "requires": { - "@turf/boolean-point-in-polygon": "^6.5.0", - "@turf/helpers": "^6.5.0", + "@turf/boolean-point-in-polygon": "^7.1.0", + "@turf/helpers": "^7.1.0", "geobuf": "^3.0.2", "pbf": "^3.2.1" } @@ -22849,12 +21504,6 @@ "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", "dev": true }, - "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 - }, "glob": { "version": "10.4.2", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.2.tgz", @@ -23075,16 +21724,10 @@ "debug": "4" } }, - "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 - }, "i18n-iso-countries": { - "version": "7.11.3", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz", - "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.12.0.tgz", + "integrity": "sha512-NDFf5j/raA5JrcPT/NcHP3RUMH7TkdkxQKAKdvDlgb+MS296WJzzqvV0Y5uwavSm7A6oYvBeSV0AxoHdDiHIiw==", "requires": { "diacritics": "1.3.0" } @@ -23118,9 +21761,9 @@ } }, "import-in-the-middle": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.8.0.tgz", - "integrity": "sha512-/xQjze8szLNnJ5rvHSzn+dcVXqCAU6Plbk4P24U/jwPmg1wy7IIp9OjKIO5tYue8GSPhDpPDiApQjvBUmWwhsQ==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-1.11.0.tgz", + "integrity": "sha512-5DimNQGoe0pLUHbR9qK84iWaWjjbsxiqXnw6Qz64+azRgleqv9k2kTt5fw7QsOpmaGYtuxxursnPPsnTKEx10Q==", "requires": { "acorn": "^8.8.2", "acorn-import-attributes": "^1.9.5", @@ -23368,9 +22011,9 @@ } }, "jose": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", - "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==" }, "js-beautify": { "version": "1.15.1", @@ -23712,9 +22355,9 @@ } }, "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==" }, "merge-stream": { "version": "2.0.0", @@ -23733,11 +22376,11 @@ "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" }, "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "requires": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "dependencies": { @@ -23856,9 +22499,9 @@ "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==" }, "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "msgpackr": { "version": "1.10.1", @@ -24112,9 +22755,9 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" }, "nodemailer": { - "version": "6.9.14", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.14.tgz", - "integrity": "sha512-Dobp/ebDKBvz91sbtRKhcznLThrKxKt97GI2FAlAyy+fk19j73Uz3sBXolVtmcXjaorivqsbbbjDY+Jkt4/bQA==" + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.15.tgz", + "integrity": "sha512-AHf04ySLC6CIfuRtRiEYtGEXgRfa6INgWGluDhnxTZhHSKvrBu7lc1VVchQ0d8nPc4cFaZoPq8vkyNoZr0TpGQ==" }, "nopt": { "version": "5.0.0", @@ -24154,23 +22797,6 @@ "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", "integrity": "sha512-TKC/8zH5pXIAMVQio2TvVDTtPRX+DJPHDqjRbxogtFiByHyzKmy96RA0JtCQJ+WouyyL4A10xomQzgbUT+1jCg==" }, - "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, - "requires": { - "path-key": "^4.0.0" - }, - "dependencies": { - "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 - } - } - }, "npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -24193,9 +22819,9 @@ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" }, "object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==" }, "obuf": { "version": "1.1.2", @@ -24237,11 +22863,11 @@ } }, "openid-client": { - "version": "5.6.5", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.5.tgz", - "integrity": "sha512-5P4qO9nGJzB5PI0LFlhj4Dzg3m4odt0qsJTfyEtZyOlkgpILwEioOhVVJOrS1iVH494S4Ee5OCjjg6Bf5WOj3w==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.0.tgz", + "integrity": "sha512-4GCCGZt1i2kTHpwvaC/sCpTpQqDnBzDzuJcJMbH+y1Q5qI8U8RBvoSh28svarXszZHR5BAMXbJPX1PGPRE3VOA==", "requires": { - "jose": "^4.15.5", + "jose": "^4.15.9", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -24431,9 +23057,9 @@ } }, "path-to-regexp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.2.0.tgz", - "integrity": "sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA==" + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==" }, "path-type": { "version": "4.0.0", @@ -24467,14 +23093,14 @@ "integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==" }, "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==", "requires": { "pg-cloudflare": "^1.1.1", - "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" } @@ -24486,9 +23112,9 @@ "optional": true }, "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==" + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz", + "integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==" }, "pg-int8": { "version": "1.0.1", @@ -24501,15 +23127,15 @@ "integrity": "sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==" }, "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==", "requires": {} }, "pg-protocol": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.1.tgz", - "integrity": "sha512-jPIlvgoD63hrEuihvIg+tJhoGjUsLPn6poJY9N5CnlPd91c2T18T/9zBtLxZSb1EhYxBRoZJtzScCaWlYLtktg==" + "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==" }, "pg-types": { "version": "2.2.0", @@ -24532,9 +23158,9 @@ } }, "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "picomatch": { "version": "4.0.2", @@ -24559,14 +23185,19 @@ "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", "dev": true }, + "point-in-polygon-hao": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.1.0.tgz", + "integrity": "sha512-3hTIM2j/v9Lio+wOyur3kckD4NxruZhpowUbEgmyikW+a2Kppjtu1eN+AhnMQtoHW46zld88JiYWv6fxpsDrTQ==" + }, "postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "requires": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" } }, "postcss-import": { @@ -24656,9 +23287,9 @@ } }, "postgres-range": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.3.tgz", - "integrity": "sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==" + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/postgres-range/-/postgres-range-1.1.4.tgz", + "integrity": "sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==" }, "prelude-ls": { "version": "1.2.1", @@ -24793,11 +23424,11 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "requires": { - "side-channel": "^1.0.4" + "side-channel": "^1.0.6" } }, "queue-microtask": { @@ -25455,27 +24086,27 @@ } }, "rollup": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", - "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", "dev": true, "requires": { - "@rollup/rollup-android-arm-eabi": "4.14.3", - "@rollup/rollup-android-arm64": "4.14.3", - "@rollup/rollup-darwin-arm64": "4.14.3", - "@rollup/rollup-darwin-x64": "4.14.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", - "@rollup/rollup-linux-arm-musleabihf": "4.14.3", - "@rollup/rollup-linux-arm64-gnu": "4.14.3", - "@rollup/rollup-linux-arm64-musl": "4.14.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", - "@rollup/rollup-linux-riscv64-gnu": "4.14.3", - "@rollup/rollup-linux-s390x-gnu": "4.14.3", - "@rollup/rollup-linux-x64-gnu": "4.14.3", - "@rollup/rollup-linux-x64-musl": "4.14.3", - "@rollup/rollup-win32-arm64-msvc": "4.14.3", - "@rollup/rollup-win32-ia32-msvc": "4.14.3", - "@rollup/rollup-win32-x64-msvc": "4.14.3", + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", "@types/estree": "1.0.5", "fsevents": "~2.3.2" } @@ -25580,9 +24211,9 @@ "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "requires": { "debug": "2.6.9", "depd": "2.0.0", @@ -25614,10 +24245,10 @@ } } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" } } }, @@ -25631,14 +24262,14 @@ } }, "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", "requires": { - "encodeurl": "~1.0.2", + "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", - "send": "0.18.0" + "send": "0.19.0" } }, "set-blocking": { @@ -25741,13 +24372,14 @@ "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" }, "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" } }, "siginfo": { @@ -25812,14 +24444,6 @@ "requires": { "debug": "~4.3.4", "ws": "~8.17.1" - }, - "dependencies": { - "ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "requires": {} - } } }, "socket.io-parser": { @@ -25838,9 +24462,9 @@ "dev": true }, "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==" }, "source-map-support": { "version": "0.5.21", @@ -25904,9 +24528,9 @@ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, "sql-formatter": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.1.tgz", - "integrity": "sha512-lw/G/emIJ+tVspOtOFzfD2YFFMN3MFPxGnbWl1DlJLB+fsX7X7zMqSRM1SLSn2YuaRJ0lTe7AMknHDqmIW1Y8w==", + "version": "15.4.2", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.2.tgz", + "integrity": "sha512-Pw4aAgfuyml/SHMlhbJhyOv+GR+Z1HNb9sgX3CVBVdN5YNM+v2VWkYJ3NNbYS7cu37GY3vP/PgnwoVynCuXRxg==", "dev": true, "requires": { "argparse": "^2.0.1", @@ -26023,12 +24647,6 @@ "ansi-regex": "^5.0.1" } }, - "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 - }, "strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -26181,9 +24799,9 @@ "dev": true }, "tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "requires": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -26319,9 +24937,9 @@ } }, "testcontainers": { - "version": "10.13.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.0.tgz", - "integrity": "sha512-SDblQvirbJw1ZpenxaAairGtAesw5XMOCHLbRhTTUBJtBkZJGce8Vx/I8lXQxWIM8HRXsg3HILTHGQvYo4x7wQ==", + "version": "10.13.1", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.13.1.tgz", + "integrity": "sha512-JBbOhxmygj/ouH/47GnoVNt+c55Telh/45IjVxEbDoswsLchVmJiuKiw/eF6lE5i7LN+/99xsrSCttI3YRtirg==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", @@ -26395,15 +25013,21 @@ "integrity": "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg==" }, "tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "tinyexec": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.0.tgz", + "integrity": "sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==", "dev": true }, "tinypool": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", - "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", + "integrity": "sha512-URZYihUbRPcGv95En+sz6MfghfIc2OJ1sv/RmhWZLouPY0/8Vo80viwPvg3dlaS9fuq7fQMEfgRRK7BBZThBEA==", "dev": true }, "tinyrainbow": { @@ -26413,9 +25037,9 @@ "dev": true }, "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 }, "tmp": { @@ -26647,15 +25271,15 @@ } }, "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==", "devOptional": true }, "ua-parser-js": { - "version": "1.0.38", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", - "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==" + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.39.tgz", + "integrity": "sha512-k24RCVWlEcjkdOxYmVJgeD/0a1TiSpqLg+ZalVGV9lsnr4yqu0w7tX/x2xX6G4zpkgQnRf89lxuZ1wsbjXM8lw==" }, "uglify-js": { "version": "3.17.4", @@ -26808,27 +25432,26 @@ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" }, "vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.7.tgz", + "integrity": "sha512-5l2zxqMEPVENgvzTuBpHer2awaetimj2BGkhBPdnwKbPNOlHsODU+oiazEZzLK7KhAnOrO+XGYJYn4ZlUhDtDQ==", "dev": true, "requires": { - "esbuild": "^0.20.1", + "esbuild": "^0.21.3", "fsevents": "~2.3.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" } }, "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, "requires": { "cac": "^6.7.14", - "debug": "^4.3.5", + "debug": "^4.3.6", "pathe": "^1.1.2", - "tinyrainbow": "^1.2.0", "vite": "^5.0.0" } }, @@ -26844,29 +25467,29 @@ } }, "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, "requires": { - "@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" }, "dependencies": { @@ -26905,12 +25528,11 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.93.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", - "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", + "version": "5.94.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz", + "integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==", "dev": true, "requires": { - "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", "@webassemblyjs/ast": "^1.12.1", "@webassemblyjs/wasm-edit": "^1.12.1", @@ -26919,7 +25541,7 @@ "acorn-import-attributes": "^1.9.5", "browserslist": "^4.21.10", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.0", + "enhanced-resolve": "^5.17.1", "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", @@ -27038,9 +25660,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "ws": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", - "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "requires": {} }, "xtend": { diff --git a/server/package.json b/server/package.json index 48e873a8f84c6..d4816109069e9 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.114.0", + "version": "1.116.2", "description": "", "author": "", "private": true, @@ -46,11 +46,11 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.49.0", + "@opentelemetry/auto-instrumentations-node": "^0.50.0", "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.53.0", "@opentelemetry/sdk-node": "^0.53.0", - "@react-email/components": "^0.0.24", + "@react-email/components": "^0.0.25", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/react": "^18.3.4", diff --git a/server/src/config.ts b/server/src/config.ts index 057c9a69e213c..53374d581f6ba 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -7,82 +7,20 @@ import { RedisOptions } from 'ioredis'; import Joi, { Root } from 'joi'; import { CLS_ID, ClsModuleOptions } from 'nestjs-cls'; import { ImmichHeader } from 'src/dtos/auth.dto'; +import { + AudioCodec, + Colorspace, + CQMode, + ImageFormat, + LogLevel, + ToneMapping, + TranscodeHWAccel, + TranscodePolicy, + VideoCodec, + VideoContainer, +} from 'src/enum'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; - -export enum TranscodePolicy { - ALL = 'all', - OPTIMAL = 'optimal', - BITRATE = 'bitrate', - REQUIRED = 'required', - DISABLED = 'disabled', -} - -export enum TranscodeTarget { - NONE, - AUDIO, - VIDEO, - ALL, -} - -export enum VideoCodec { - H264 = 'h264', - HEVC = 'hevc', - VP9 = 'vp9', - AV1 = 'av1', -} - -export enum AudioCodec { - MP3 = 'mp3', - AAC = 'aac', - LIBOPUS = 'libopus', -} - -export enum VideoContainer { - MOV = 'mov', - MP4 = 'mp4', - OGG = 'ogg', - WEBM = 'webm', -} - -export enum TranscodeHWAccel { - NVENC = 'nvenc', - QSV = 'qsv', - VAAPI = 'vaapi', - RKMPP = 'rkmpp', - DISABLED = 'disabled', -} - -export enum ToneMapping { - HABLE = 'hable', - MOBIUS = 'mobius', - REINHARD = 'reinhard', - DISABLED = 'disabled', -} - -export enum CQMode { - AUTO = 'auto', - CQP = 'cqp', - ICQ = 'icq', -} - -export enum Colorspace { - SRGB = 'srgb', - P3 = 'p3', -} - -export enum ImageFormat { - JPEG = 'jpeg', - WEBP = 'webp', -} - -export enum LogLevel { - VERBOSE = 'verbose', - DEBUG = 'debug', - LOG = 'log', - WARN = 'warn', - ERROR = 'error', - FATAL = 'fatal', -} +import { ImageOptions } from 'src/interfaces/media.interface'; export interface SystemConfig { ffmpeg: { @@ -172,11 +110,8 @@ export interface SystemConfig { template: string; }; image: { - thumbnailFormat: ImageFormat; - thumbnailSize: number; - previewFormat: ImageFormat; - previewSize: number; - quality: number; + thumbnail: ImageOptions; + preview: ImageOptions; colorspace: Colorspace; extractEmbedded: boolean; }; @@ -285,8 +220,8 @@ export const defaults = Object.freeze({ }, map: { enabled: true, - lightStyle: '', - darkStyle: '', + lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', + darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', }, reverseGeocoding: { enabled: true, @@ -322,11 +257,16 @@ export const defaults = Object.freeze({ template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, image: { - thumbnailFormat: ImageFormat.WEBP, - thumbnailSize: 250, - previewFormat: ImageFormat.JPEG, - previewSize: 1440, - quality: 80, + thumbnail: { + format: ImageFormat.WEBP, + size: 250, + quality: 80, + }, + preview: { + format: ImageFormat.JPEG, + size: 1440, + quality: 80, + }, colorspace: Colorspace.P3, extractEmbedded: false, }, diff --git a/server/src/constants.ts b/server/src/constants.ts index 6cfcc41d89ba6..e0a4fe8cef306 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -54,11 +54,6 @@ export const resourcePaths = { export const MOBILE_REDIRECT = 'app.immich:///oauth-callback'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; -export enum AuthType { - PASSWORD = 'password', - OAUTH = 'oauth', -} - export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico']; export const FACE_THUMBNAIL_SIZE = 250; diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index fb5ec58f2544c..b2d3933be4cbc 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -33,16 +33,17 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto'; +import { RouteKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; -import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor'; +import { FileUploadInterceptor, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor'; import { AssetMediaService } from 'src/services/asset-media.service'; import { sendFile } from 'src/utils/file'; import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; @ApiTags('Assets') -@Controller(Route.ASSET) +@Controller(RouteKey.ASSET) export class AssetMediaController { constructor( @Inject(ILoggerRepository) private logger: ILoggerRepository, diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index c6fdac1710edc..8a5b5fb0b63a8 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -1,5 +1,6 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { EndpointLifecycle } from 'src/decorators'; import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto'; import { AssetBulkDeleteDto, @@ -13,13 +14,13 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; +import { RouteKey } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; -import { Route } from 'src/middleware/file-upload.interceptor'; import { AssetService } from 'src/services/asset.service'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Assets') -@Controller(Route.ASSET) +@Controller(RouteKey.ASSET) export class AssetController { constructor(private service: AssetService) {} @@ -31,6 +32,7 @@ export class AssetController { @Get('random') @Authenticated() + @EndpointLifecycle({ deprecatedAt: 'v1.116.0' }) getRandom(@Auth() auth: AuthDto, @Query() dto: RandomAssetsDto): Promise { return this.service.getRandom(auth, dto.count ?? 1); } diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts index 7dcef9df5f391..04250f530044f 100644 --- a/server/src/controllers/auth.controller.ts +++ b/server/src/controllers/auth.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, HttpCode, HttpStatus, Post, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { AuthType } from 'src/constants'; import { AuthDto, ChangePasswordDto, @@ -13,6 +12,7 @@ import { ValidateAccessTokenResponseDto, } from 'src/dtos/auth.dto'; import { UserAdminResponseDto } from 'src/dtos/user.dto'; +import { AuthType } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { respondWithCookie, respondWithoutCookie } from 'src/utils/response'; diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts index 2aa5920fab7b8..7da19e207fce0 100644 --- a/server/src/controllers/job.controller.ts +++ b/server/src/controllers/job.controller.ts @@ -1,6 +1,6 @@ -import { Body, Controller, Get, Param, Put } from '@nestjs/common'; +import { Body, Controller, Get, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; +import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto'; import { Authenticated } from 'src/middleware/auth.guard'; import { JobService } from 'src/services/job.service'; @@ -15,6 +15,12 @@ export class JobController { return this.service.getAllJobsStatus(); } + @Post() + @Authenticated({ admin: true }) + createJob(@Body() dto: JobCreateDto): Promise { + return this.service.create(dto); + } + @Put(':id') @Authenticated({ admin: true }) sendJobCommand(@Param() { id }: JobIdParamDto, @Body() dto: JobCommandDto): Promise { diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index a45617fc2a503..b8959ca28875c 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -4,7 +4,6 @@ import { CreateLibraryDto, LibraryResponseDto, LibraryStatsResponseDto, - ScanLibraryDto, UpdateLibraryDto, ValidateLibraryDto, ValidateLibraryResponseDto, @@ -43,6 +42,13 @@ export class LibraryController { return this.service.update(id, dto); } + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) + deleteLibrary(@Param() { id }: UUIDParamDto): Promise { + return this.service.delete(id); + } + @Post(':id/validate') @HttpCode(200) @Authenticated({ admin: true }) @@ -51,13 +57,6 @@ export class LibraryController { return this.service.validate(id, dto); } - @Delete(':id') - @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) - deleteLibrary(@Param() { id }: UUIDParamDto): Promise { - return this.service.delete(id); - } - @Get(':id/statistics') @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { @@ -66,15 +65,8 @@ export class LibraryController { @Post(':id/scan') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ admin: true }) - scanLibrary(@Param() { id }: UUIDParamDto, @Body() dto: ScanLibraryDto) { - return this.service.queueScan(id, dto); - } - - @Post(':id/removeOffline') - @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ admin: true }) - removeOfflineFiles(@Param() { id }: UUIDParamDto) { - return this.service.queueRemoveOffline(id); + @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) + scanLibrary(@Param() { id }: UUIDParamDto) { + return this.service.queueScan(id); } } diff --git a/server/src/controllers/map.controller.ts b/server/src/controllers/map.controller.ts index d6c26c58a073c..88104e6b588d8 100644 --- a/server/src/controllers/map.controller.ts +++ b/server/src/controllers/map.controller.ts @@ -7,7 +7,6 @@ import { MapReverseGeocodeDto, MapReverseGeocodeResponseDto, } from 'src/dtos/map.dto'; -import { MapThemeDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MapService } from 'src/services/map.service'; @@ -22,12 +21,6 @@ export class MapController { return this.service.getMapMarkers(auth, options); } - @Authenticated({ sharedLink: true }) - @Get('style.json') - getMapStyle(@Query() dto: MapThemeDto) { - return this.service.getMapStyle(dto.theme); - } - @Authenticated() @Get('reverse-geocode') @HttpCode(HttpStatus.OK) diff --git a/server/src/controllers/notification.controller.ts b/server/src/controllers/notification.controller.ts index 2772e93b5d816..3dd72dd73a91d 100644 --- a/server/src/controllers/notification.controller.ts +++ b/server/src/controllers/notification.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; +import { TestEmailResponseDto } from 'src/dtos/notification.dto'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { NotificationService } from 'src/services/notification.service'; @@ -13,7 +14,7 @@ export class NotificationController { @Post('test-email') @HttpCode(HttpStatus.OK) @Authenticated({ admin: true }) - sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto) { + sendTestEmail(@Auth() auth: AuthDto, @Body() dto: SystemConfigSmtpDto): Promise { return this.service.sendTestEmail(auth.user.id, dto); } } diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts index b733dc612b227..4e626b10f01b4 100644 --- a/server/src/controllers/oauth.controller.ts +++ b/server/src/controllers/oauth.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Post, Redirect, Req, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { AuthType } from 'src/constants'; import { AuthDto, ImmichCookie, @@ -11,6 +10,7 @@ import { OAuthConfigDto, } from 'src/dtos/auth.dto'; import { UserAdminResponseDto } from 'src/dtos/user.dto'; +import { AuthType } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { respondWithCookie } from 'src/utils/response'; diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index 5b8c1eeece026..9fdb2746fc1d3 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, + RandomSearchDto, SearchExploreResponseDto, SearchPeopleDto, SearchPlacesDto, @@ -28,6 +29,13 @@ export class SearchController { return this.service.searchMetadata(auth, dto); } + @Post('random') + @HttpCode(HttpStatus.OK) + @Authenticated() + searchRandom(@Auth() auth: AuthDto, @Body() dto: RandomSearchDto): Promise { + return this.service.searchRandom(auth, dto); + } + @Post('smart') @HttpCode(HttpStatus.OK) @Authenticated() diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts index 20adbb11bbe7f..dfcdfa6ba243f 100644 --- a/server/src/controllers/trash.controller.ts +++ b/server/src/controllers/trash.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { TrashResponseDto } from 'src/dtos/trash.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TrashService } from 'src/services/trash.service'; @@ -12,23 +13,23 @@ export class TrashController { constructor(private service: TrashService) {} @Post('empty') - @HttpCode(HttpStatus.NO_CONTENT) + @HttpCode(HttpStatus.OK) @Authenticated({ permission: Permission.ASSET_DELETE }) - emptyTrash(@Auth() auth: AuthDto): Promise { + emptyTrash(@Auth() auth: AuthDto): Promise { return this.service.empty(auth); } @Post('restore') - @HttpCode(HttpStatus.NO_CONTENT) + @HttpCode(HttpStatus.OK) @Authenticated({ permission: Permission.ASSET_DELETE }) - restoreTrash(@Auth() auth: AuthDto): Promise { + restoreTrash(@Auth() auth: AuthDto): Promise { return this.service.restore(auth); } @Post('restore/assets') - @HttpCode(HttpStatus.NO_CONTENT) + @HttpCode(HttpStatus.OK) @Authenticated({ permission: Permission.ASSET_DELETE }) - restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { return this.service.restoreAssets(auth, dto); } } diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts index 01b225839080d..10076098d6516 100644 --- a/server/src/controllers/user.controller.ts +++ b/server/src/controllers/user.controller.ts @@ -21,15 +21,16 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto } from 'src/dtos/user.dto'; +import { RouteKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; -import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor'; +import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; import { UserService } from 'src/services/user.service'; import { sendFile } from 'src/utils/file'; import { UUIDParamDto } from 'src/validation'; @ApiTags('Users') -@Controller(Route.USER) +@Controller(RouteKey.USER) export class UserController { constructor( private service: UserService, diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index e20a0c658db7f..8ce8f6b67a228 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -1,12 +1,10 @@ import { randomUUID } from 'node:crypto'; import { dirname, join, resolve } from 'node:path'; -import { ImageFormat } from 'src/config'; import { APP_MEDIA_LOCATION } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { AssetFileType } from 'src/enum'; +import { AssetFileType, AssetPathType, ImageFormat, PathType, PersonPathType, StorageFolder } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -16,14 +14,6 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { getAssetFiles } from 'src/utils/asset.util'; -export enum StorageFolder { - ENCODED_VIDEO = 'encoded-video', - LIBRARY = 'library', - UPLOAD = 'upload', - PROFILE = 'profile', - THUMBNAILS = 'thumbs', -} - export const THUMBNAIL_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.THUMBNAILS)); export const ENCODED_VIDEO_DIR = resolve(join(APP_MEDIA_LOCATION, StorageFolder.ENCODED_VIDEO)); diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 7c1434004a437..8ed53344cc02f 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -120,6 +120,10 @@ export class SystemConfigCore { } } + if (config.server.externalDomain.length > 0) { + config.server.externalDomain = new URL(config.server.externalDomain).origin; + } + if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) { config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec); } diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 2316e114e885e..9b6910391af9b 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -4,8 +4,8 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; import _ from 'lodash'; import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants'; +import { MetadataKey } from 'src/enum'; import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface'; -import { Metadata } from 'src/middleware/auth.guard'; import { setUnion } from 'src/utils/set'; // PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the @@ -141,7 +141,7 @@ export type EmitConfig = { /** lower value has higher priority, defaults to 0 */ priority?: number; }; -export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config); +export const OnEmit = (config: EmitConfig) => SetMetadata(MetadataKey.ON_EMIT_CONFIG, config); type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleMetadata = { diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index e9e346c4cb593..c62857da65042 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -56,9 +56,6 @@ export class AssetMediaCreateDto extends AssetMediaBase { @ValidateBoolean({ optional: true }) isVisible?: boolean; - @ValidateBoolean({ optional: true }) - isOffline?: boolean; - @ValidateUUID({ optional: true }) livePhotoVideoId?: string; diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 02ea2c69a990e..703b1ccfe3225 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -69,8 +69,8 @@ export class UpdateAssetDto extends UpdateAssetBase { @IsString() description?: string; - @ValidateUUID({ optional: true }) - livePhotoVideoId?: string; + @ValidateUUID({ optional: true, nullable: true }) + livePhotoVideoId?: string | null; } export class RandomAssetsDto { diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts index dcace5a551213..434da46eba976 100644 --- a/server/src/dtos/audit.dto.ts +++ b/server/src/dtos/audit.dto.ts @@ -1,8 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; -import { EntityType } from 'src/enum'; +import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from 'src/enum'; import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index b7d8cf59bf55a..895f710b7a782 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty } from 'class-validator'; +import { ManualJobName } from 'src/enum'; import { JobCommand, QueueName } from 'src/interfaces/job.interface'; import { ValidateBoolean } from 'src/validation'; @@ -20,6 +21,12 @@ export class JobCommandDto { force!: boolean; } +export class JobCreateDto { + @IsEnum(ManualJobName) + @ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' }) + name!: ManualJobName; +} + export class JobCountsDto { @ApiProperty({ type: 'integer' }) active!: number; diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index c2c3ac9d27546..7fb363dd9a5e1 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; import { LibraryEntity } from 'src/entities/library.entity'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { Optional, ValidateUUID } from 'src/validation'; export class CreateLibraryDto { @ValidateUUID() @@ -89,14 +89,6 @@ export class LibrarySearchDto { userId?: string; } -export class ScanLibraryDto { - @ValidateBoolean({ optional: true }) - refreshModifiedFiles?: boolean; - - @ValidateBoolean({ optional: true }) - refreshAllFiles?: boolean; -} - export class LibraryResponseDto { id!: string; ownerId!: string; diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts index dffacc793d57b..f8b9e2043f3ea 100644 --- a/server/src/dtos/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -27,14 +27,14 @@ export class DuplicateDetectionConfig extends TaskConfig { export class FacialRecognitionConfig extends ModelConfig { @IsNumber() - @Min(0) + @Min(0.1) @Max(1) @Type(() => Number) @ApiProperty({ type: 'number', format: 'double' }) minScore!: number; @IsNumber() - @Min(0) + @Min(0.1) @Max(2) @Type(() => Number) @ApiProperty({ type: 'number', format: 'double' }) diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts new file mode 100644 index 0000000000000..34b3923580830 --- /dev/null +++ b/server/src/dtos/notification.dto.ts @@ -0,0 +1,3 @@ +export class TestEmailResponseDto { + messageId!: string; +} diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 9e36cfee800b8..5c5dce1a1190a 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -99,12 +99,6 @@ class BaseSearchDto { @Optional({ nullable: true, emptyToNull: true }) lensModel?: string | null; - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; - @IsInt() @Min(1) @Max(1000) @@ -119,7 +113,15 @@ class BaseSearchDto { personIds?: string[]; } -export class MetadataSearchDto extends BaseSearchDto { +export class RandomSearchDto extends BaseSearchDto { + @ValidateBoolean({ optional: true }) + withStacked?: boolean; + + @ValidateBoolean({ optional: true }) + withPeople?: boolean; +} + +export class MetadataSearchDto extends RandomSearchDto { @ValidateUUID({ optional: true }) id?: string; @@ -133,12 +135,6 @@ export class MetadataSearchDto extends BaseSearchDto { @Optional() checksum?: string; - @ValidateBoolean({ optional: true }) - withStacked?: boolean; - - @ValidateBoolean({ optional: true }) - withPeople?: boolean; - @IsString() @IsNotEmpty() @Optional() @@ -168,12 +164,24 @@ export class MetadataSearchDto extends BaseSearchDto { @Optional() @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) order?: AssetOrder; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; } export class SmartSearchDto extends BaseSearchDto { @IsString() @IsNotEmpty() query!: string; + + @IsInt() + @Min(1) + @Type(() => Number) + @Optional() + page?: number; } export class SearchPlacesDto { diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 78e59e4d1a695..aafadff47888f 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -121,6 +121,8 @@ export class ServerConfigDto { isInitialized!: boolean; isOnboarded!: boolean; externalDomain!: string; + mapDarkStyleUrl!: string; + mapLightStyleUrl!: string; } export class ServerFeaturesDto { diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index 14027aa16ad32..039dbd20ff36a 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -18,20 +18,20 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; +import { SystemConfig } from 'src/config'; +import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto'; import { AudioCodec, CQMode, Colorspace, ImageFormat, LogLevel, - SystemConfig, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec, VideoContainer, -} from 'src/config'; -import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig } from 'src/dtos/model-config.dto'; +} from 'src/enum'; import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface'; import { ValidateBoolean, validateCronExpression } from 'src/validation'; @@ -296,10 +296,12 @@ class SystemConfigMapDto { @ValidateBoolean() enabled!: boolean; - @IsString() + @IsNotEmpty() + @IsUrl() lightStyle!: string; - @IsString() + @IsNotEmpty() + @IsUrl() darkStyle!: string; } @@ -471,26 +473,10 @@ export class SystemConfigThemeDto { customCss!: string; } -class SystemConfigImageDto { +class SystemConfigGeneratedImageDto { @IsEnum(ImageFormat) @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) - thumbnailFormat!: ImageFormat; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - thumbnailSize!: number; - - @IsEnum(ImageFormat) - @ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat }) - previewFormat!: ImageFormat; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer' }) - previewSize!: number; + format!: ImageFormat; @IsInt() @Min(1) @@ -499,6 +485,24 @@ class SystemConfigImageDto { @ApiProperty({ type: 'integer' }) quality!: number; + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + size!: number; +} + +export class SystemConfigImageDto { + @Type(() => SystemConfigGeneratedImageDto) + @ValidateNested() + @IsObject() + thumbnail!: SystemConfigGeneratedImageDto; + + @Type(() => SystemConfigGeneratedImageDto) + @ValidateNested() + @IsObject() + preview!: SystemConfigGeneratedImageDto; + @IsEnum(Colorspace) @ApiProperty({ enumName: 'Colorspace', enum: Colorspace }) colorspace!: Colorspace; diff --git a/server/src/dtos/trash.dto.ts b/server/src/dtos/trash.dto.ts new file mode 100644 index 0000000000000..d8e139bff2858 --- /dev/null +++ b/server/src/dtos/trash.dto.ts @@ -0,0 +1,6 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class TrashResponseDto { + @ApiProperty({ type: 'integer' }) + count!: number; +} diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 9659fa39650a3..16eea373e3458 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -8,12 +8,6 @@ export class CreateProfileImageDto { export class CreateProfileImageResponseDto { userId!: string; + profileChangedAt!: Date; profileImagePath!: string; } - -export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { - return { - userId, - profileImagePath, - }; -} diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index f7cd70ee745c2..36f0b6386f76d 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -32,6 +32,7 @@ export class UserResponseDto { profileImagePath!: string; @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor }) avatarColor!: UserAvatarColor; + profileChangedAt!: Date; } export class UserLicense { @@ -47,6 +48,7 @@ export const mapUser = (entity: UserEntity): UserResponseDto => { name: entity.name, profileImagePath: entity.profileImagePath, avatarColor: getPreferences(entity).avatar.color, + profileChangedAt: entity.profileChangedAt, }; }; diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index 9ebf9364d1b2a..0b893134d0e0d 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -10,7 +10,7 @@ import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { StackEntity } from 'src/entities/stack.entity'; import { TagEntity } from 'src/entities/tag.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { AssetType } from 'src/enum'; +import { AssetStatus, AssetType } from 'src/enum'; import { Column, CreateDateColumn, @@ -70,6 +70,9 @@ export class AssetEntity { @Column() type!: AssetType; + @Column({ type: 'enum', enum: AssetStatus, default: AssetStatus.ACTIVE }) + status!: AssetStatus; + @Column() originalPath!: string; diff --git a/server/src/entities/move.entity.ts b/server/src/entities/move.entity.ts index f3dad6b280306..5cdef5d22ef76 100644 --- a/server/src/entities/move.entity.ts +++ b/server/src/entities/move.entity.ts @@ -1,3 +1,4 @@ +import { PathType } from 'src/enum'; import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm'; @Entity('move_history') @@ -21,21 +22,3 @@ export class MoveEntity { @Column({ type: 'varchar' }) newPath!: string; } - -export enum AssetPathType { - ORIGINAL = 'original', - PREVIEW = 'preview', - THUMBNAIL = 'thumbnail', - ENCODED_VIDEO = 'encoded_video', - SIDECAR = 'sidecar', -} - -export enum PersonPathType { - FACE = 'face', -} - -export enum UserPathType { - PROFILE = 'profile', -} - -export type PathType = AssetPathType | PersonPathType | UserPathType; diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 9cacad315ba21..ea446be390844 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -67,4 +67,7 @@ export class UserEntity { @OneToMany(() => UserMetadataEntity, (metadata) => metadata.user) metadata!: UserMetadataEntity[]; + + @Column({ type: 'timestamptz', default: () => 'CURRENT_TIMESTAMP' }) + profileChangedAt!: Date; } diff --git a/server/src/enum.ts b/server/src/enum.ts index 32254854e4c5a..e0c1e27859de9 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -1,3 +1,8 @@ +export enum AuthType { + PASSWORD = 'password', + OAUTH = 'oauth', +} + export enum AssetType { IMAGE = 'IMAGE', VIDEO = 'VIDEO', @@ -148,6 +153,14 @@ export enum SharedLinkType { INDIVIDUAL = 'INDIVIDUAL', } +export enum StorageFolder { + ENCODED_VIDEO = 'encoded-video', + LIBRARY = 'library', + UPLOAD = 'upload', + PROFILE = 'profile', + THUMBNAILS = 'thumbs', +} + export enum SystemMetadataKey { REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', FACIAL_RECOGNITION_STATE = 'facial-recognition-state', @@ -182,7 +195,136 @@ export enum UserStatus { DELETED = 'deleted', } +export enum AssetStatus { + ACTIVE = 'active', + TRASHED = 'trashed', + DELETED = 'deleted', +} + export enum SourceType { MACHINE_LEARNING = 'machine-learning', EXIF = 'exif', } + +export enum ManualJobName { + PERSON_CLEANUP = 'person-cleanup', + TAG_CLEANUP = 'tag-cleanup', + USER_CLEANUP = 'user-cleanup', +} + +export enum AssetPathType { + ORIGINAL = 'original', + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', + ENCODED_VIDEO = 'encoded_video', + SIDECAR = 'sidecar', +} + +export enum PersonPathType { + FACE = 'face', +} + +export enum UserPathType { + PROFILE = 'profile', +} + +export type PathType = AssetPathType | PersonPathType | UserPathType; + +export enum TranscodePolicy { + ALL = 'all', + OPTIMAL = 'optimal', + BITRATE = 'bitrate', + REQUIRED = 'required', + DISABLED = 'disabled', +} + +export enum TranscodeTarget { + NONE, + AUDIO, + VIDEO, + ALL, +} + +export enum VideoCodec { + H264 = 'h264', + HEVC = 'hevc', + VP9 = 'vp9', + AV1 = 'av1', +} + +export enum AudioCodec { + MP3 = 'mp3', + AAC = 'aac', + LIBOPUS = 'libopus', +} + +export enum VideoContainer { + MOV = 'mov', + MP4 = 'mp4', + OGG = 'ogg', + WEBM = 'webm', +} + +export enum TranscodeHWAccel { + NVENC = 'nvenc', + QSV = 'qsv', + VAAPI = 'vaapi', + RKMPP = 'rkmpp', + DISABLED = 'disabled', +} + +export enum ToneMapping { + HABLE = 'hable', + MOBIUS = 'mobius', + REINHARD = 'reinhard', + DISABLED = 'disabled', +} + +export enum CQMode { + AUTO = 'auto', + CQP = 'cqp', + ICQ = 'icq', +} + +export enum Colorspace { + SRGB = 'srgb', + P3 = 'p3', +} + +export enum ImageFormat { + JPEG = 'jpeg', + WEBP = 'webp', +} + +export enum LogLevel { + VERBOSE = 'verbose', + DEBUG = 'debug', + LOG = 'log', + WARN = 'warn', + ERROR = 'error', + FATAL = 'fatal', +} + +export enum MetadataKey { + AUTH_ROUTE = 'auth_route', + ADMIN_ROUTE = 'admin_route', + SHARED_ROUTE = 'shared_route', + API_KEY_SECURITY = 'api_key', + ON_EMIT_CONFIG = 'on_emit_config', +} + +export enum RouteKey { + ASSET = 'assets', + USER = 'users', +} + +export enum CacheControl { + PRIVATE_WITH_CACHE = 'private_with_cache', + PRIVATE_WITHOUT_CACHE = 'private_without_cache', + NONE = 'none', +} + +export enum PaginationMode { + LIMIT_OFFSET = 'limit-offset', + SKIP_TAKE = 'skip-take', +} diff --git a/server/src/interfaces/album.interface.ts b/server/src/interfaces/album.interface.ts index 091442ff0592a..24c64bdc9d2c0 100644 --- a/server/src/interfaces/album.interface.ts +++ b/server/src/interfaces/album.interface.ts @@ -16,18 +16,15 @@ export interface AlbumInfoOptions { export interface IAlbumRepository extends IBulkAsset { getById(id: string, options: AlbumInfoOptions): Promise; - getByIds(ids: string[]): Promise; getByAssetId(ownerId: string, assetId: string): Promise; removeAsset(assetId: string): Promise; getMetadataForIds(ids: string[]): Promise; - getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; getShared(ownerId: string): Promise; getNotShared(ownerId: string): Promise; restoreAll(userId: string): Promise; softDeleteAll(userId: string): Promise; deleteAll(userId: string): Promise; - getAll(): Promise; create(album: Partial): Promise; update(album: Partial): Promise; delete(id: string): Promise; diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 9f7213de82b80..750a85209474c 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetStatus, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; @@ -36,8 +36,6 @@ export enum WithoutProperty { export enum WithProperty { SIDECAR = 'sidecar', - IS_ONLINE = 'isOnline', - IS_OFFLINE = 'isOffline', } export enum TimeBucketSize { @@ -56,6 +54,7 @@ export interface AssetBuilderOptions { userIds?: string[]; withStacked?: boolean; exifInfo?: boolean; + status?: AssetStatus; assetType?: AssetType; } @@ -142,13 +141,17 @@ export interface AssetUpdateDuplicateOptions { duplicateIds: string[]; } +export interface UpsertFileOptions { + assetId: string; + type: AssetFileType; + path: string; +} + export type AssetPathEntity = Pick; export const IAssetRepository = 'IAssetRepository'; export interface IAssetRepository { - getAssetsByOriginalPath(userId: string, partialPath: string): Promise; - getUniqueOriginalPaths(userId: string): Promise; create(asset: AssetCreate): Promise; getByIds( ids: string[], @@ -175,10 +178,8 @@ export interface IAssetRepository { libraryId?: string, withDeleted?: boolean, ): Paginated; - getRandom(userId: string, count: number): Promise; - getFirstAssetForAlbumId(albumId: string): Promise; + getRandom(userIds: string[], count: number): Promise; getLastUpdatedAssetForAlbumId(albumId: string): Promise; - getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated; getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; @@ -188,8 +189,6 @@ export interface IAssetRepository { updateDuplicates(options: AssetUpdateDuplicateOptions): Promise; update(asset: AssetUpdateOptions): Promise; remove(asset: AssetEntity): Promise; - softDeleteAll(ids: string[]): Promise; - restoreAll(ids: string[]): Promise; findLivePhotoMatch(options: LivePhotoSearchOptions): Promise; getStatistics(ownerId: string, options: AssetStatsOptions): Promise; getTimeBuckets(options: TimeBucketOptions): Promise; @@ -201,5 +200,6 @@ export interface IAssetRepository { getDuplicates(options: AssetBuilderOptions): Promise; getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; - upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise; + upsertFile(file: UpsertFileOptions): Promise; + upsertFiles(files: UpsertFileOptions[]): Promise; } diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts new file mode 100644 index 0000000000000..11bccbe348b1a --- /dev/null +++ b/server/src/interfaces/config.interface.ts @@ -0,0 +1,14 @@ +import { VectorExtension } from 'src/interfaces/database.interface'; + +export const IConfigRepository = 'IConfigRepository'; + +export interface EnvData { + database: { + skipMigrations: boolean; + vectorExtension: VectorExtension; + }; +} + +export interface IConfigRepository { + getEnv(): EnvData; +} diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 61233a8001eb3..bc5ce90f40b9b 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -21,10 +21,26 @@ type EmitEventMap = { 'asset.tag': [{ assetId: string }]; 'asset.untag': [{ assetId: string }]; 'asset.hide': [{ assetId: string; userId: string }]; + 'asset.show': [{ assetId: string; userId: string }]; + 'asset.trash': [{ assetId: string; userId: string }]; + 'asset.delete': [{ assetId: string; userId: string }]; + + // asset bulk events + 'assets.trash': [{ assetIds: string[]; userId: string }]; + 'assets.delete': [{ assetIds: string[]; userId: string }]; + 'assets.restore': [{ assetIds: string[]; userId: string }]; // session events 'session.delete': [{ sessionId: string }]; + // stack events + 'stack.create': [{ stackId: string; userId: string }]; + 'stack.update': [{ stackId: string; userId: string }]; + 'stack.delete': [{ stackId: string; userId: string }]; + + // stack bulk events + 'stacks.delete': [{ stackIds: string[]; userId: string }]; + // user events 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index bc780398eaf05..aa3090675e3fe 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -37,9 +37,7 @@ export enum JobName { // thumbnails QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', - GENERATE_PREVIEW = 'generate-preview', - GENERATE_THUMBNAIL = 'generate-thumbnail', - GENERATE_THUMBHASH = 'generate-thumbhash', + GENERATE_THUMBNAILS = 'generate-thumbnails', GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', // metadata @@ -60,6 +58,9 @@ export enum JobName { STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', + // tags + TAG_CLEANUP = 'tag-cleanup', + // migration QUEUE_MIGRATION = 'queue-migration', MIGRATE_ASSET = 'migrate-asset', @@ -73,12 +74,12 @@ export enum JobName { FACIAL_RECOGNITION = 'facial-recognition', // library management - LIBRARY_SCAN = 'library-refresh', - LIBRARY_SCAN_ASSET = 'library-refresh-asset', - LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', - LIBRARY_CHECK_OFFLINE = 'library-check-offline', + LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files', + LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets', + LIBRARY_SYNC_FILE = 'library-sync-file', + LIBRARY_SYNC_ASSET = 'library-sync-asset', LIBRARY_DELETE = 'library-delete', - LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', + LIBRARY_QUEUE_SYNC_ALL = 'library-queue-sync-all', LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', // cleanup @@ -90,6 +91,8 @@ export enum JobName { QUEUE_SMART_SEARCH = 'queue-smart-search', SMART_SEARCH = 'smart-search', + QUEUE_TRASH_EMPTY = 'queue-trash-empty', + // duplicate detection QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection', DUPLICATE_DETECTION = 'duplicate-detection', @@ -111,7 +114,7 @@ export enum JobName { } export const JOBS_ASSET_PAGINATION_SIZE = 1000; -export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000; +export const JOBS_LIBRARY_PAGINATION_SIZE = 10_000; export interface IBaseJob { force?: boolean; @@ -120,6 +123,7 @@ export interface IBaseJob { export interface IEntityJob extends IBaseJob { id: string; source?: 'upload' | 'sidecar-write' | 'copy'; + notify?: boolean; } export interface IAssetDeleteJob extends IEntityJob { @@ -131,16 +135,11 @@ export interface ILibraryFileJob extends IEntityJob { assetPath: string; } -export interface ILibraryOfflineJob extends IEntityJob { +export interface ILibraryAssetJob extends IEntityJob { importPaths: string[]; exclusionPatterns: string[]; } -export interface ILibraryRefreshJob extends IEntityJob { - refreshModifiedFiles: boolean; - refreshAllFiles: boolean; -} - export interface IBulkEntityJob extends IBaseJob { ids: string[]; } @@ -211,9 +210,7 @@ export type JobItem = // Thumbnails | { name: JobName.QUEUE_GENERATE_THUMBNAILS; data: IBaseJob } - | { name: JobName.GENERATE_PREVIEW; data: IEntityJob } - | { name: JobName.GENERATE_THUMBNAIL; data: IEntityJob } - | { name: JobName.GENERATE_THUMBHASH; data: IEntityJob } + | { name: JobName.GENERATE_THUMBNAILS; data: IEntityJob } // User | { name: JobName.USER_DELETE_CHECK; data?: IBaseJob } @@ -249,6 +246,7 @@ export type JobItem = // Smart Search | { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob } | { name: JobName.SMART_SEARCH; data: IEntityJob } + | { name: JobName.QUEUE_TRASH_EMPTY; data?: IBaseJob } // Duplicate Detection | { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob } @@ -261,18 +259,21 @@ export type JobItem = | { name: JobName.CLEAN_OLD_AUDIT_LOGS; data?: IBaseJob } | { name: JobName.CLEAN_OLD_SESSION_TOKENS; data?: IBaseJob } + // Tags + | { name: JobName.TAG_CLEANUP; data?: IBaseJob } + // Asset Deletion | { name: JobName.PERSON_CLEANUP; data?: IBaseJob } | { name: JobName.ASSET_DELETION; data: IAssetDeleteJob } | { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob } // Library Management - | { name: JobName.LIBRARY_SCAN_ASSET; data: ILibraryFileJob } - | { name: JobName.LIBRARY_SCAN; data: ILibraryRefreshJob } - | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob } + | { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob } + | { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob } + | { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob } + | { name: JobName.LIBRARY_SYNC_ASSET; data: IEntityJob } | { name: JobName.LIBRARY_DELETE; data: IEntityJob } - | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob } - | { name: JobName.LIBRARY_CHECK_OFFLINE; data: IEntityJob } + | { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob } // Notification @@ -299,7 +300,6 @@ export interface IJobRepository { addHandler(queueName: QueueName, concurrency: number, handler: JobItemHandler): void; addCronJob(name: string, expression: string, onTick: () => void, start?: boolean): void; updateCronJob(name: string, expression?: string, start?: boolean): void; - deleteCronJob(name: string): void; setConcurrency(queueName: QueueName, concurrency: number): void; queue(item: JobItem): Promise; queueAll(items: JobItem[]): Promise; diff --git a/server/src/interfaces/logger.interface.ts b/server/src/interfaces/logger.interface.ts index f0afdce2a521c..42523afa6b513 100644 --- a/server/src/interfaces/logger.interface.ts +++ b/server/src/interfaces/logger.interface.ts @@ -1,4 +1,4 @@ -import { LogLevel } from 'src/config'; +import { LogLevel } from 'src/enum'; export const ILoggerRepository = 'ILoggerRepository'; @@ -6,6 +6,7 @@ export interface ILoggerRepository { setAppName(name: string): void; setContext(message: string): void; setLogLevel(level: LogLevel): void; + isLevelEnabled(level: LogLevel): boolean; verbose(message: any, ...args: any): void; debug(message: any, ...args: any): void; diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index f7389d3d068cd..2bc8ccde36d8b 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -1,5 +1,5 @@ import { Writable } from 'node:stream'; -import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config'; +import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/enum'; export const IMediaRepository = 'IMediaRepository'; @@ -10,13 +10,44 @@ export interface CropOptions { height: number; } -export interface ThumbnailOptions { - size: number; +export interface ImageOptions { format: ImageFormat; - colorspace: string; quality: number; + size: number; +} + +export interface RawImageInfo { + width: number; + height: number; + channels: 1 | 2 | 3 | 4; +} + +interface DecodeImageOptions { + colorspace: string; crop?: CropOptions; processInvalidImages: boolean; + raw?: RawImageInfo; +} + +export interface DecodeToBufferOptions extends DecodeImageOptions { + size: number; +} + +export type GenerateThumbnailOptions = ImageOptions & DecodeImageOptions; + +export type GenerateThumbnailFromBufferOptions = GenerateThumbnailOptions & { raw: RawImageInfo }; + +export type GenerateThumbhashOptions = DecodeImageOptions; + +export type GenerateThumbhashFromBufferOptions = GenerateThumbhashOptions & { raw: RawImageInfo }; + +export interface GenerateThumbnailsOptions { + colorspace: string; + crop?: CropOptions; + preview?: ImageOptions; + processInvalidImages: boolean; + thumbhash?: boolean; + thumbnail?: ImageOptions; } export interface VideoStreamInfo { @@ -62,6 +93,10 @@ export interface TranscodeCommand { inputOptions: string[]; outputOptions: string[]; twoPass: boolean; + progress: { + frameCount: number; + percentInterval: number; + }; } export interface BitrateDistribution { @@ -71,6 +106,11 @@ export interface BitrateDistribution { unit: string; } +export interface ImageBuffer { + data: Buffer; + info: RawImageInfo; +} + export interface VideoCodecSWConfig { getCommand(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream: AudioStreamInfo): TranscodeCommand; } @@ -79,14 +119,21 @@ export interface VideoCodecHWConfig extends VideoCodecSWConfig { getSupportedCodecs(): Array; } +export interface ProbeOptions { + countFrames: boolean; +} + export interface IMediaRepository { // image extract(input: string, output: string): Promise; - generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise; - generateThumbhash(imagePath: string): Promise; + decodeImage(input: string, options: DecodeToBufferOptions): Promise; + generateThumbnail(input: string, options: GenerateThumbnailOptions, outputFile: string): Promise; + generateThumbnail(input: Buffer, options: GenerateThumbnailFromBufferOptions, outputFile: string): Promise; + generateThumbhash(input: string, options: GenerateThumbhashOptions): Promise; + generateThumbhash(input: Buffer, options: GenerateThumbhashFromBufferOptions): Promise; getImageDimensions(input: string): Promise; // video - probe(input: string): Promise; + probe(input: string, options?: ProbeOptions): Promise; transcode(input: string, output: string | Writable, command: TranscodeCommand): Promise; } diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts index 39ff6ab4af33d..574420e27a1c8 100644 --- a/server/src/interfaces/metadata.interface.ts +++ b/server/src/interfaces/metadata.interface.ts @@ -7,7 +7,18 @@ export interface ExifDuration { Scale?: number; } -type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo'; +type StringOrNumber = string | number; + +type TagsWithWrongTypes = + | 'FocalLength' + | 'Duration' + | 'Description' + | 'ImageDescription' + | 'RegionInfo' + | 'TagsList' + | 'Keywords' + | 'HierarchicalSubject' + | 'ISO'; export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; @@ -20,10 +31,14 @@ export interface ImmichTags extends Omit { EmbeddedVideoType?: string; EmbeddedVideoFile?: BinaryField; MotionPhotoVideo?: BinaryField; + TagsList?: StringOrNumber[]; + HierarchicalSubject?: StringOrNumber[]; + Keywords?: StringOrNumber | StringOrNumber[]; + ISO?: number | number[]; // Type is wrong, can also be number. - Description?: string | number; - ImageDescription?: string | number; + Description?: StringOrNumber; + ImageDescription?: StringOrNumber; // Extended properties for image regions, such as faces RegionInfo?: { @@ -53,9 +68,4 @@ export interface IMetadataRepository { readTags(path: string): Promise; writeTags(path: string, tags: Partial): Promise; extractBinaryTag(tagName: string, path: string): Promise; - getCountries(userIds: string[]): Promise>; - getStates(userIds: string[], country?: string): Promise>; - getCities(userIds: string[], country?: string, state?: string): Promise>; - getCameraMakes(userIds: string[], model?: string): Promise>; - getCameraModels(userIds: string[], make?: string): Promise>; } diff --git a/server/src/interfaces/move.interface.ts b/server/src/interfaces/move.interface.ts index c9d39e78cf497..0e79cfcadc5a8 100644 --- a/server/src/interfaces/move.interface.ts +++ b/server/src/interfaces/move.interface.ts @@ -1,4 +1,5 @@ -import { MoveEntity, PathType } from 'src/entities/move.entity'; +import { MoveEntity } from 'src/entities/move.entity'; +import { PathType } from 'src/enum'; export const IMoveRepository = 'IMoveRepository'; diff --git a/server/src/interfaces/person.interface.ts b/server/src/interfaces/person.interface.ts index 5708274a6e99f..65814e0046f46 100644 --- a/server/src/interfaces/person.interface.ts +++ b/server/src/interfaces/person.interface.ts @@ -1,6 +1,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { SourceType } from 'src/enum'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; @@ -40,10 +41,12 @@ export interface PeopleStatistics { hidden: number; } -export interface DeleteAllFacesOptions { - sourceType?: string; +export interface DeleteFacesOptions { + sourceType: SourceType; } +export type UnassignFacesOptions = DeleteFacesOptions; + export interface IPersonRepository { getAll(pagination: PaginationOptions, options?: FindManyOptions): Paginated; getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated; @@ -59,7 +62,7 @@ export interface IPersonRepository { createFaces(entities: Partial[]): Promise; delete(entities: PersonEntity[]): Promise; deleteAll(): Promise; - deleteAllFaces(options: DeleteAllFacesOptions): Promise; + deleteFaces(options: DeleteFacesOptions): Promise; replaceFaces(assetId: string, entities: Partial[], sourceType?: string): Promise; getAllFaces(pagination: PaginationOptions, options?: FindManyOptions): Paginated; getFaceById(id: string): Promise; @@ -75,6 +78,7 @@ export interface IPersonRepository { reassignFace(assetFaceId: string, newPersonId: string): Promise; getNumberOfPeople(userId: string): Promise; reassignFaces(data: UpdateFacesData): Promise; + unassignFaces(options: UnassignFacesOptions): Promise; update(person: Partial): Promise; updateAll(people: Partial[]): Promise; getLatestFaceDate(): Promise; diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 0226e3663c3b5..63d74a35fb626 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -1,7 +1,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; -import { AssetType } from 'src/enum'; +import { AssetStatus, AssetType } from 'src/enum'; import { Paginated } from 'src/utils/pagination'; export const ISearchRepository = 'ISearchRepository'; @@ -61,6 +61,7 @@ export interface SearchStatusOptions { isVisible?: boolean; isNotInAlbum?: boolean; type?: AssetType; + status?: AssetStatus; withArchived?: boolean; withDeleted?: boolean; } @@ -175,10 +176,16 @@ export interface ISearchRepository { searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; searchDuplicates(options: AssetDuplicateSearch): Promise; searchFaces(search: FaceEmbeddingSearch): Promise; + searchRandom(size: number, options: AssetSearchOptions): Promise; upsert(assetId: string, embedding: number[]): Promise; searchPlaces(placeName: string): Promise; getAssetsByCity(userIds: string[]): Promise; deleteAllSearchEmbeddings(): Promise; getDimensionSize(): Promise; setDimensionSize(dimSize: number): Promise; + getCountries(userIds: string[]): Promise>; + getStates(userIds: string[], country?: string): Promise>; + getCities(userIds: string[], country?: string, state?: string): Promise>; + getCameraMakes(userIds: string[], model?: string): Promise>; + getCameraModels(userIds: string[], make?: string): Promise>; } diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index fec3d66dd5c03..321f7b8367f23 100644 --- a/server/src/interfaces/storage.interface.ts +++ b/server/src/interfaces/storage.interface.ts @@ -35,7 +35,9 @@ export interface IStorageRepository { createZipStream(): ImmichZipStream; createReadStream(filepath: string, mimeType?: string | null): Promise; readFile(filepath: string, options?: FileReadOptions): Promise; - writeFile(filepath: string, buffer: Buffer): Promise; + createFile(filepath: string, buffer: Buffer): Promise; + createOrOverwriteFile(filepath: string, buffer: Buffer): Promise; + overwriteFile(filepath: string, buffer: Buffer): Promise; realpath(filepath: string): Promise; unlink(filepath: string): Promise; unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise; diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts index aca9c223d552b..16a34d6ac4960 100644 --- a/server/src/interfaces/tag.interface.ts +++ b/server/src/interfaces/tag.interface.ts @@ -17,4 +17,5 @@ export interface ITagRepository extends IBulkAsset { upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise; upsertAssetIds(items: AssetTagItem[]): Promise; + deleteEmptyTags(): Promise; } diff --git a/server/src/interfaces/trash.interface.ts b/server/src/interfaces/trash.interface.ts new file mode 100644 index 0000000000000..96c2322d8afd6 --- /dev/null +++ b/server/src/interfaces/trash.interface.ts @@ -0,0 +1,10 @@ +import { Paginated, PaginationOptions } from 'src/utils/pagination'; + +export const ITrashRepository = 'ITrashRepository'; + +export interface ITrashRepository { + empty(userId: string): Promise; + restore(userId: string): Promise; + restoreAll(assetIds: string[]): Promise; + getDeletedIds(pagination: PaginationOptions): Paginated; +} diff --git a/server/src/interfaces/view.interface.ts b/server/src/interfaces/view.interface.ts new file mode 100644 index 0000000000000..f819160002041 --- /dev/null +++ b/server/src/interfaces/view.interface.ts @@ -0,0 +1,8 @@ +import { AssetEntity } from 'src/entities/asset.entity'; + +export const IViewRepository = 'IViewRepository'; + +export interface IViewRepository { + getAssetsByOriginalPath(userId: string, partialPath: string): Promise; + getUniqueOriginalPaths(userId: string): Promise; +} diff --git a/server/src/main.ts b/server/src/main.ts index ee4de1a259d19..48ce179e88753 100644 --- a/server/src/main.ts +++ b/server/src/main.ts @@ -2,7 +2,7 @@ import { CommandFactory } from 'nest-commander'; import { fork } from 'node:child_process'; import { Worker } from 'node:worker_threads'; import { ImmichAdminModule } from 'src/app.module'; -import { LogLevel } from 'src/config'; +import { LogLevel } from 'src/enum'; import { getWorkers } from 'src/utils/workers'; const immichApp = process.argv[2] || process.env.IMMICH_APP; @@ -18,7 +18,9 @@ async function bootstrapImmichAdmin() { function bootstrapWorker(name: string) { console.log(`Starting ${name} worker`); - const worker = name === 'api' ? fork(`./dist/workers/${name}.js`) : new Worker(`./dist/workers/${name}.js`); + const execArgv = process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)); + const worker = + name === 'api' ? fork(`./dist/workers/${name}.js`, [], { execArgv }) : new Worker(`./dist/workers/${name}.js`); worker.on('error', (error) => { console.error(`${name} worker error: ${error}`); diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index d6138f2d3ae24..7bc4f41b21c8f 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -11,19 +11,11 @@ import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto'; -import { Permission } from 'src/enum'; +import { MetadataKey, Permission } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { UAParser } from 'ua-parser-js'; -export enum Metadata { - AUTH_ROUTE = 'auth_route', - ADMIN_ROUTE = 'admin_route', - SHARED_ROUTE = 'shared_route', - API_KEY_SECURITY = 'api_key', - ON_EMIT_CONFIG = 'on_emit_config', -} - type AdminRoute = { admin?: true }; type SharedLinkRoute = { sharedLink?: true }; type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute); @@ -32,8 +24,8 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator = const decorators: MethodDecorator[] = [ ApiBearerAuth(), ApiCookieAuth(), - ApiSecurity(Metadata.API_KEY_SECURITY), - SetMetadata(Metadata.AUTH_ROUTE, options || {}), + ApiSecurity(MetadataKey.API_KEY_SECURITY), + SetMetadata(MetadataKey.AUTH_ROUTE, options || {}), ]; if ((options as SharedLinkRoute)?.sharedLink) { @@ -85,7 +77,7 @@ export class AuthGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const targets = [context.getHandler()]; - const options = this.reflector.getAllAndOverride(Metadata.AUTH_ROUTE, targets); + const options = this.reflector.getAllAndOverride(MetadataKey.AUTH_ROUTE, targets); if (!options) { return true; } diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts index 6ec8b401efb7f..075a7f504636a 100644 --- a/server/src/middleware/file-upload.interceptor.ts +++ b/server/src/middleware/file-upload.interceptor.ts @@ -7,6 +7,7 @@ import multer, { StorageEngine, diskStorage } from 'multer'; import { createHash, randomUUID } from 'node:crypto'; import { Observable } from 'rxjs'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; +import { RouteKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthRequest } from 'src/middleware/auth.guard'; import { AssetMediaService, UploadFile } from 'src/services/asset-media.service'; @@ -28,11 +29,6 @@ export function getFiles(files: UploadFiles) { }; } -export enum Route { - ASSET = 'assets', - USER = 'users', -} - export interface ImmichFile extends Express.Multer.File { /** sha1 hash of file */ uuid: string; @@ -115,7 +111,7 @@ export class FileUploadInterceptor implements NestInterceptor { const context_ = context.switchToHttp(); const route = this.reflect.get(PATH_METADATA, context.getClass()); - const handler: RequestHandler | null = this.getHandler(route as Route); + const handler: RequestHandler | null = this.getHandler(route as RouteKey); if (handler) { await new Promise((resolve, reject) => { const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve()); @@ -176,13 +172,13 @@ export class FileUploadInterceptor implements NestInterceptor { return false; } - private getHandler(route: Route) { + private getHandler(route: RouteKey) { switch (route) { - case Route.ASSET: { + case RouteKey.ASSET: { return this.handlers.assetUpload; } - case Route.USER: { + case RouteKey.USER: { return this.handlers.userProfile; } diff --git a/server/src/migrations/1726491047923-AddprofileChangedAt.ts b/server/src/migrations/1726491047923-AddprofileChangedAt.ts new file mode 100644 index 0000000000000..bcf568426a817 --- /dev/null +++ b/server/src/migrations/1726491047923-AddprofileChangedAt.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddprofileChangedAt1726491047923 implements MigrationInterface { + name = 'AddprofileChangedAt1726491047923' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" ADD "profileChangedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "profileChangedAt"`); + } + +} diff --git a/server/src/migrations/1726593009549-AddAssetStatus.ts b/server/src/migrations/1726593009549-AddAssetStatus.ts new file mode 100644 index 0000000000000..5b243b05b5c24 --- /dev/null +++ b/server/src/migrations/1726593009549-AddAssetStatus.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAssetStatus1726593009549 implements MigrationInterface { + name = 'AddAssetStatus1726593009549' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "assets_status_enum" AS ENUM('active', 'trashed', 'deleted')`); + await queryRunner.query(`ALTER TABLE "assets" ADD "status" "assets_status_enum" NOT NULL DEFAULT 'active'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "status"`); + await queryRunner.query(`DROP TYPE "assets_status_enum"`); + } + +} diff --git a/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts new file mode 100644 index 0000000000000..e02203997f723 --- /dev/null +++ b/server/src/migrations/1727471863507-SeparateQualityForThumbnailAndPreview.ts @@ -0,0 +1,37 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SeparateQualityForThumbnailAndPreview1727471863507 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update system_metadata + set value = jsonb_set(value, '{image}', jsonb_strip_nulls( + jsonb_build_object( + 'preview', jsonb_build_object( + 'format', value->'image'->'previewFormat', + 'quality', value->'image'->'quality', + 'size', value->'image'->'previewSize'), + 'thumbnail', jsonb_build_object( + 'format', value->'image'->'thumbnailFormat', + 'quality', value->'image'->'quality', + 'size', value->'image'->'thumbnailSize'), + 'extractEmbedded', value->'extractEmbedded', + 'colorspace', value->'colorspace' + ))) + where key = 'system-config'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + update system_metadata + set value = jsonb_set(value, '{image}', jsonb_strip_nulls(jsonb_build_object( + 'previewFormat', value->'image'->'preview'->'format', + 'previewSize', value->'image'->'preview'->'size', + 'thumbnailFormat', value->'image'->'thumbnail'->'format', + 'thumbnailSize', value->'image'->'thumbnail'->'size', + 'extractEmbedded', value->'extractEmbedded', + 'colorspace', value->'colorspace', + 'quality', value->'image'->'preview'->'quality' + ))) + where key = 'system-config'`); + } +} diff --git a/server/src/queries/activity.repository.sql b/server/src/queries/activity.repository.sql index 3f3e04140ccc2..44042c0e6d39f 100644 --- a/server/src/queries/activity.repository.sql +++ b/server/src/queries/activity.repository.sql @@ -23,7 +23,8 @@ SELECT "ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status", "ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt", "ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes", - "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes" + "ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes", + "ActivityEntity__ActivityEntity_user"."profileChangedAt" AS "ActivityEntity__ActivityEntity_user_profileChangedAt" FROM "activity" "ActivityEntity" LEFT JOIN "users" "ActivityEntity__ActivityEntity_user" ON "ActivityEntity__ActivityEntity_user"."id" = "ActivityEntity"."userId" diff --git a/server/src/queries/album.repository.sql b/server/src/queries/album.repository.sql index 729f7c7f20f0d..c4f6fbdd3218b 100644 --- a/server/src/queries/album.repository.sql +++ b/server/src/queries/album.repository.sql @@ -30,6 +30,7 @@ FROM "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -47,6 +48,7 @@ FROM "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -80,64 +82,6 @@ ORDER BY LIMIT 1 --- AlbumRepository.getByIds -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", - "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", - "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", - "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."oauthId" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_oauthId", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileImagePath" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileImagePath", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."shouldChangePassword" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_shouldChangePassword", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."createdAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_createdAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" -FROM - "albums" "AlbumEntity" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) - LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" - LEFT JOIN "users" "a641d58cf46d4a391ba060ac4dc337665c69ffea" ON "a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" = "AlbumEntity__AlbumEntity_albumUsers"."usersId" - AND ( - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" IS NULL - ) -WHERE - ((("AlbumEntity"."id" IN ($1)))) - AND ("AlbumEntity"."deletedAt" IS NULL) - -- AlbumRepository.getByAssetId SELECT "AlbumEntity"."id" AS "AlbumEntity_id", @@ -164,6 +108,7 @@ SELECT "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt", "AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId", "AlbumEntity__AlbumEntity_albumUsers"."usersId" AS "AlbumEntity__AlbumEntity_albumUsers_usersId", "AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role", @@ -180,7 +125,8 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", - "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes" + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" @@ -241,35 +187,6 @@ WHERE GROUP BY "album"."id" --- AlbumRepository.getInvalidThumbnail -SELECT - "albums"."id" AS "albums_id" -FROM - "albums" "albums" -WHERE - ( - "albums"."albumThumbnailAssetId" IS NULL - AND EXISTS ( - SELECT - 1 - FROM - "albums_assets_assets" "albums_assets" - WHERE - "albums"."id" = "albums_assets"."albumsId" - ) - OR "albums"."albumThumbnailAssetId" IS NOT NULL - AND NOT EXISTS ( - SELECT - 1 - FROM - "albums_assets_assets" "albums_assets" - WHERE - "albums"."id" = "albums_assets"."albumsId" - AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId" - ) - ) - AND ("albums"."deletedAt" IS NULL) - -- AlbumRepository.getOwned SELECT "AlbumEntity"."id" AS "AlbumEntity_id", @@ -299,6 +216,7 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -324,7 +242,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -372,6 +291,7 @@ SELECT "a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes", "a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes", + "a641d58cf46d4a391ba060ac4dc337665c69ffea"."profileChangedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_profileChangedAt", "AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id", "AlbumEntity__AlbumEntity_sharedLinks"."description" AS "AlbumEntity__AlbumEntity_sharedLinks_description", "AlbumEntity__AlbumEntity_sharedLinks"."password" AS "AlbumEntity__AlbumEntity_sharedLinks_password", @@ -397,7 +317,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -495,7 +416,8 @@ SELECT "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" + "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes", + "AlbumEntity__AlbumEntity_owner"."profileChangedAt" AS "AlbumEntity__AlbumEntity_owner_profileChangedAt" FROM "albums" "AlbumEntity" LEFT JOIN "albums_shared_users_users" "AlbumEntity__AlbumEntity_albumUsers" ON "AlbumEntity__AlbumEntity_albumUsers"."albumsId" = "AlbumEntity"."id" @@ -528,41 +450,6 @@ WHERE ORDER BY "AlbumEntity"."createdAt" DESC --- AlbumRepository.getAll -SELECT - "AlbumEntity"."id" AS "AlbumEntity_id", - "AlbumEntity"."ownerId" AS "AlbumEntity_ownerId", - "AlbumEntity"."albumName" AS "AlbumEntity_albumName", - "AlbumEntity"."description" AS "AlbumEntity_description", - "AlbumEntity"."createdAt" AS "AlbumEntity_createdAt", - "AlbumEntity"."updatedAt" AS "AlbumEntity_updatedAt", - "AlbumEntity"."deletedAt" AS "AlbumEntity_deletedAt", - "AlbumEntity"."albumThumbnailAssetId" AS "AlbumEntity_albumThumbnailAssetId", - "AlbumEntity"."isActivityEnabled" AS "AlbumEntity_isActivityEnabled", - "AlbumEntity"."order" AS "AlbumEntity_order", - "AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id", - "AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name", - "AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin", - "AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email", - "AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel", - "AlbumEntity__AlbumEntity_owner"."oauthId" AS "AlbumEntity__AlbumEntity_owner_oauthId", - "AlbumEntity__AlbumEntity_owner"."profileImagePath" AS "AlbumEntity__AlbumEntity_owner_profileImagePath", - "AlbumEntity__AlbumEntity_owner"."shouldChangePassword" AS "AlbumEntity__AlbumEntity_owner_shouldChangePassword", - "AlbumEntity__AlbumEntity_owner"."createdAt" AS "AlbumEntity__AlbumEntity_owner_createdAt", - "AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt", - "AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status", - "AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt", - "AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes", - "AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes" -FROM - "albums" "AlbumEntity" - LEFT JOIN "users" "AlbumEntity__AlbumEntity_owner" ON "AlbumEntity__AlbumEntity_owner"."id" = "AlbumEntity"."ownerId" - AND ( - "AlbumEntity__AlbumEntity_owner"."deletedAt" IS NULL - ) -WHERE - "AlbumEntity"."deletedAt" IS NULL - -- AlbumRepository.removeAsset DELETE FROM "albums_assets_assets" WHERE @@ -596,16 +483,13 @@ UPDATE "albums" SET "albumThumbnailAssetId" = ( SELECT - "albums_assets2"."assetsId" + "album_assets"."assetsId" FROM - "assets" "assets", - "albums_assets_assets" "albums_assets2" + "albums_assets_assets" "album_assets" + INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id" + AND "assets"."deletedAt" IS NULL WHERE - ( - "albums_assets2"."assetsId" = "assets"."id" - AND "albums_assets2"."albumsId" = "albums"."id" - ) - AND ("assets"."deletedAt" IS NULL) + "album_assets"."albumsId" = "albums"."id" ORDER BY "assets"."fileCreatedAt" DESC LIMIT @@ -618,17 +502,21 @@ WHERE SELECT 1 FROM - "albums_assets_assets" "albums_assets" + "albums_assets_assets" "album_assets" + INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id" + AND "assets"."deletedAt" IS NULL WHERE - "albums"."id" = "albums_assets"."albumsId" + "album_assets"."albumsId" = "albums"."id" ) OR "albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM - "albums_assets_assets" "albums_assets" + "albums_assets_assets" "album_assets" + INNER JOIN "assets" "assets" ON "album_assets"."assetsId" = "assets"."id" + AND "assets"."deletedAt" IS NULL WHERE - "albums"."id" = "albums_assets"."albumsId" - AND "albums"."albumThumbnailAssetId" = "albums_assets"."assetsId" + "album_assets"."albumsId" = "albums"."id" + AND "albums"."albumThumbnailAssetId" = "album_assets"."assetsId" ) diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index e5f389ac4d017..f4989d355e880 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -24,6 +24,7 @@ FROM "APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt", "APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes", "APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes", + "APIKeyEntity__APIKeyEntity_user"."profileChangedAt" AS "APIKeyEntity__APIKeyEntity_user_profileChangedAt", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."userId" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_userId", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."key" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_key", "7f5f7a38bf327bfbbf826778460704c9a50fe6f4"."value" AS "7f5f7a38bf327bfbbf826778460704c9a50fe6f4_value" diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index da5ec1d4d1d53..eda91482bb1d0 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -8,6 +8,7 @@ SELECT "entity"."libraryId" AS "entity_libraryId", "entity"."deviceId" AS "entity_deviceId", "entity"."type" AS "entity_type", + "entity"."status" AS "entity_status", "entity"."originalPath" AS "entity_originalPath", "entity"."thumbhash" AS "entity_thumbhash", "entity"."encodedVideoPath" AS "entity_encodedVideoPath", @@ -96,6 +97,7 @@ SELECT "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -130,6 +132,7 @@ SELECT "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -218,6 +221,7 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."libraryId" AS "bd93d5747511a4dad4923546c51365bf1a803774_libraryId", "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", + "bd93d5747511a4dad4923546c51365bf1a803774"."status" AS "bd93d5747511a4dad4923546c51365bf1a803774_status", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", @@ -264,35 +268,6 @@ DELETE FROM "assets" WHERE "ownerId" = $1 --- AssetRepository.getExternalLibraryAssetPaths -SELECT DISTINCT - "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" -FROM - ( - SELECT - "AssetEntity"."id" AS "AssetEntity_id", - "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."isOffline" AS "AssetEntity_isOffline" - FROM - "assets" "AssetEntity" - LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId" - AND ( - "AssetEntity__AssetEntity_library"."deletedAt" IS NULL - ) - WHERE - ( - ( - ((("AssetEntity__AssetEntity_library"."id" = $1))) - AND ("AssetEntity"."isExternal" = $2) - ) - ) - AND ("AssetEntity"."deletedAt" IS NULL) - ) "distinctAlias" -ORDER BY - "AssetEntity_id" ASC -LIMIT - 2 - -- AssetRepository.getByLibraryIdAndOriginalPath SELECT DISTINCT "distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id" @@ -305,6 +280,7 @@ FROM "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -361,18 +337,6 @@ WHERE AND "originalPath" = path ); --- AssetRepository.updateOfflineLibraryAssets -UPDATE "assets" -SET - "isOffline" = $1, - "updatedAt" = CURRENT_TIMESTAMP -WHERE - ( - "libraryId" = $2 - AND NOT ("originalPath" IN ($3)) - AND "isOffline" = $4 - ) - -- AssetRepository.getAllByDeviceId SELECT "AssetEntity"."deviceAssetId" AS "AssetEntity_deviceAssetId", @@ -402,6 +366,7 @@ SELECT "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -455,6 +420,7 @@ SELECT "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -527,6 +493,7 @@ SELECT "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -581,6 +548,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -640,6 +608,7 @@ SELECT "stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", @@ -719,6 +688,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -778,6 +748,7 @@ SELECT "stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", @@ -833,6 +804,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -892,6 +864,7 @@ SELECT "stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", @@ -997,6 +970,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -1072,6 +1046,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -1134,109 +1109,6 @@ WHERE AND "asset"."ownerId" IN ($1) AND "asset"."updatedAt" > $2 --- AssetRepository.getAssetsByOriginalPath -SELECT - "asset"."id" AS "asset_id", - "asset"."deviceAssetId" AS "asset_deviceAssetId", - "asset"."ownerId" AS "asset_ownerId", - "asset"."libraryId" AS "asset_libraryId", - "asset"."deviceId" AS "asset_deviceId", - "asset"."type" AS "asset_type", - "asset"."originalPath" AS "asset_originalPath", - "asset"."thumbhash" AS "asset_thumbhash", - "asset"."encodedVideoPath" AS "asset_encodedVideoPath", - "asset"."createdAt" AS "asset_createdAt", - "asset"."updatedAt" AS "asset_updatedAt", - "asset"."deletedAt" AS "asset_deletedAt", - "asset"."fileCreatedAt" AS "asset_fileCreatedAt", - "asset"."localDateTime" AS "asset_localDateTime", - "asset"."fileModifiedAt" AS "asset_fileModifiedAt", - "asset"."isFavorite" AS "asset_isFavorite", - "asset"."isArchived" AS "asset_isArchived", - "asset"."isExternal" AS "asset_isExternal", - "asset"."isOffline" AS "asset_isOffline", - "asset"."checksum" AS "asset_checksum", - "asset"."duration" AS "asset_duration", - "asset"."isVisible" AS "asset_isVisible", - "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", - "asset"."originalFileName" AS "asset_originalFileName", - "asset"."sidecarPath" AS "asset_sidecarPath", - "asset"."stackId" AS "asset_stackId", - "asset"."duplicateId" AS "asset_duplicateId", - "exifInfo"."assetId" AS "exifInfo_assetId", - "exifInfo"."description" AS "exifInfo_description", - "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", - "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight", - "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte", - "exifInfo"."orientation" AS "exifInfo_orientation", - "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal", - "exifInfo"."modifyDate" AS "exifInfo_modifyDate", - "exifInfo"."timeZone" AS "exifInfo_timeZone", - "exifInfo"."latitude" AS "exifInfo_latitude", - "exifInfo"."longitude" AS "exifInfo_longitude", - "exifInfo"."projectionType" AS "exifInfo_projectionType", - "exifInfo"."city" AS "exifInfo_city", - "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID", - "exifInfo"."autoStackId" AS "exifInfo_autoStackId", - "exifInfo"."state" AS "exifInfo_state", - "exifInfo"."country" AS "exifInfo_country", - "exifInfo"."make" AS "exifInfo_make", - "exifInfo"."model" AS "exifInfo_model", - "exifInfo"."lensModel" AS "exifInfo_lensModel", - "exifInfo"."fNumber" AS "exifInfo_fNumber", - "exifInfo"."focalLength" AS "exifInfo_focalLength", - "exifInfo"."iso" AS "exifInfo_iso", - "exifInfo"."exposureTime" AS "exifInfo_exposureTime", - "exifInfo"."profileDescription" AS "exifInfo_profileDescription", - "exifInfo"."colorspace" AS "exifInfo_colorspace", - "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", - "exifInfo"."rating" AS "exifInfo_rating", - "exifInfo"."fps" AS "exifInfo_fps", - "stack"."id" AS "stack_id", - "stack"."ownerId" AS "stack_ownerId", - "stack"."primaryAssetId" AS "stack_primaryAssetId", - "stackedAssets"."id" AS "stackedAssets_id", - "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", - "stackedAssets"."ownerId" AS "stackedAssets_ownerId", - "stackedAssets"."libraryId" AS "stackedAssets_libraryId", - "stackedAssets"."deviceId" AS "stackedAssets_deviceId", - "stackedAssets"."type" AS "stackedAssets_type", - "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", - "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", - "stackedAssets"."createdAt" AS "stackedAssets_createdAt", - "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", - "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", - "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", - "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", - "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", - "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", - "stackedAssets"."isArchived" AS "stackedAssets_isArchived", - "stackedAssets"."isExternal" AS "stackedAssets_isExternal", - "stackedAssets"."isOffline" AS "stackedAssets_isOffline", - "stackedAssets"."checksum" AS "stackedAssets_checksum", - "stackedAssets"."duration" AS "stackedAssets_duration", - "stackedAssets"."isVisible" AS "stackedAssets_isVisible", - "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", - "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", - "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", - "stackedAssets"."stackId" AS "stackedAssets_stackId", - "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" -FROM - "assets" "asset" - LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" - LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" - LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" - AND ("stackedAssets"."deletedAt" IS NULL) -WHERE - "asset"."ownerId" = $1 - AND ( - "asset"."originalPath" LIKE $2 - AND "asset"."originalPath" NOT LIKE $3 - ) -ORDER BY - regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC - -- AssetRepository.upsertFile INSERT INTO "asset_files" ( @@ -1260,3 +1132,27 @@ RETURNING "id", "createdAt", "updatedAt" + +-- AssetRepository.upsertFiles +INSERT INTO + "asset_files" ( + "id", + "assetId", + "createdAt", + "updatedAt", + "type", + "path" + ) +VALUES + (DEFAULT, $1, DEFAULT, DEFAULT, $2, $3) +ON CONFLICT ("assetId", "type") DO +UPDATE +SET + "assetId" = EXCLUDED."assetId", + "type" = EXCLUDED."type", + "path" = EXCLUDED."path", + "updatedAt" = DEFAULT +RETURNING + "id", + "createdAt", + "updatedAt" diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index 5dd32ce365d9e..a5d6ba05dba1f 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -28,7 +28,8 @@ FROM "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -68,7 +69,8 @@ SELECT "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" @@ -104,7 +106,8 @@ SELECT "LibraryEntity__LibraryEntity_owner"."status" AS "LibraryEntity__LibraryEntity_owner_status", "LibraryEntity__LibraryEntity_owner"."updatedAt" AS "LibraryEntity__LibraryEntity_owner_updatedAt", "LibraryEntity__LibraryEntity_owner"."quotaSizeInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaSizeInBytes", - "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes" + "LibraryEntity__LibraryEntity_owner"."quotaUsageInBytes" AS "LibraryEntity__LibraryEntity_owner_quotaUsageInBytes", + "LibraryEntity__LibraryEntity_owner"."profileChangedAt" AS "LibraryEntity__LibraryEntity_owner_profileChangedAt" FROM "libraries" "LibraryEntity" LEFT JOIN "users" "LibraryEntity__LibraryEntity_owner" ON "LibraryEntity__LibraryEntity_owner"."id" = "LibraryEntity"."ownerId" diff --git a/server/src/queries/metadata.repository.sql b/server/src/queries/metadata.repository.sql deleted file mode 100644 index 212527432072b..0000000000000 --- a/server/src/queries/metadata.repository.sql +++ /dev/null @@ -1,56 +0,0 @@ --- NOTE: This file is auto generated by ./sql-generator - --- MetadataRepository.getCountries -SELECT DISTINCT - ON ("exif"."country") "exif"."country" AS "country" -FROM - "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" - AND ("asset"."deletedAt" IS NULL) -WHERE - "asset"."ownerId" IN ($1) - --- MetadataRepository.getStates -SELECT DISTINCT - ON ("exif"."state") "exif"."state" AS "state" -FROM - "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" - AND ("asset"."deletedAt" IS NULL) -WHERE - "asset"."ownerId" IN ($1) - AND "exif"."country" = $2 - --- MetadataRepository.getCities -SELECT DISTINCT - ON ("exif"."city") "exif"."city" AS "city" -FROM - "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" - AND ("asset"."deletedAt" IS NULL) -WHERE - "asset"."ownerId" IN ($1) - AND "exif"."country" = $2 - AND "exif"."state" = $3 - --- MetadataRepository.getCameraMakes -SELECT DISTINCT - ON ("exif"."make") "exif"."make" AS "make" -FROM - "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" - AND ("asset"."deletedAt" IS NULL) -WHERE - "asset"."ownerId" IN ($1) - AND "exif"."model" = $2 - --- MetadataRepository.getCameraModels -SELECT DISTINCT - ON ("exif"."model") "exif"."model" AS "model" -FROM - "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" - AND ("asset"."deletedAt" IS NULL) -WHERE - "asset"."ownerId" IN ($1) - AND "exif"."make" = $2 diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 57969e4989c91..fa8b0910b4ff8 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -159,6 +159,7 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", + "AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", @@ -215,19 +216,14 @@ SELECT "person"."isHidden" AS "person_isHidden" FROM "person" "person" - LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" WHERE "person"."ownerId" = $1 AND ( LOWER("person"."name") LIKE $2 OR LOWER("person"."name") LIKE $3 ) -GROUP BY - "person"."id" -ORDER BY - COUNT("face"."assetId") DESC LIMIT - 20 + 1000 -- PersonRepository.getDistinctNames SELECT DISTINCT @@ -265,6 +261,7 @@ FROM "AssetEntity"."libraryId" AS "AssetEntity_libraryId", "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", + "AssetEntity"."status" AS "AssetEntity_status", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", @@ -396,6 +393,7 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."libraryId" AS "AssetFaceEntity__AssetFaceEntity_asset_libraryId", "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", + "AssetFaceEntity__AssetFaceEntity_asset"."status" AS "AssetFaceEntity__AssetFaceEntity_asset_status", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index dd2e3ae75c819..cd9a84b016d0f 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -13,6 +13,7 @@ FROM "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -43,6 +44,7 @@ FROM "stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", @@ -75,10 +77,11 @@ FROM "asset"."fileCreatedAt" >= $1 AND "exifInfo"."lensModel" = $2 AND 1 = 1 + AND "asset"."ownerId" IN ($3) AND 1 = 1 AND ( - "asset"."isFavorite" = $3 - AND "asset"."isArchived" = $4 + "asset"."isFavorite" = $4 + AND "asset"."isArchived" = $5 ) ) AND ("asset"."deletedAt" IS NULL) @@ -89,6 +92,190 @@ ORDER BY LIMIT 101 +-- SearchRepository.searchRandom +SELECT DISTINCT + "distinctAlias"."asset_id" AS "ids_asset_id", + "distinctAlias"."asset_id" +FROM + ( + SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", + "asset"."originalPath" AS "asset_originalPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "asset"."duplicateId" AS "asset_duplicateId", + "stack"."id" AS "stack_id", + "stack"."ownerId" AS "stack_ownerId", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId", + "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" + FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) + WHERE + ( + "asset"."fileCreatedAt" >= $1 + AND "exifInfo"."lensModel" = $2 + AND 1 = 1 + AND "asset"."ownerId" IN ($3) + AND 1 = 1 + AND ( + "asset"."isFavorite" = $4 + AND "asset"."isArchived" = $5 + ) + AND "asset"."id" > $6 + ) + AND ("asset"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "distinctAlias"."asset_id" ASC, + "asset_id" ASC +LIMIT + 100 +SELECT DISTINCT + "distinctAlias"."asset_id" AS "ids_asset_id", + "distinctAlias"."asset_id" +FROM + ( + SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", + "asset"."originalPath" AS "asset_originalPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "asset"."duplicateId" AS "asset_duplicateId", + "stack"."id" AS "stack_id", + "stack"."ownerId" AS "stack_ownerId", + "stack"."primaryAssetId" AS "stack_primaryAssetId", + "stackedAssets"."id" AS "stackedAssets_id", + "stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId", + "stackedAssets"."ownerId" AS "stackedAssets_ownerId", + "stackedAssets"."libraryId" AS "stackedAssets_libraryId", + "stackedAssets"."deviceId" AS "stackedAssets_deviceId", + "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", + "stackedAssets"."originalPath" AS "stackedAssets_originalPath", + "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", + "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", + "stackedAssets"."createdAt" AS "stackedAssets_createdAt", + "stackedAssets"."updatedAt" AS "stackedAssets_updatedAt", + "stackedAssets"."deletedAt" AS "stackedAssets_deletedAt", + "stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt", + "stackedAssets"."localDateTime" AS "stackedAssets_localDateTime", + "stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt", + "stackedAssets"."isFavorite" AS "stackedAssets_isFavorite", + "stackedAssets"."isArchived" AS "stackedAssets_isArchived", + "stackedAssets"."isExternal" AS "stackedAssets_isExternal", + "stackedAssets"."isOffline" AS "stackedAssets_isOffline", + "stackedAssets"."checksum" AS "stackedAssets_checksum", + "stackedAssets"."duration" AS "stackedAssets_duration", + "stackedAssets"."isVisible" AS "stackedAssets_isVisible", + "stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId", + "stackedAssets"."originalFileName" AS "stackedAssets_originalFileName", + "stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath", + "stackedAssets"."stackId" AS "stackedAssets_stackId", + "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" + FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" + LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" + LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" + AND ("stackedAssets"."deletedAt" IS NULL) + WHERE + ( + "asset"."fileCreatedAt" >= $1 + AND "exifInfo"."lensModel" = $2 + AND 1 = 1 + AND "asset"."ownerId" IN ($3) + AND 1 = 1 + AND ( + "asset"."isFavorite" = $4 + AND "asset"."isArchived" = $5 + ) + AND "asset"."id" < $6 + ) + AND ("asset"."deletedAt" IS NULL) + ) "distinctAlias" +ORDER BY + "distinctAlias"."asset_id" ASC, + "asset_id" ASC +LIMIT + 100 + -- SearchRepository.searchSmart START TRANSACTION SET @@ -106,6 +293,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -136,6 +324,7 @@ SELECT "stackedAssets"."libraryId" AS "stackedAssets_libraryId", "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", + "stackedAssets"."status" AS "stackedAssets_status", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", @@ -345,6 +534,7 @@ SELECT "asset"."libraryId" AS "asset_libraryId", "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", "asset"."originalPath" AS "asset_originalPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", @@ -401,3 +591,58 @@ FROM INNER JOIN cte ON asset.id = cte."assetId" ORDER BY exif.city + +-- SearchRepository.getCountries +SELECT DISTINCT + ON ("exif"."country") "exif"."country" AS "country" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" IN ($1) + +-- SearchRepository.getStates +SELECT DISTINCT + ON ("exif"."state") "exif"."state" AS "state" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" IN ($1) + AND "exif"."country" = $2 + +-- SearchRepository.getCities +SELECT DISTINCT + ON ("exif"."city") "exif"."city" AS "city" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" IN ($1) + AND "exif"."country" = $2 + AND "exif"."state" = $3 + +-- SearchRepository.getCameraMakes +SELECT DISTINCT + ON ("exif"."make") "exif"."make" AS "make" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" IN ($1) + AND "exif"."model" = $2 + +-- SearchRepository.getCameraModels +SELECT DISTINCT + ON ("exif"."model") "exif"."model" AS "model" +FROM + "exif" "exif" + LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + AND ("asset"."deletedAt" IS NULL) +WHERE + "asset"."ownerId" IN ($1) + AND "exif"."make" = $2 diff --git a/server/src/queries/session.repository.sql b/server/src/queries/session.repository.sql index 17fff94f42ac8..2f0613b4d0398 100644 --- a/server/src/queries/session.repository.sql +++ b/server/src/queries/session.repository.sql @@ -39,6 +39,7 @@ FROM "SessionEntity__SessionEntity_user"."updatedAt" AS "SessionEntity__SessionEntity_user_updatedAt", "SessionEntity__SessionEntity_user"."quotaSizeInBytes" AS "SessionEntity__SessionEntity_user_quotaSizeInBytes", "SessionEntity__SessionEntity_user"."quotaUsageInBytes" AS "SessionEntity__SessionEntity_user_quotaUsageInBytes", + "SessionEntity__SessionEntity_user"."profileChangedAt" AS "SessionEntity__SessionEntity_user_profileChangedAt", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."userId" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_userId", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."key" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_key", "469e6aa7ff79eff78f8441f91ba15bb07d3634dd"."value" AS "469e6aa7ff79eff78f8441f91ba15bb07d3634dd_value" diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 10af8d17dbddb..a19b698f765a4 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -27,6 +27,7 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", + "SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", @@ -93,6 +94,7 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."libraryId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_libraryId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", + "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."status" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_status", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", @@ -156,7 +158,8 @@ FROM "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -213,6 +216,7 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."libraryId" AS "SharedLinkEntity__SharedLinkEntity_assets_libraryId", "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", + "SharedLinkEntity__SharedLinkEntity_assets"."status" AS "SharedLinkEntity__SharedLinkEntity_assets_status", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", @@ -257,7 +261,8 @@ SELECT "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."status" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_status", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."updatedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_updatedAt", "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaSizeInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaSizeInBytes", - "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes" + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."quotaUsageInBytes" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_quotaUsageInBytes", + "6d7fd45329a05fd86b3dbcacde87fe76e33a422d"."profileChangedAt" AS "6d7fd45329a05fd86b3dbcacde87fe76e33a422d_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "shared_link__asset" "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity" ON "SharedLinkEntity__SharedLinkEntity_assets_SharedLinkEntity"."sharedLinksId" = "SharedLinkEntity"."id" @@ -309,7 +314,8 @@ FROM "SharedLinkEntity__SharedLinkEntity_user"."status" AS "SharedLinkEntity__SharedLinkEntity_user_status", "SharedLinkEntity__SharedLinkEntity_user"."updatedAt" AS "SharedLinkEntity__SharedLinkEntity_user_updatedAt", "SharedLinkEntity__SharedLinkEntity_user"."quotaSizeInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaSizeInBytes", - "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes" + "SharedLinkEntity__SharedLinkEntity_user"."quotaUsageInBytes" AS "SharedLinkEntity__SharedLinkEntity_user_quotaUsageInBytes", + "SharedLinkEntity__SharedLinkEntity_user"."profileChangedAt" AS "SharedLinkEntity__SharedLinkEntity_user_profileChangedAt" FROM "shared_links" "SharedLinkEntity" LEFT JOIN "users" "SharedLinkEntity__SharedLinkEntity_user" ON "SharedLinkEntity__SharedLinkEntity_user"."id" = "SharedLinkEntity"."userId" diff --git a/server/src/queries/user.repository.sql b/server/src/queries/user.repository.sql index 2c75786f975b9..ab0a6cc534bd0 100644 --- a/server/src/queries/user.repository.sql +++ b/server/src/queries/user.repository.sql @@ -15,7 +15,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE @@ -60,7 +61,8 @@ SELECT "user"."status" AS "user_status", "user"."updatedAt" AS "user_updatedAt", "user"."quotaSizeInBytes" AS "user_quotaSizeInBytes", - "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes" + "user"."quotaUsageInBytes" AS "user_quotaUsageInBytes", + "user"."profileChangedAt" AS "user_profileChangedAt" FROM "users" "user" WHERE @@ -82,7 +84,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE @@ -106,7 +109,8 @@ SELECT "UserEntity"."status" AS "UserEntity_status", "UserEntity"."updatedAt" AS "UserEntity_updatedAt", "UserEntity"."quotaSizeInBytes" AS "UserEntity_quotaSizeInBytes", - "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes" + "UserEntity"."quotaUsageInBytes" AS "UserEntity_quotaUsageInBytes", + "UserEntity"."profileChangedAt" AS "UserEntity_profileChangedAt" FROM "users" "UserEntity" WHERE diff --git a/server/src/queries/view.repository.sql b/server/src/queries/view.repository.sql new file mode 100644 index 0000000000000..e5b88ffef98d4 --- /dev/null +++ b/server/src/queries/view.repository.sql @@ -0,0 +1,79 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- ViewRepository.getAssetsByOriginalPath +SELECT + "asset"."id" AS "asset_id", + "asset"."deviceAssetId" AS "asset_deviceAssetId", + "asset"."ownerId" AS "asset_ownerId", + "asset"."libraryId" AS "asset_libraryId", + "asset"."deviceId" AS "asset_deviceId", + "asset"."type" AS "asset_type", + "asset"."status" AS "asset_status", + "asset"."originalPath" AS "asset_originalPath", + "asset"."thumbhash" AS "asset_thumbhash", + "asset"."encodedVideoPath" AS "asset_encodedVideoPath", + "asset"."createdAt" AS "asset_createdAt", + "asset"."updatedAt" AS "asset_updatedAt", + "asset"."deletedAt" AS "asset_deletedAt", + "asset"."fileCreatedAt" AS "asset_fileCreatedAt", + "asset"."localDateTime" AS "asset_localDateTime", + "asset"."fileModifiedAt" AS "asset_fileModifiedAt", + "asset"."isFavorite" AS "asset_isFavorite", + "asset"."isArchived" AS "asset_isArchived", + "asset"."isExternal" AS "asset_isExternal", + "asset"."isOffline" AS "asset_isOffline", + "asset"."checksum" AS "asset_checksum", + "asset"."duration" AS "asset_duration", + "asset"."isVisible" AS "asset_isVisible", + "asset"."livePhotoVideoId" AS "asset_livePhotoVideoId", + "asset"."originalFileName" AS "asset_originalFileName", + "asset"."sidecarPath" AS "asset_sidecarPath", + "asset"."stackId" AS "asset_stackId", + "asset"."duplicateId" AS "asset_duplicateId", + "exifInfo"."assetId" AS "exifInfo_assetId", + "exifInfo"."description" AS "exifInfo_description", + "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", + "exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight", + "exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte", + "exifInfo"."orientation" AS "exifInfo_orientation", + "exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal", + "exifInfo"."modifyDate" AS "exifInfo_modifyDate", + "exifInfo"."timeZone" AS "exifInfo_timeZone", + "exifInfo"."latitude" AS "exifInfo_latitude", + "exifInfo"."longitude" AS "exifInfo_longitude", + "exifInfo"."projectionType" AS "exifInfo_projectionType", + "exifInfo"."city" AS "exifInfo_city", + "exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID", + "exifInfo"."autoStackId" AS "exifInfo_autoStackId", + "exifInfo"."state" AS "exifInfo_state", + "exifInfo"."country" AS "exifInfo_country", + "exifInfo"."make" AS "exifInfo_make", + "exifInfo"."model" AS "exifInfo_model", + "exifInfo"."lensModel" AS "exifInfo_lensModel", + "exifInfo"."fNumber" AS "exifInfo_fNumber", + "exifInfo"."focalLength" AS "exifInfo_focalLength", + "exifInfo"."iso" AS "exifInfo_iso", + "exifInfo"."exposureTime" AS "exifInfo_exposureTime", + "exifInfo"."profileDescription" AS "exifInfo_profileDescription", + "exifInfo"."colorspace" AS "exifInfo_colorspace", + "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", + "exifInfo"."rating" AS "exifInfo_rating", + "exifInfo"."fps" AS "exifInfo_fps" +FROM + "assets" "asset" + LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" +WHERE + ( + ( + "asset"."isVisible" = $1 + AND "asset"."isArchived" = $2 + AND "asset"."ownerId" = $3 + ) + AND ( + "asset"."originalPath" LIKE $4 + AND "asset"."originalPath" NOT LIKE $5 + ) + ) + AND ("asset"."deletedAt" IS NULL) +ORDER BY + regexp_replace("asset"."originalPath", '.*/(.+)', '\1') ASC diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts index fd3a89993a6bd..f7b4cb44aa976 100644 --- a/server/src/repositories/album.repository.ts +++ b/server/src/repositories/album.repository.ts @@ -57,22 +57,6 @@ export class AlbumRepository implements IAlbumRepository { return withoutDeletedUsers(album); } - @GenerateSql({ params: [[DummyValue.UUID]] }) - @ChunkedArray() - async getByIds(ids: string[]): Promise { - const albums = await this.repository.find({ - where: { - id: In(ids), - }, - relations: { - owner: true, - albumUsers: { user: true }, - }, - }); - - return albums.map((album) => withoutDeletedUsers(album)); - } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID] }) async getByAssetId(ownerId: string, assetId: string): Promise { const albums = await this.repository.find({ @@ -116,34 +100,6 @@ export class AlbumRepository implements IAlbumRepository { })); } - /** - * Returns the album IDs that have an invalid thumbnail, when: - * - Thumbnail references an asset outside the album - * - Empty album still has a thumbnail set - */ - @GenerateSql() - async getInvalidThumbnail(): Promise { - // Using dataSource, because there is no direct access to albums_assets_assets. - const albumHasAssets = this.dataSource - .createQueryBuilder() - .select('1') - .from('albums_assets_assets', 'albums_assets') - .where('"albums"."id" = "albums_assets"."albumsId"'); - - const albumContainsThumbnail = albumHasAssets - .clone() - .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"'); - - const albums = await this.repository - .createQueryBuilder('albums') - .select('albums.id') - .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`) - .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`) - .getMany(); - - return albums.map((album) => album.id); - } - @GenerateSql({ params: [DummyValue.UUID] }) async getOwned(ownerId: string): Promise { const albums = await this.repository.find({ @@ -199,15 +155,6 @@ export class AlbumRepository implements IAlbumRepository { await this.repository.delete({ ownerId: userId }); } - @GenerateSql() - getAll(): Promise { - return this.repository.find({ - relations: { - owner: true, - }, - }); - } - @GenerateSql({ params: [DummyValue.UUID] }) async removeAsset(assetId: string): Promise { // Using dataSource, because there is no direct access to albums_assets_assets. @@ -330,32 +277,26 @@ export class AlbumRepository implements IAlbumRepository { @GenerateSql() async updateThumbnails(): Promise { // Subquery for getting a new thumbnail. - const newThumbnail = this.assetRepository - .createQueryBuilder('assets') - .select('albums_assets2.assetsId') - .addFrom('albums_assets_assets', 'albums_assets2') - .where('albums_assets2.assetsId = assets.id') - .andWhere('albums_assets2.albumsId = "albums"."id"') // Reference to albums.id outside this query - .orderBy('assets.fileCreatedAt', 'DESC') - .limit(1); - // Using dataSource, because there is no direct access to albums_assets_assets. - const albumHasAssets = this.dataSource - .createQueryBuilder() - .select('1') - .from('albums_assets_assets', 'albums_assets') - .where('"albums"."id" = "albums_assets"."albumsId"'); + const builder = this.dataSource + .createQueryBuilder('albums_assets_assets', 'album_assets') + .innerJoin('assets', 'assets', '"album_assets"."assetsId" = "assets"."id"') + .where('"album_assets"."albumsId" = "albums"."id"'); - const albumContainsThumbnail = albumHasAssets + const newThumbnail = builder .clone() - .andWhere('"albums"."albumThumbnailAssetId" = "albums_assets"."assetsId"'); + .select('"album_assets"."assetsId"') + .orderBy('"assets"."fileCreatedAt"', 'DESC') + .limit(1); + const hasAssets = builder.clone().select('1'); + const hasInvalidAsset = hasAssets.clone().andWhere('"albums"."albumThumbnailAssetId" = "album_assets"."assetsId"'); const updateAlbums = this.repository .createQueryBuilder('albums') .update(AlbumEntity) .set({ albumThumbnailAssetId: () => `(${newThumbnail.getQuery()})` }) - .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${albumHasAssets.getQuery()})`) - .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${albumContainsThumbnail.getQuery()})`); + .where(`"albums"."albumThumbnailAssetId" IS NULL AND EXISTS (${hasAssets.getQuery()})`) + .orWhere(`"albums"."albumThumbnailAssetId" IS NOT NULL AND NOT EXISTS (${hasInvalidAsset.getQuery()})`); const result = await updateAlbums.execute(); diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 059a05f9e770d..8bca755c32e26 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -6,14 +6,13 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; -import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetStatus, AssetType, PaginationMode } from 'src/enum'; import { AssetBuilderOptions, AssetCreate, AssetDeltaSyncOptions, AssetExploreFieldOptions, AssetFullSyncOptions, - AssetPathEntity, AssetStats, AssetStatsOptions, AssetUpdateAllOptions, @@ -31,7 +30,7 @@ import { import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { searchAssetBuilder } from 'src/utils/database'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; +import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; import { Brackets, FindOptionsOrder, @@ -177,14 +176,6 @@ export class AssetRepository implements IAssetRepository { return this.getAll(pagination, { ...options, userIds: [userId] }); } - @GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] }) - getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated { - return paginate(this.repository, pagination, { - select: { id: true, originalPath: true, isOffline: true }, - where: { library: { id: libraryId }, isExternal: true }, - }); - } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise { return this.repository.findOne({ @@ -198,24 +189,16 @@ export class AssetRepository implements IAssetRepository { async getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise { const result = await this.repository.query( ` - WITH paths AS (SELECT unnest($2::text[]) AS path) - SELECT path FROM paths - WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path); - `, + WITH paths AS (SELECT unnest($2::text[]) AS path) + SELECT path + FROM paths + WHERE NOT EXISTS (SELECT 1 FROM assets WHERE "libraryId" = $1 AND "originalPath" = path); + `, [libraryId, originalPaths], ); return result.map((row: { path: string }) => row.path); } - @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) - @ChunkedArray({ paramIndex: 1 }) - async updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise { - await this.repository.update( - { library: { id: libraryId }, originalPath: Not(In(originalPaths)), isOffline: false }, - { isOffline: true }, - ); - } - getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files'); builder = searchAssetBuilder(builder, options); @@ -295,16 +278,6 @@ export class AssetRepository implements IAssetRepository { .execute(); } - @Chunked() - async softDeleteAll(ids: string[]): Promise { - await this.repository.softDelete({ id: In(ids) }); - } - - @Chunked() - async restoreAll(ids: string[]): Promise { - await this.repository.restore({ id: In(ids) }); - } - async update(asset: AssetUpdateOptions): Promise { await this.repository.update(asset.id, asset); } @@ -383,12 +356,10 @@ export class AssetRepository implements IAssetRepository { } @GenerateSql( - ...Object.values(WithProperty) - .filter((property) => property !== WithProperty.IS_OFFLINE && property !== WithProperty.IS_ONLINE) - .map((property) => ({ - name: property, - params: [DummyValue.PAGINATION, property], - })), + ...Object.values(WithProperty).map((property) => ({ + name: property, + params: [DummyValue.PAGINATION, property], + })), ) getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated { let relations: FindOptionsRelations = {}; @@ -541,26 +512,16 @@ export class AssetRepository implements IAssetRepository { where = [{ sidecarPath: Not(IsNull()), isVisible: true }]; break; } - case WithProperty.IS_OFFLINE: { - if (!libraryId) { - throw new Error('Library id is required when finding offline assets'); - } - where = [{ isOffline: true, libraryId }]; - break; - } - case WithProperty.IS_ONLINE: { - if (!libraryId) { - throw new Error('Library id is required when finding online assets'); - } - where = [{ isOffline: false, libraryId }]; - break; - } default: { throw new Error(`Invalid getWith property: ${property}`); } } + if (libraryId) { + where = [{ ...where, libraryId }]; + } + return paginate(this.repository, pagination, { where, withDeleted, @@ -571,13 +532,6 @@ export class AssetRepository implements IAssetRepository { }); } - getFirstAssetForAlbumId(albumId: string): Promise { - return this.repository.findOne({ - where: { albums: { id: albumId } }, - order: { fileCreatedAt: 'DESC' }, - }); - } - getLastUpdatedAssetForAlbumId(albumId: string): Promise { return this.repository.findOne({ where: { albums: { id: albumId } }, @@ -604,7 +558,10 @@ export class AssetRepository implements IAssetRepository { } if (isTrashed !== undefined) { - builder.withDeleted().andWhere(`asset.deletedAt is not null`); + builder + .withDeleted() + .andWhere(`asset.deletedAt is not null`) + .andWhere('asset.status = :status', { status: AssetStatus.TRASHED }); } const items = await builder.getRawMany(); @@ -623,14 +580,9 @@ export class AssetRepository implements IAssetRepository { return result; } - @GenerateSql({ params: [DummyValue.UUID, DummyValue.NUMBER] }) - getRandom(ownerId: string, count: number): Promise { - const builder = this.getBuilder({ - userIds: [ownerId], - exifInfo: true, - }); - - return builder.orderBy('RANDOM()').limit(count).getMany(); + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.NUMBER] }) + getRandom(userIds: string[], count: number): Promise { + return this.getBuilder({ userIds, exifInfo: true }).orderBy('RANDOM()').limit(count).getMany(); } @GenerateSql({ params: [{ size: TimeBucketSize.MONTH }] }) @@ -767,6 +719,13 @@ export class AssetRepository implements IAssetRepository { if (options.isTrashed !== undefined) { builder.andWhere(`asset.deletedAt ${options.isTrashed ? 'IS NOT NULL' : 'IS NULL'}`).withDeleted(); + + if (options.isTrashed) { + // TODO: Temporarily inverted to support showing offline assets in the trash queries. + // Once offline assets are handled in a separate screen, this should be set back to status = TRASHED + // and the offline screens should use a separate isOffline = true parameter in the timeline query. + builder.andWhere('asset.status != :status', { status: AssetStatus.DELETED }); + } } if (options.isDuplicate !== undefined) { @@ -841,52 +800,13 @@ export class AssetRepository implements IAssetRepository { return builder.getMany(); } - async getUniqueOriginalPaths(userId: string): Promise { - const builder = this.getBuilder({ - userIds: [userId], - exifInfo: false, - withStacked: false, - isArchived: false, - isTrashed: false, - }); - - const results = await builder - .select("DISTINCT substring(asset.originalPath FROM '^(.*/)[^/]*$')", 'directoryPath') - .getRawMany(); - - return results.map((row: { directoryPath: string }) => row.directoryPath.replaceAll(/^\/|\/$/g, '')); - } - - @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) - async getAssetsByOriginalPath(userId: string, partialPath: string): Promise { - const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, ''); - - const builder = this.getBuilder({ - userIds: [userId], - exifInfo: true, - withStacked: false, - isArchived: false, - isTrashed: false, - }); - - const assets = await builder - .where('asset.ownerId = :userId', { userId }) - .andWhere( - new Brackets((qb) => { - qb.where('asset.originalPath LIKE :likePath', { likePath: `%${normalizedPath}/%` }).andWhere( - 'asset.originalPath NOT LIKE :notLikePath', - { notLikePath: `%${normalizedPath}/%/%` }, - ); - }), - ) - .orderBy(String.raw`regexp_replace(asset.originalPath, '.*/(.+)', '\1')`, 'ASC') - .getMany(); - - return assets; + @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] }) + async upsertFile(file: { assetId: string; type: AssetFileType; path: string }): Promise { + await this.fileRepository.upsert(file, { conflictPaths: ['assetId', 'type'] }); } @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] }) - async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise { - await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] }); + async upsertFiles(files: { assetId: string; type: AssetFileType; path: string }[]): Promise { + await this.fileRepository.upsert(files, { conflictPaths: ['assetId', 'type'] }); } } diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts new file mode 100644 index 0000000000000..f16fa3bbd4ef9 --- /dev/null +++ b/server/src/repositories/config.repository.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; +import { getVectorExtension } from 'src/database.config'; +import { EnvData, IConfigRepository } from 'src/interfaces/config.interface'; + +@Injectable() +export class ConfigRepository implements IConfigRepository { + getEnv(): EnvData { + return { + database: { + skipMigrations: process.env.DB_SKIP_MIGRATIONS === 'true', + vectorExtension: getVectorExtension(), + }, + }; + } +} diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 3be6b375a087d..fac250d6670d7 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -5,6 +5,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; +import { IConfigRepository } from 'src/interfaces/config.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; @@ -29,7 +30,9 @@ import { IStackRepository } from 'src/interfaces/stack.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; +import { ITrashRepository } from 'src/interfaces/trash.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { IViewRepository } from 'src/interfaces/view.interface'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -37,6 +40,7 @@ import { AlbumRepository } from 'src/repositories/album.repository'; import { ApiKeyRepository } from 'src/repositories/api-key.repository'; import { AssetRepository } from 'src/repositories/asset.repository'; import { AuditRepository } from 'src/repositories/audit.repository'; +import { ConfigRepository } from 'src/repositories/config.repository'; import { CryptoRepository } from 'src/repositories/crypto.repository'; import { DatabaseRepository } from 'src/repositories/database.repository'; import { EventRepository } from 'src/repositories/event.repository'; @@ -61,7 +65,9 @@ import { StackRepository } from 'src/repositories/stack.repository'; import { StorageRepository } from 'src/repositories/storage.repository'; import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository'; import { TagRepository } from 'src/repositories/tag.repository'; +import { TrashRepository } from 'src/repositories/trash.repository'; import { UserRepository } from 'src/repositories/user.repository'; +import { ViewRepository } from 'src/repositories/view-repository'; export const repositories = [ { provide: IAccessRepository, useClass: AccessRepository }, @@ -70,6 +76,7 @@ export const repositories = [ { provide: IAlbumUserRepository, useClass: AlbumUserRepository }, { provide: IAssetRepository, useClass: AssetRepository }, { provide: IAuditRepository, useClass: AuditRepository }, + { provide: IConfigRepository, useClass: ConfigRepository }, { provide: ICryptoRepository, useClass: CryptoRepository }, { provide: IDatabaseRepository, useClass: DatabaseRepository }, { provide: IEventRepository, useClass: EventRepository }, @@ -95,5 +102,7 @@ export const repositories = [ { provide: IStorageRepository, useClass: StorageRepository }, { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, + { provide: ITrashRepository, useClass: TrashRepository }, { provide: IUserRepository, useClass: UserRepository }, + { provide: IViewRepository, useClass: ViewRepository }, ]; diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index f64e5175e5127..3f154ee01615a 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -36,11 +36,12 @@ export const JOBS_TO_QUEUE: Record = { // thumbnails [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_PREVIEW]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, - [JobName.GENERATE_THUMBHASH]: QueueName.THUMBNAIL_GENERATION, + [JobName.GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION, [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION, + // tags + [JobName.TAG_CLEANUP]: QueueName.BACKGROUND_TASK, + // metadata [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION, @@ -76,12 +77,12 @@ export const JOBS_TO_QUEUE: Record = { [JobName.SIDECAR_WRITE]: QueueName.SIDECAR, // Library management - [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, - [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, + [JobName.LIBRARY_SYNC_FILE]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_SYNC_FILES]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_SYNC_ASSETS]: QueueName.LIBRARY, [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, - [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY, - [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, - [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, + [JobName.LIBRARY_SYNC_ASSET]: QueueName.LIBRARY, + [JobName.LIBRARY_QUEUE_SYNC_ALL]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, // Notification @@ -92,6 +93,9 @@ export const JOBS_TO_QUEUE: Record = { // Version check [JobName.VERSION_CHECK]: QueueName.BACKGROUND_TASK, + + // Trash + [JobName.QUEUE_TRASH_EMPTY]: QueueName.BACKGROUND_TASK, }; @Instrumentation() @@ -150,10 +154,6 @@ export class JobRepository implements IJobRepository { } } - deleteCronJob(name: string): void { - this.schedulerReqistry.deleteCronJob(name); - } - setConcurrency(queueName: QueueName, concurrency: number) { const worker = this.workers[queueName]; if (!worker) { diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts index 1e0c7b74d973e..1d7e734e735e6 100644 --- a/server/src/repositories/logger.repository.ts +++ b/server/src/repositories/logger.repository.ts @@ -1,7 +1,7 @@ import { ConsoleLogger, Injectable, Scope } from '@nestjs/common'; import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-enabled.util'; import { ClsService } from 'nestjs-cls'; -import { LogLevel } from 'src/config'; +import { LogLevel } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { LogColor } from 'src/utils/logger'; diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index a84ef6f596f4e..cca87f44f2b81 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -1,26 +1,41 @@ import { Inject, Injectable } from '@nestjs/common'; import { exiftool } from 'exiftool-vendored'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; +import { Duration } from 'luxon'; import fs from 'node:fs/promises'; import { Writable } from 'node:stream'; -import { promisify } from 'node:util'; import sharp from 'sharp'; -import { Colorspace } from 'src/config'; +import { Colorspace, LogLevel } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { + DecodeToBufferOptions, + GenerateThumbhashOptions, + GenerateThumbnailOptions, IMediaRepository, ImageDimensions, - ThumbnailOptions, + ProbeOptions, TranscodeCommand, VideoInfo, } from 'src/interfaces/media.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { handlePromiseError } from 'src/utils/misc'; -const probe = promisify(ffmpeg.ffprobe); +const probe = (input: string, options: string[]): Promise => + new Promise((resolve, reject) => + ffmpeg.ffprobe(input, options, (error, data) => (error ? reject(error) : resolve(data))), + ); sharp.concurrency(0); sharp.cache({ files: 0 }); +type ProgressEvent = { + frames: number; + currentFps: number; + currentKbps: number; + targetSize: number; + timemark: string; + percent?: number; +}; + @Instrumentation() @Injectable() export class MediaRepository implements IMediaRepository { @@ -44,19 +59,12 @@ export class MediaRepository implements IMediaRepository { return true; } - async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise { - // some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes - const pipeline = sharp(input, { failOn: options.processInvalidImages ? 'none' : 'error', limitInputPixels: false }) - .pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16') - .rotate(); + decodeImage(input: string, options: DecodeToBufferOptions) { + return this.getImageDecodingPipeline(input, options).raw().toBuffer({ resolveWithObject: true }); + } - if (options.crop) { - pipeline.extract(options.crop); - } - - await pipeline - .resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }) - .withIccProfile(options.colorspace) + async generateThumbnail(input: string | Buffer, options: GenerateThumbnailOptions, output: string): Promise { + await this.getImageDecodingPipeline(input, options) .toFormat(options.format, { quality: options.quality, // this is default in libvips (except the threshold is 90), but we need to set it manually in sharp @@ -65,8 +73,42 @@ export class MediaRepository implements IMediaRepository { .toFile(output); } - async probe(input: string): Promise { - const results = await probe(input); + private getImageDecodingPipeline(input: string | Buffer, options: DecodeToBufferOptions) { + let pipeline = sharp(input, { + // some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes + failOn: options.processInvalidImages ? 'none' : 'error', + limitInputPixels: false, + raw: options.raw, + }); + + if (!options.raw) { + pipeline = pipeline + .pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16') + .withIccProfile(options.colorspace) + .rotate(); + } + + if (options.crop) { + pipeline = pipeline.extract(options.crop); + } + + return pipeline.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true }); + } + + async generateThumbhash(input: string | Buffer, options: GenerateThumbhashOptions): Promise { + const [{ rgbaToThumbHash }, { data, info }] = await Promise.all([ + import('thumbhash'), + sharp(input, options) + .resize(100, 100, { fit: 'inside', withoutEnlargement: true }) + .raw() + .ensureAlpha() + .toBuffer({ resolveWithObject: true }), + ]); + return Buffer.from(rgbaToThumbHash(info.width, info.height, data)); + } + + async probe(input: string, options?: ProbeOptions): Promise { + const results = await probe(input, options?.countFrames ? ['-count_packets'] : []); // gets frame count quickly: https://stackoverflow.com/a/28376817 return { format: { formatName: results.format.format_name, @@ -83,10 +125,10 @@ export class MediaRepository implements IMediaRepository { width: stream.width || 0, codecName: stream.codec_name === 'h265' ? 'hevc' : stream.codec_name, codecType: stream.codec_type, - frameCount: Number.parseInt(stream.nb_frames ?? '0'), - rotation: Number.parseInt(`${stream.rotation ?? 0}`), + frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames), + rotation: this.parseInt(stream.rotation), isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67', - bitrate: Number.parseInt(stream.bit_rate ?? '0'), + bitrate: this.parseInt(stream.bit_rate), })), audioStreams: results.streams .filter((stream) => stream.codec_type === 'audio') @@ -94,7 +136,7 @@ export class MediaRepository implements IMediaRepository { index: stream.index, codecType: stream.codec_type, codecName: stream.codec_name, - frameCount: Number.parseInt(stream.nb_frames ?? '0'), + frameCount: this.parseInt(options?.countFrames ? stream.nb_read_packets : stream.nb_frames), })), }; } @@ -137,29 +179,43 @@ export class MediaRepository implements IMediaRepository { }); } - async generateThumbhash(imagePath: string): Promise { - const maxSize = 100; - - const { data, info } = await sharp(imagePath) - .resize(maxSize, maxSize, { fit: 'inside', withoutEnlargement: true }) - .raw() - .ensureAlpha() - .toBuffer({ resolveWithObject: true }); - - const thumbhash = await import('thumbhash'); - return Buffer.from(thumbhash.rgbaToThumbHash(info.width, info.height, data)); - } - async getImageDimensions(input: string): Promise { const { width = 0, height = 0 } = await sharp(input).metadata(); return { width, height }; } private configureFfmpegCall(input: string, output: string | Writable, options: TranscodeCommand) { - return ffmpeg(input, { niceness: 10 }) + const ffmpegCall = ffmpeg(input, { niceness: 10 }) .inputOptions(options.inputOptions) .outputOptions(options.outputOptions) .output(output) - .on('error', (error, stdout, stderr) => this.logger.error(stderr || error)); + .on('start', (command: string) => this.logger.debug(command)) + .on('error', (error, _, stderr) => this.logger.error(stderr || error)); + + const { frameCount, percentInterval } = options.progress; + const frameInterval = Math.ceil(frameCount / (100 / percentInterval)); + if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) { + let lastProgressFrame: number = 0; + ffmpegCall.on('progress', (progress: ProgressEvent) => { + if (progress.frames - lastProgressFrame < frameInterval) { + return; + } + + lastProgressFrame = progress.frames; + const percent = ((progress.frames / frameCount) * 100).toFixed(2); + const ms = Math.floor((frameCount - progress.frames) / progress.currentFps) * 1000; + const duration = ms ? Duration.fromMillis(ms).rescale().toHuman({ unitDisplay: 'narrow' }) : ''; + const outputText = output instanceof Writable ? 'stream' : output.split('/').pop(); + this.logger.debug( + `Transcoding ${percent}% done${duration ? `, estimated ${duration} remaining` : ''} for output ${outputText}`, + ); + }); + } + + return ffmpegCall; + } + + private parseInt(value: string | number | undefined): number { + return Number.parseInt(value as string) || 0; } } diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts index f5933915ce241..b02e071b1ac8d 100644 --- a/server/src/repositories/metadata.repository.ts +++ b/server/src/repositories/metadata.repository.ts @@ -2,7 +2,6 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored'; import geotz from 'geo-tz'; -import { DummyValue, GenerateSql } from 'src/decorators'; import { ExifEntity } from 'src/entities/exif.entity'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface'; @@ -54,91 +53,4 @@ export class MetadataRepository implements IMetadataRepository { this.logger.warn(`Error writing exif data (${path}): ${error}`); } } - - @GenerateSql({ params: [[DummyValue.UUID]] }) - async getCountries(userIds: string[]): Promise { - const results = await this.exifRepository - .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') - .where('asset.ownerId IN (:...userIds )', { userIds }) - .select('exif.country', 'country') - .distinctOn(['exif.country']) - .getRawMany<{ country: string }>(); - - return results.map(({ country }) => country).filter((item) => item !== ''); - } - - @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) - async getStates(userIds: string[], country: string | undefined): Promise { - const query = this.exifRepository - .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') - .where('asset.ownerId IN (:...userIds )', { userIds }) - .select('exif.state', 'state') - .distinctOn(['exif.state']); - - if (country) { - query.andWhere('exif.country = :country', { country }); - } - - const result = await query.getRawMany<{ state: string }>(); - - return result.map(({ state }) => state).filter((item) => item !== ''); - } - - @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] }) - async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise { - const query = this.exifRepository - .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') - .where('asset.ownerId IN (:...userIds )', { userIds }) - .select('exif.city', 'city') - .distinctOn(['exif.city']); - - if (country) { - query.andWhere('exif.country = :country', { country }); - } - - if (state) { - query.andWhere('exif.state = :state', { state }); - } - - const results = await query.getRawMany<{ city: string }>(); - - return results.map(({ city }) => city).filter((item) => item !== ''); - } - - @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) - async getCameraMakes(userIds: string[], model: string | undefined): Promise { - const query = this.exifRepository - .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') - .where('asset.ownerId IN (:...userIds )', { userIds }) - .select('exif.make', 'make') - .distinctOn(['exif.make']); - - if (model) { - query.andWhere('exif.model = :model', { model }); - } - - const results = await query.getRawMany<{ make: string }>(); - return results.map(({ make }) => make).filter((item) => item !== ''); - } - - @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) - async getCameraModels(userIds: string[], make: string | undefined): Promise { - const query = this.exifRepository - .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') - .where('asset.ownerId IN (:...userIds )', { userIds }) - .select('exif.model', 'model') - .distinctOn(['exif.model']); - - if (make) { - query.andWhere('exif.make = :make', { make }); - } - - const results = await query.getRawMany<{ model: string }>(); - return results.map(({ model }) => model).filter((item) => item !== ''); - } } diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts index a8416ff0ac4c6..45fd4465265d7 100644 --- a/server/src/repositories/move.repository.ts +++ b/server/src/repositories/move.repository.ts @@ -1,7 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { DummyValue, GenerateSql } from 'src/decorators'; -import { MoveEntity, PathType } from 'src/entities/move.entity'; +import { MoveEntity } from 'src/entities/move.entity'; +import { PathType } from 'src/enum'; import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Repository } from 'typeorm'; diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts index c0bfee53987dc..0350e8a953027 100644 --- a/server/src/repositories/person.repository.ts +++ b/server/src/repositories/person.repository.ts @@ -6,20 +6,21 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { SourceType } from 'src/enum'; +import { PaginationMode, SourceType } from 'src/enum'; import { AssetFaceId, - DeleteAllFacesOptions, + DeleteFacesOptions, IPersonRepository, PeopleStatistics, PersonNameResponse, PersonNameSearchOptions, PersonSearchOptions, PersonStatistics, + UnassignFacesOptions, UpdateFacesData, } from 'src/interfaces/person.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; +import { Paginated, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination'; import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm'; @Instrumentation() @@ -39,12 +40,23 @@ export class PersonRepository implements IPersonRepository { .createQueryBuilder() .update() .set({ personId: newPersonId }) - .where(_.omitBy({ personId: oldPersonId ?? undefined, id: faceIds ? In(faceIds) : undefined }, _.isUndefined)) + .where(_.omitBy({ personId: oldPersonId, id: faceIds ? In(faceIds) : undefined }, _.isUndefined)) .execute(); return result.affected ?? 0; } + async unassignFaces({ sourceType }: UnassignFacesOptions): Promise { + await this.assetFaceRepository + .createQueryBuilder() + .update() + .set({ personId: null }) + .where({ sourceType }) + .execute(); + + await this.vacuum({ reindexVectors: false }); + } + async delete(entities: PersonEntity[]): Promise { await this.personRepository.remove(entities); } @@ -53,21 +65,14 @@ export class PersonRepository implements IPersonRepository { await this.personRepository.clear(); } - async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise { - if (!sourceType) { - return this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE'); - } - + async deleteFaces({ sourceType }: DeleteFacesOptions): Promise { await this.assetFaceRepository .createQueryBuilder('asset_faces') .delete() .andWhere('sourceType = :sourceType', { sourceType }) .execute(); - await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search'); - if (sourceType === SourceType.MACHINE_LEARNING) { - await this.assetFaceRepository.query('REINDEX INDEX face_index'); - } + await this.vacuum({ reindexVectors: sourceType === SourceType.MACHINE_LEARNING }); } getAllFaces( @@ -184,14 +189,11 @@ export class PersonRepository implements IPersonRepository { getByName(userId: string, personName: string, { withHidden }: PersonNameSearchOptions): Promise { const queryBuilder = this.personRepository .createQueryBuilder('person') - .leftJoin('person.faces', 'face') .where( 'person.ownerId = :userId AND (LOWER(person.name) LIKE :nameStart OR LOWER(person.name) LIKE :nameAnywhere)', { userId, nameStart: `${personName.toLowerCase()}%`, nameAnywhere: `% ${personName.toLowerCase()}%` }, ) - .groupBy('person.id') - .orderBy('COUNT(face.assetId)', 'DESC') - .limit(20); + .limit(1000); if (!withHidden) { queryBuilder.andWhere('person.isHidden = false'); @@ -334,4 +336,13 @@ export class PersonRepository implements IPersonRepository { const { id } = await this.personRepository.save(person); return this.personRepository.findOneByOrFail({ id }); } + + private async vacuum({ reindexVectors }: { reindexVectors: boolean }): Promise { + await this.assetFaceRepository.query('VACUUM ANALYZE asset_faces, face_search, person'); + await this.assetFaceRepository.query('REINDEX TABLE asset_faces'); + await this.assetFaceRepository.query('REINDEX TABLE person'); + if (reindexVectors) { + await this.assetFaceRepository.query('REINDEX TABLE face_search'); + } + } } diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 40f87ddf242c7..cb80c8d2f1c4e 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -1,13 +1,15 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { randomUUID } from 'node:crypto'; import { getVectorExtension } from 'src/database.config'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; -import { AssetType } from 'src/enum'; +import { AssetType, PaginationMode } from 'src/enum'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { @@ -22,7 +24,7 @@ import { } from 'src/interfaces/search.interface'; import { asVector, searchAssetBuilder } from 'src/utils/database'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination'; +import { Paginated, PaginationResult, paginatedBuilder } from 'src/utils/pagination'; import { isValidInteger } from 'src/validation'; import { Repository, SelectQueryBuilder } from 'typeorm'; @@ -35,6 +37,7 @@ export class SearchRepository implements ISearchRepository { constructor( @InjectRepository(SmartInfoEntity) private repository: Repository, @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectRepository(ExifEntity) private exifRepository: Repository, @InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository, @InjectRepository(SmartSearchEntity) private smartSearchRepository: Repository, @InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository, @@ -61,18 +64,16 @@ export class SearchRepository implements ISearchRepository { { takenAfter: DummyValue.DATE, lensModel: DummyValue.STRING, - ownerId: DummyValue.UUID, withStacked: true, isFavorite: true, - ownerIds: [DummyValue.UUID], + userIds: [DummyValue.UUID], }, ], }) async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated { let builder = this.assetRepository.createQueryBuilder('asset'); - builder = searchAssetBuilder(builder, options); + builder = searchAssetBuilder(builder, options).orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC'); - builder.orderBy('asset.fileCreatedAt', options.orderDirection ?? 'DESC'); return paginatedBuilder(builder, { mode: PaginationMode.SKIP_TAKE, skip: (pagination.page - 1) * pagination.size, @@ -80,6 +81,35 @@ export class SearchRepository implements ISearchRepository { }); } + @GenerateSql({ + params: [ + 100, + { + takenAfter: DummyValue.DATE, + lensModel: DummyValue.STRING, + withStacked: true, + isFavorite: true, + userIds: [DummyValue.UUID], + }, + ], + }) + async searchRandom(size: number, options: AssetSearchOptions): Promise { + const builder1 = searchAssetBuilder(this.assetRepository.createQueryBuilder('asset'), options); + const builder2 = builder1.clone(); + + const uuid = randomUUID(); + builder1.andWhere('asset.id > :uuid', { uuid }).orderBy('asset.id').take(size); + builder2.andWhere('asset.id < :uuid', { uuid }).orderBy('asset.id').take(size); + + const [assets1, assets2] = await Promise.all([builder1.getMany(), builder2.getMany()]); + const missingCount = size - assets1.length; + for (let i = 0; i < missingCount && i < assets2.length; i++) { + assets1.push(assets2[i]); + } + + return assets1; + } + private createPersonFilter(builder: SelectQueryBuilder, personIds: string[]) { return builder .select(`${builder.alias}."assetId"`) @@ -322,6 +352,93 @@ export class SearchRepository implements ISearchRepository { return this.smartSearchRepository.clear(); } + @GenerateSql({ params: [[DummyValue.UUID]] }) + async getCountries(userIds: string[]): Promise { + const results = await this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId IN (:...userIds )', { userIds }) + .select('exif.country', 'country') + .distinctOn(['exif.country']) + .getRawMany<{ country: string }>(); + + return results.map(({ country }) => country).filter((item) => item !== ''); + } + + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) + async getStates(userIds: string[], country: string | undefined): Promise { + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId IN (:...userIds )', { userIds }) + .select('exif.state', 'state') + .distinctOn(['exif.state']); + + if (country) { + query.andWhere('exif.country = :country', { country }); + } + + const result = await query.getRawMany<{ state: string }>(); + + return result.map(({ state }) => state).filter((item) => item !== ''); + } + + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] }) + async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise { + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId IN (:...userIds )', { userIds }) + .select('exif.city', 'city') + .distinctOn(['exif.city']); + + if (country) { + query.andWhere('exif.country = :country', { country }); + } + + if (state) { + query.andWhere('exif.state = :state', { state }); + } + + const results = await query.getRawMany<{ city: string }>(); + + return results.map(({ city }) => city).filter((item) => item !== ''); + } + + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) + async getCameraMakes(userIds: string[], model: string | undefined): Promise { + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId IN (:...userIds )', { userIds }) + .select('exif.make', 'make') + .distinctOn(['exif.make']); + + if (model) { + query.andWhere('exif.model = :model', { model }); + } + + const results = await query.getRawMany<{ make: string }>(); + return results.map(({ make }) => make).filter((item) => item !== ''); + } + + @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) + async getCameraModels(userIds: string[], make: string | undefined): Promise { + const query = this.exifRepository + .createQueryBuilder('exif') + .leftJoin('exif.asset', 'asset') + .where('asset.ownerId IN (:...userIds )', { userIds }) + .select('exif.model', 'model') + .distinctOn(['exif.model']); + + if (make) { + query.andWhere('exif.make = :make', { make }); + } + + const results = await query.getRawMany<{ model: string }>(); + return results.map(({ model }) => model).filter((item) => item !== ''); + } + private getRuntimeConfig(numResults?: number): string { if (getVectorExtension() === DatabaseExtension.VECTOR) { return 'SET LOCAL hnsw.ef_search = 1000;'; // mitigate post-filter recall diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index c699047ce1575..6fd9bb8b04147 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -40,8 +40,16 @@ export class StorageRepository implements IStorageRepository { return fs.stat(filepath); } - writeFile(filepath: string, buffer: Buffer) { - return fs.writeFile(filepath, buffer); + createFile(filepath: string, buffer: Buffer) { + return fs.writeFile(filepath, buffer, { flag: 'wx' }); + } + + createOrOverwriteFile(filepath: string, buffer: Buffer) { + return fs.writeFile(filepath, buffer, { flag: 'w' }); + } + + overwriteFile(filepath: string, buffer: Buffer) { + return fs.writeFile(filepath, buffer, { flag: 'r+' }); } rename(source: string, target: string) { diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 9389aeb13b4e3..1a5415b8dbb08 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -1,10 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { TagEntity } from 'src/entities/tag.entity'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { DataSource, In, Repository } from 'typeorm'; +import { DataSource, In, Repository, TreeRepository } from 'typeorm'; @Instrumentation() @Injectable() @@ -12,7 +13,11 @@ export class TagRepository implements ITagRepository { constructor( @InjectDataSource() private dataSource: DataSource, @InjectRepository(TagEntity) private repository: Repository, - ) {} + @InjectRepository(TagEntity) private tree: TreeRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, + ) { + this.logger.setContext(TagRepository.name); + } get(id: string): Promise { return this.repository.findOne({ where: { id } }); @@ -174,6 +179,34 @@ export class TagRepository implements ITagRepository { }); } + async deleteEmptyTags() { + await this.dataSource.transaction(async (manager) => { + const ids = new Set(); + const tags = await manager.find(TagEntity); + for (const tag of tags) { + const count = await manager + .createQueryBuilder('assets', 'asset') + .innerJoin( + 'asset.tags', + 'asset_tags', + 'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)', + { tagId: tag.id }, + ) + .getCount(); + + if (count === 0) { + this.logger.debug(`Found empty tag: ${tag.id} - ${tag.value}`); + ids.add(tag.id); + } + } + + if (ids.size > 0) { + await manager.delete(TagEntity, { id: In([...ids]) }); + this.logger.log(`Deleted ${ids.size} empty tags`); + } + }); + } + private async save(partial: Partial): Promise { const { id } = await this.repository.save(partial); return this.repository.findOneOrFail({ where: { id } }); diff --git a/server/src/repositories/trash.repository.ts b/server/src/repositories/trash.repository.ts new file mode 100644 index 0000000000000..d24f4f709afac --- /dev/null +++ b/server/src/repositories/trash.repository.ts @@ -0,0 +1,52 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetStatus } from 'src/enum'; +import { ITrashRepository } from 'src/interfaces/trash.interface'; +import { Paginated, paginatedBuilder, PaginationOptions } from 'src/utils/pagination'; +import { In, Repository } from 'typeorm'; + +export class TrashRepository implements ITrashRepository { + constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} + + async getDeletedIds(pagination: PaginationOptions): Paginated { + const { hasNextPage, items } = await paginatedBuilder( + this.assetRepository + .createQueryBuilder('asset') + .select('asset.id') + .where({ status: AssetStatus.DELETED }) + .withDeleted(), + pagination, + ); + + return { + hasNextPage, + items: items.map((asset) => asset.id), + }; + } + + async restore(userId: string): Promise { + const result = await this.assetRepository.update( + { ownerId: userId, status: AssetStatus.TRASHED }, + { status: AssetStatus.ACTIVE, deletedAt: null }, + ); + + return result.affected || 0; + } + + async empty(userId: string): Promise { + const result = await this.assetRepository.update( + { ownerId: userId, status: AssetStatus.TRASHED }, + { status: AssetStatus.DELETED }, + ); + + return result.affected || 0; + } + + async restoreAll(ids: string[]): Promise { + const result = await this.assetRepository.update( + { id: In(ids), status: AssetStatus.TRASHED }, + { status: AssetStatus.ACTIVE, deletedAt: null }, + ); + return result.affected ?? 0; + } +} diff --git a/server/src/repositories/view-repository.ts b/server/src/repositories/view-repository.ts new file mode 100644 index 0000000000000..3645e3638af32 --- /dev/null +++ b/server/src/repositories/view-repository.ts @@ -0,0 +1,48 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { DummyValue, GenerateSql } from 'src/decorators'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { IViewRepository } from 'src/interfaces/view.interface'; +import { Brackets, Repository } from 'typeorm'; + +export class ViewRepository implements IViewRepository { + constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {} + + async getUniqueOriginalPaths(userId: string): Promise { + const results = await this.assetRepository + .createQueryBuilder('asset') + .where({ + isVisible: true, + isArchived: false, + ownerId: userId, + }) + .select("DISTINCT substring(asset.originalPath FROM '^(.*/)[^/]*$')", 'directoryPath') + .getRawMany(); + + return results.map((row: { directoryPath: string }) => row.directoryPath.replaceAll(/^\/|\/$/g, '')); + } + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] }) + async getAssetsByOriginalPath(userId: string, partialPath: string): Promise { + const normalizedPath = partialPath.replaceAll(/^\/|\/$/g, ''); + const assets = await this.assetRepository + .createQueryBuilder('asset') + .where({ + isVisible: true, + isArchived: false, + ownerId: userId, + }) + .leftJoinAndSelect('asset.exifInfo', 'exifInfo') + .andWhere( + new Brackets((qb) => { + qb.where('asset.originalPath LIKE :likePath', { likePath: `%${normalizedPath}/%` }).andWhere( + 'asset.originalPath NOT LIKE :notLikePath', + { notLikePath: `%${normalizedPath}/%/%` }, + ); + }), + ) + .orderBy(String.raw`regexp_replace(asset.originalPath, '.*/(.+)', '\1')`, 'ASC') + .getMany(); + + return assets; + } +} diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 164e823336878..b8624b29aebd5 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -67,7 +67,6 @@ describe(AlbumService.name, () => { { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, ]); - albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, {}); expect(result).toHaveLength(2); @@ -85,7 +84,6 @@ describe(AlbumService.name, () => { endDate: new Date('1970-01-01'), }, ]); - albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id }); expect(result).toHaveLength(1); @@ -98,7 +96,6 @@ describe(AlbumService.name, () => { albumMock.getMetadataForIds.mockResolvedValue([ { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, ]); - albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { shared: true }); expect(result).toHaveLength(1); @@ -111,7 +108,6 @@ describe(AlbumService.name, () => { albumMock.getMetadataForIds.mockResolvedValue([ { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, ]); - albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { shared: false }); expect(result).toHaveLength(1); @@ -130,7 +126,6 @@ describe(AlbumService.name, () => { endDate: new Date('1970-01-01'), }, ]); - albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, {}); @@ -139,48 +134,6 @@ describe(AlbumService.name, () => { expect(albumMock.getOwned).toHaveBeenCalledTimes(1); }); - it('updates the album thumbnail by listing all albums', async () => { - albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]); - albumMock.getMetadataForIds.mockResolvedValue([ - { - albumId: albumStub.oneAssetInvalidThumbnail.id, - assetCount: 1, - startDate: new Date('1970-01-01'), - endDate: new Date('1970-01-01'), - }, - ]); - albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]); - albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail); - assetMock.getFirstAssetForAlbumId.mockResolvedValue(albumStub.oneAssetInvalidThumbnail.assets[0]); - - const result = await sut.getAll(authStub.admin, {}); - - expect(result).toHaveLength(1); - expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1); - expect(albumMock.update).toHaveBeenCalledTimes(1); - }); - - it('removes the thumbnail for an empty album', async () => { - albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]); - albumMock.getMetadataForIds.mockResolvedValue([ - { - albumId: albumStub.emptyWithInvalidThumbnail.id, - assetCount: 1, - startDate: new Date('1970-01-01'), - endDate: new Date('1970-01-01'), - }, - ]); - albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]); - albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail); - assetMock.getFirstAssetForAlbumId.mockResolvedValue(null); - - const result = await sut.getAll(authStub.admin, {}); - - expect(result).toHaveLength(1); - expect(albumMock.getInvalidThumbnail).toHaveBeenCalledTimes(1); - expect(albumMock.update).toHaveBeenCalledTimes(1); - }); - describe('create', () => { it('creates album', async () => { albumMock.create.mockResolvedValue(albumStub.empty); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index b59364af9fb6e..2f5d2308415ff 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -52,11 +52,7 @@ export class AlbumService { } async getAll({ user: { id: ownerId } }: AuthDto, { assetId, shared }: GetAlbumsDto): Promise { - const invalidAlbumIds = await this.albumRepository.getInvalidThumbnail(); - for (const albumId of invalidAlbumIds) { - const newThumbnail = await this.assetRepository.getFirstAssetForAlbumId(albumId); - await this.albumRepository.update({ id: albumId, albumThumbnailAsset: newThumbnail }); - } + await this.albumRepository.updateThumbnails(); let albums: AlbumEntity[]; if (assetId) { diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 9d6f0ff9cf547..c03c974b2c8e2 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -4,7 +4,7 @@ import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; -import { AssetType } from 'src/enum'; +import { AssetStatus, AssetType, CacheControl } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; @@ -12,7 +12,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AssetMediaService } from 'src/services/asset-media.service'; -import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; @@ -478,7 +478,10 @@ describe(AssetMediaService.name, () => { }), ); - expect(assetMock.softDeleteAll).toHaveBeenCalledWith([copiedAsset.id]); + expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], { + deletedAt: expect.any(Date), + status: AssetStatus.TRASHED, + }); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(storageMock.utimes).toHaveBeenCalledWith( updatedFile.originalPath, @@ -506,7 +509,10 @@ describe(AssetMediaService.name, () => { id: 'copied-asset', }); - expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['copied-asset']); + expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], { + deletedAt: expect.any(Date), + status: AssetStatus.TRASHED, + }); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(storageMock.utimes).toHaveBeenCalledWith( updatedFile.originalPath, @@ -532,7 +538,10 @@ describe(AssetMediaService.name, () => { id: 'copied-asset', }); - expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['copied-asset']); + expect(assetMock.updateAll).toHaveBeenCalledWith([copiedAsset.id], { + deletedAt: expect.any(Date), + status: AssetStatus.TRASHED, + }); expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(storageMock.utimes).toHaveBeenCalledWith( updatedFile.originalPath, @@ -561,7 +570,7 @@ describe(AssetMediaService.name, () => { }); expect(assetMock.create).not.toHaveBeenCalled(); - expect(assetMock.softDeleteAll).not.toHaveBeenCalled(); + expect(assetMock.updateAll).not.toHaveBeenCalled(); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.DELETE_FILES, data: { files: [updatedFile.originalPath, undefined] }, diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 111d222c160c8..e1b30e891f936 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -7,7 +7,7 @@ import { } from '@nestjs/common'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, @@ -27,17 +27,17 @@ import { } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; -import { AssetType, Permission } from 'src/enum'; +import { AssetStatus, AssetType, CacheControl, Permission, StorageFolder } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { requireAccess, requireUploadAccess } from 'src/utils/access'; import { getAssetFiles, onBeforeLink } from 'src/utils/asset.util'; -import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { fromChecksum } from 'src/utils/request'; import { QueryFailedError } from 'typeorm'; @@ -193,9 +193,8 @@ export class AssetMediaService { // but the local variable holds the original file data paths. const copiedPhoto = await this.createCopy(asset); // and immediate trash it - await this.assetRepository.softDeleteAll([copiedPhoto.id]); - - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, [copiedPhoto.id]); + await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED }); + await this.eventRepository.emit('asset.trash', { assetId: copiedPhoto.id, userId: auth.user.id }); await this.userRepository.updateUsage(auth.user.id, file.size); @@ -428,7 +427,6 @@ export class AssetMediaService { livePhotoVideoId: dto.livePhotoVideoId, originalFileName: file.originalName, sidecarPath: sidecarFile?.originalPath, - isOffline: dto.isOffline ?? false, }); if (sidecarFile) { diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 3ac7aa1c718f7..f36d26fa7c5b3 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -2,7 +2,7 @@ import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetType } from 'src/enum'; +import { AssetStatus, AssetType } from 'src/enum'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; @@ -269,10 +269,10 @@ describe(AssetService.name, () => { await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true }); - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.ASSET_DELETION, data: { id: 'asset1', deleteOnDisk: true } }, - { name: JobName.ASSET_DELETION, data: { id: 'asset2', deleteOnDisk: true } }, - ]); + expect(eventMock.emit).toHaveBeenCalledWith('assets.delete', { + assetIds: ['asset1', 'asset2'], + userId: 'user-id', + }); }); it('should soft delete a batch of assets', async () => { @@ -280,7 +280,10 @@ describe(AssetService.name, () => { await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: false }); - expect(assetMock.softDeleteAll).toHaveBeenCalledWith(['asset1', 'asset2']); + expect(assetMock.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], { + deletedAt: expect.any(Date), + status: AssetStatus.TRASHED, + }); expect(jobMock.queue.mock.calls).toEqual([]); }); }); @@ -392,7 +395,7 @@ describe(AssetService.name, () => { it('should run the refresh thumbnails job', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }); - expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }]); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }]); }); it('should run the transcode video', async () => { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index ecc9a135759da..aa88eaf9576b4 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -20,10 +20,10 @@ import { import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { Permission } from 'src/enum'; +import { AssetStatus, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IAssetDeleteJob, IJobRepository, @@ -39,7 +39,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { requireAccess } from 'src/utils/access'; -import { getAssetFiles, getMyPartnerIds, onBeforeLink } from 'src/utils/asset.util'; +import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService { @@ -98,7 +98,12 @@ export class AssetService { } async getRandom(auth: AuthDto, count: number): Promise { - const assets = await this.assetRepository.getRandom(auth.user.id, count); + const partnerIds = await getMyPartnerIds({ + userId: auth.user.id, + repository: this.partnerRepository, + timelineEnabled: true, + }); + const assets = await this.assetRepository.getRandom([auth.user.id, ...partnerIds], count); return assets.map((a) => mapAsset(a, { auth })); } @@ -159,17 +164,26 @@ export class AssetService { await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; + const repos = { asset: this.assetRepository, event: this.eventRepository }; + let previousMotion: AssetEntity | null = null; if (rest.livePhotoVideoId) { - await onBeforeLink( - { asset: this.assetRepository, event: this.eventRepository }, - { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId }, - ); + await onBeforeLink(repos, { userId: auth.user.id, livePhotoVideoId: rest.livePhotoVideoId }); + } else if (rest.livePhotoVideoId === null) { + const asset = await this.findOrFail(id); + if (asset.livePhotoVideoId) { + previousMotion = await onBeforeUnlink(repos, { livePhotoVideoId: asset.livePhotoVideoId }); + } } await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); await this.assetRepository.update({ id, ...rest }); + + if (previousMotion) { + await onAfterUnlink(repos, { userId: auth.user.id, livePhotoVideoId: previousMotion.id }); + } + const asset = await this.assetRepository.getById(id, { exifInfo: true, owner: true, @@ -180,9 +194,11 @@ export class AssetService { }, files: true, }); + if (!asset) { throw new BadRequestException('Asset not found'); } + return mapAsset(asset, { auth }); } @@ -257,7 +273,8 @@ export class AssetService { if (!asset.libraryId) { await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0)); } - this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, asset.ownerId, id); + + await this.eventRepository.emit('asset.delete', { assetId: id, userId: asset.ownerId }); // delete the motion if it is not used by another asset if (asset.livePhotoVideoId) { @@ -285,18 +302,11 @@ export class AssetService { const { ids, force } = dto; await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); - - if (force) { - await this.jobRepository.queueAll( - ids.map((id) => ({ - name: JobName.ASSET_DELETION, - data: { id, deleteOnDisk: true }, - })), - ); - } else { - await this.assetRepository.softDeleteAll(ids); - this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, ids); - } + await this.assetRepository.updateAll(ids, { + deletedAt: new Date(), + status: force ? AssetStatus.DELETED : AssetStatus.TRASHED, + }); + await this.eventRepository.emit(force ? 'assets.delete' : 'assets.trash', { assetIds: ids, userId: auth.user.id }); } async run(auth: AuthDto, dto: AssetJobsDto) { @@ -312,7 +322,7 @@ export class AssetService { } case AssetJobName.REGENERATE_THUMBNAIL: { - jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id } }); + jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id } }); break; } @@ -326,6 +336,14 @@ export class AssetService { await this.jobRepository.queueAll(jobs); } + private async findOrFail(id: string) { + const asset = await this.assetRepository.getById(id); + if (!asset) { + throw new BadRequestException('Asset not found'); + } + return asset; + } + private async updateMetadata(dto: ISidecarWriteJob) { const { id, description, dateTimeOriginal, latitude, longitude, rating } = dto; const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 72db2b6eb56ce..ced0f49c63716 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { AuditDeletesDto, AuditDeletesResponseDto, @@ -12,8 +12,15 @@ import { PathEntityType, } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; -import { AssetFileType, DatabaseAction, Permission } from 'src/enum'; +import { + AssetFileType, + AssetPathType, + DatabaseAction, + Permission, + PersonPathType, + StorageFolder, + UserPathType, +} from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index acc2d3459ccd1..d22a3b3634ebc 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -1,9 +1,9 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { Issuer, generators } from 'openid-client'; -import { AuthType } from 'src/constants'; import { AuthDto, SignUpDto } from 'src/dtos/auth.dto'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AuthType } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 6eaf755d0eb49..6b1e4c512f816 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -12,7 +12,7 @@ import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; import { Issuer, UserinfoResponse, custom, generators } from 'openid-client'; import { SystemConfig } from 'src/config'; -import { AuthType, LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; +import { LOGIN_URL, MOBILE_REDIRECT, SALT_ROUNDS } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { UserCore } from 'src/cores/user.core'; import { @@ -31,7 +31,7 @@ import { } from 'src/dtos/auth.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserEntity } from 'src/entities/user.entity'; -import { Permission } from 'src/enum'; +import { AuthType, Permission } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index c63428560e03c..fc8130cadc273 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,12 +1,20 @@ -import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { IConfigRepository } from 'src/interfaces/config.interface'; +import { + DatabaseExtension, + EXTENSION_NAMES, + IDatabaseRepository, + VectorExtension, +} from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DatabaseService } from 'src/services/database.service'; +import { newConfigRepositoryMock } from 'test/repositories/config.repository.mock'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { Mocked } from 'vitest'; describe(DatabaseService.name, () => { let sut: DatabaseService; + let configMock: Mocked; let databaseMock: Mocked; let loggerMock: Mocked; let extensionRange: string; @@ -16,9 +24,11 @@ describe(DatabaseService.name, () => { let versionAboveRange: string; beforeEach(() => { + configMock = newConfigRepositoryMock(); databaseMock = newDatabaseRepositoryMock(); loggerMock = newLoggerRepositoryMock(); - sut = new DatabaseService(databaseMock, loggerMock); + + sut = new DatabaseService(configMock, databaseMock, loggerMock); extensionRange = '0.2.x'; databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange); @@ -33,11 +43,6 @@ describe(DatabaseService.name, () => { }); }); - afterEach(() => { - delete process.env.DB_SKIP_MIGRATIONS; - delete process.env.DB_VECTOR_EXTENSION; - }); - it('should work', () => { expect(sut).toBeDefined(); }); @@ -50,12 +55,12 @@ describe(DatabaseService.name, () => { expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); }); - describe.each([ + describe.each(>[ { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, ])('should work with $extensionName', ({ extension, extensionName }) => { beforeEach(() => { - process.env.DB_VECTOR_EXTENSION = extensionName; + configMock.getEnv.mockReturnValue({ database: { skipMigrations: false, vectorExtension: extension } }); }); it(`should start up successfully with ${extension}`, async () => { @@ -236,18 +241,28 @@ describe(DatabaseService.name, () => { expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); expect(loggerMock.fatal).not.toHaveBeenCalled(); }); + }); - it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { - process.env.DB_SKIP_MIGRATIONS = 'true'; - - await expect(sut.onBootstrap()).resolves.toBeUndefined(); - - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { + configMock.getEnv.mockReturnValue({ + database: { + skipMigrations: true, + vectorExtension: DatabaseExtension.VECTORS, + }, }); + + await expect(sut.onBootstrap()).resolves.toBeUndefined(); + + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); it(`should throw error if pgvector extension could not be created`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; + configMock.getEnv.mockReturnValue({ + database: { + skipMigrations: true, + vectorExtension: DatabaseExtension.VECTOR, + }, + }); databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: minVersionInRange, diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index a5280ff28be23..ee6176115b311 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { Duration } from 'luxon'; import semver from 'semver'; -import { getVectorExtension } from 'src/database.config'; import { OnEmit } from 'src/decorators'; +import { IConfigRepository } from 'src/interfaces/config.interface'; import { DatabaseExtension, DatabaseLock, @@ -67,6 +67,7 @@ export class DatabaseService { private reconnection?: NodeJS.Timeout; constructor( + @Inject(IConfigRepository) private configRepository: IConfigRepository, @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { @@ -85,7 +86,8 @@ export class DatabaseService { } await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { - const extension = getVectorExtension(); + const envData = this.configRepository.getEnv(); + const extension = envData.database.vectorExtension; const name = EXTENSION_NAMES[extension]; const extensionRange = this.databaseRepository.getExtensionVersionRange(extension); @@ -116,7 +118,8 @@ export class DatabaseService { await this.checkReindexing(); - if (process.env.DB_SKIP_MIGRATIONS !== 'true') { + const { database } = this.configRepository.getEnv(); + if (!database.skipMigrations) { await this.databaseRepository.runMigrations(); } }); diff --git a/server/src/services/job.service.spec.ts b/server/src/services/job.service.spec.ts index 1c810facb453a..c2d7a29b9f0cb 100644 --- a/server/src/services/job.service.spec.ts +++ b/server/src/services/job.service.spec.ts @@ -288,7 +288,7 @@ describe(JobService.name, () => { }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, - jobs: [JobName.GENERATE_PREVIEW], + jobs: [JobName.GENERATE_THUMBNAILS], }, { item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } }, @@ -299,28 +299,16 @@ describe(JobService.name, () => { jobs: [], }, { - item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1' } }, - jobs: [JobName.GENERATE_THUMBNAIL, JobName.GENERATE_THUMBHASH], + item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, + jobs: [], }, { - item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-1', source: 'upload' } }, - jobs: [ - JobName.GENERATE_THUMBNAIL, - JobName.GENERATE_THUMBHASH, - JobName.SMART_SEARCH, - JobName.FACE_DETECTION, - JobName.VIDEO_CONVERSION, - ], + item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, + jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION], }, { - item: { name: JobName.GENERATE_PREVIEW, data: { id: 'asset-live-image', source: 'upload' } }, - jobs: [ - JobName.GENERATE_THUMBNAIL, - JobName.GENERATE_THUMBHASH, - JobName.SMART_SEARCH, - JobName.FACE_DETECTION, - JobName.VIDEO_CONVERSION, - ], + item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-live-image', source: 'upload' } }, + jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION], }, { item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } }, @@ -338,11 +326,11 @@ describe(JobService.name, () => { for (const { item, jobs } of tests) { it(`should queue ${jobs.length} jobs when a ${item.name} job finishes successfully`, async () => { - if (item.name === JobName.GENERATE_PREVIEW && item.data.source === 'upload') { + if (item.name === JobName.GENERATE_THUMBNAILS && item.data.source === 'upload') { if (item.data.id === 'asset-live-image') { - assetMock.getByIds.mockResolvedValue([assetStub.livePhotoStillAsset]); + assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoStillAsset]); } else { - assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + assetMock.getByIdsWithAllRelations.mockResolvedValue([assetStub.livePhotoMotionAsset]); } } @@ -361,7 +349,7 @@ describe(JobService.name, () => { } }); - it(`should not queue any jobs when ${item.name} finishes with 'false'`, async () => { + it(`should not queue any jobs when ${item.name} fails`, async () => { await sut.init(makeMockHandlers(JobStatus.FAILED)); await jobMock.addHandler.mock.calls[0][2](item); diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index aa84ef4f40957..9c73e71cbf684 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -2,8 +2,8 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { snakeCase } from 'lodash'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapAsset } from 'src/dtos/asset-response.dto'; -import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto'; -import { AssetType } from 'src/enum'; +import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto'; +import { AssetType, ManualJobName } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { @@ -22,6 +22,26 @@ import { IMetricRepository } from 'src/interfaces/metric.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +const asJobItem = (dto: JobCreateDto): JobItem => { + switch (dto.name) { + case ManualJobName.TAG_CLEANUP: { + return { name: JobName.TAG_CLEANUP }; + } + + case ManualJobName.PERSON_CLEANUP: { + return { name: JobName.PERSON_CLEANUP }; + } + + case ManualJobName.USER_CLEANUP: { + return { name: JobName.USER_DELETE_CHECK }; + } + + default: { + throw new BadRequestException('Invalid job name'); + } + } +}; + @Injectable() export class JobService { private configCore: SystemConfigCore; @@ -39,6 +59,10 @@ export class JobService { this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } + async create(dto: JobCreateDto): Promise { + await this.jobRepository.queue(asJobItem(dto)); + } + async handleCommand(queueName: QueueName, dto: JobCommandDto): Promise { this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`); @@ -140,7 +164,7 @@ export class JobService { } case QueueName.LIBRARY: { - return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } }); + return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, data: { force } }); } default: { @@ -162,11 +186,16 @@ export class JobService { this.jobRepository.addHandler(queueName, concurrency, async (item: JobItem): Promise => { const { name, data } = item; + const handler = jobHandlers[name]; + if (!handler) { + this.logger.warn(`Skipping unknown job: "${name}"`); + return; + } + const queueMetric = `immich.queues.${snakeCase(queueName)}.active`; this.metricRepository.jobs.addToGauge(queueMetric, 1); try { - const handler = jobHandlers[name]; const status = await handler(data); const jobMetric = `immich.jobs.${name.replaceAll('-', '_')}.${status}`; this.metricRepository.jobs.addToCounter(jobMetric, 1); @@ -231,6 +260,7 @@ export class JobService { name: JobName.METADATA_EXTRACTION, data: { id: item.data.id, source: 'sidecar-write' }, }); + break; } case JobName.METADATA_EXTRACTION: { @@ -251,7 +281,7 @@ export class JobService { case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { if (item.data.source === 'upload' || item.data.source === 'copy') { - await this.jobRepository.queue({ name: JobName.GENERATE_PREVIEW, data: item.data }); + await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: item.data }); } break; } @@ -265,40 +295,33 @@ export class JobService { break; } - case JobName.GENERATE_PREVIEW: { - const jobs: JobItem[] = [ - { name: JobName.GENERATE_THUMBNAIL, data: item.data }, - { name: JobName.GENERATE_THUMBHASH, data: item.data }, - ]; - - if (item.data.source === 'upload') { - jobs.push({ name: JobName.SMART_SEARCH, data: item.data }, { name: JobName.FACE_DETECTION, data: item.data }); - - const [asset] = await this.assetRepository.getByIds([item.data.id]); - if (asset) { - if (asset.type === AssetType.VIDEO) { - jobs.push({ name: JobName.VIDEO_CONVERSION, data: item.data }); - } else if (asset.livePhotoVideoId) { - jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.livePhotoVideoId } }); - } - } - } - - await this.jobRepository.queueAll(jobs); - break; - } - - case JobName.GENERATE_THUMBNAIL: { - if (item.data.source !== 'upload') { + case JobName.GENERATE_THUMBNAILS: { + if (!item.data.notify && item.data.source !== 'upload') { break; } const [asset] = await this.assetRepository.getByIdsWithAllRelations([item.data.id]); + if (!asset) { + this.logger.warn(`Could not find asset ${item.data.id} after generating thumbnails`); + break; + } - // Only live-photo motion part will be marked as not visible immediately on upload. Skip notifying clients - if (asset && asset.isVisible) { + const jobs: JobItem[] = [ + { name: JobName.SMART_SEARCH, data: item.data }, + { name: JobName.FACE_DETECTION, data: item.data }, + ]; + + if (asset.type === AssetType.VIDEO) { + jobs.push({ name: JobName.VIDEO_CONVERSION, data: item.data }); + } else if (asset.livePhotoVideoId) { + jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id: asset.livePhotoVideoId } }); + } + + await this.jobRepository.queueAll(jobs); + if (asset.isVisible) { this.eventRepository.clientSend(ClientEvent.UPLOAD_SUCCESS, asset.ownerId, mapAsset(asset)); } + break; } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 2d4e1d5776bfe..8b14c76cbcc1d 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -10,9 +10,8 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; import { IJobRepository, + ILibraryAssetJob, ILibraryFileJob, - ILibraryOfflineJob, - ILibraryRefreshJob, JobName, JOBS_LIBRARY_PAGINATION_SIZE, JobStatus, @@ -37,6 +36,10 @@ import { makeMockWatcher, newStorageRepositoryMock } from 'test/repositories/sto import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; import { Mocked, vitest } from 'vitest'; +async function* mockWalk() { + yield await Promise.resolve(['/data/user1/photo.jpg']); +} + describe(LibraryService.name, () => { let sut: LibraryService; @@ -91,7 +94,7 @@ describe(LibraryService.name, () => { enabled: true, cronExpression: '0 1 * * *', }, - watch: { enabled: false }, + watch: { enabled: true }, }, } as SystemConfig); @@ -163,102 +166,29 @@ describe(LibraryService.name, () => { describe('handleQueueAssetRefresh', () => { it('should queue refresh of a new asset', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - // eslint-disable-next-line @typescript-eslint/require-await - storageMock.walk.mockImplementation(async function* generator() { - yield ['/data/user1/photo.jpg']; - }); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); + storageMock.walk.mockImplementation(mockWalk); - await sut.handleQueueAssetRefresh(mockLibraryJob); + await sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id }); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.LIBRARY_SCAN_ASSET, + name: JobName.LIBRARY_SYNC_FILE, data: { id: libraryStub.externalLibrary1.id, ownerId: libraryStub.externalLibrary1.owner.id, assetPath: '/data/user1/photo.jpg', - force: false, - }, - }, - ]); - }); - - it('should queue offline check of existing online assets', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - - assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - storageMock.walk.mockImplementation(async function* generator() {}); - assetMock.getWith.mockResolvedValue({ items: [assetStub.external], hasNextPage: false }); - - await sut.handleQueueAssetRefresh(mockLibraryJob); - - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { - name: JobName.LIBRARY_CHECK_OFFLINE, - data: { - id: assetStub.external.id, - importPaths: libraryStub.externalLibrary1.importPaths, - exclusionPatterns: [], }, }, ]); }); it("should fail when library can't be found", async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - libraryMock.get.mockResolvedValue(null); - await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); - }); - - it('should force queue new assets', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: true, - }; - - assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - // eslint-disable-next-line @typescript-eslint/require-await - storageMock.walk.mockImplementation(async function* generator() { - yield ['/data/user1/photo.jpg']; - }); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); - - await sut.handleQueueAssetRefresh(mockLibraryJob); - - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { - name: JobName.LIBRARY_SCAN_ASSET, - data: { - id: libraryStub.externalLibrary1.id, - ownerId: libraryStub.externalLibrary1.owner.id, - assetPath: '/data/user1/photo.jpg', - force: true, - }, - }, - ]); + await expect(sut.handleQueueSyncFiles({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED); }); it('should ignore import paths that do not exist', async () => { @@ -276,16 +206,9 @@ describe(LibraryService.name, () => { assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibraryWithImportPaths1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, - }; - libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); - await sut.handleQueueAssetRefresh(mockLibraryJob); + await sut.handleQueueSyncFiles({ id: libraryStub.externalLibraryWithImportPaths1.id }); expect(storageMock.walk).toHaveBeenCalledWith({ pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]], @@ -296,9 +219,36 @@ describe(LibraryService.name, () => { }); }); - describe('handleOfflineCheck', () => { + describe('handleQueueRemoveDeleted', () => { + it('should queue online check of existing assets', async () => { + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); + storageMock.walk.mockImplementation(async function* generator() {}); + assetMock.getAll.mockResolvedValue({ items: [assetStub.external], hasNextPage: false }); + + await sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id }); + + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { + name: JobName.LIBRARY_SYNC_ASSET, + data: { + id: assetStub.external.id, + importPaths: libraryStub.externalLibrary1.importPaths, + exclusionPatterns: [], + }, + }, + ]); + }); + + it("should fail when library can't be found", async () => { + libraryMock.get.mockResolvedValue(null); + + await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SKIPPED); + }); + }); + + describe('handleSyncAsset', () => { it('should skip missing assets', async () => { - const mockAssetJob: ILibraryOfflineJob = { + const mockAssetJob: ILibraryAssetJob = { id: assetStub.external.id, importPaths: ['/'], exclusionPatterns: [], @@ -306,41 +256,31 @@ describe(LibraryService.name, () => { assetMock.getById.mockResolvedValue(null); - await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED); - expect(assetMock.update).not.toHaveBeenCalled(); - }); - - it('should do nothing with already-offline assets', async () => { - const mockAssetJob: ILibraryOfflineJob = { - id: assetStub.external.id, - importPaths: ['/'], - exclusionPatterns: [], - }; - - assetMock.getById.mockResolvedValue(assetStub.offline); - - await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.update).not.toHaveBeenCalled(); + expect(assetMock.remove).not.toHaveBeenCalled(); }); it('should offline assets no longer on disk', async () => { - const mockAssetJob: ILibraryOfflineJob = { + const mockAssetJob: ILibraryAssetJob = { id: assetStub.external.id, importPaths: ['/'], exclusionPatterns: [], }; assetMock.getById.mockResolvedValue(assetStub.external); + storageMock.stat.mockRejectedValue(new Error('ENOENT, no such file or directory')); - await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + isOffline: true, + deletedAt: expect.any(Date), + }); }); it('should offline assets matching an exclusion pattern', async () => { - const mockAssetJob: ILibraryOfflineJob = { + const mockAssetJob: ILibraryAssetJob = { id: assetStub.external.id, importPaths: ['/'], exclusionPatterns: ['**/user1/**'], @@ -348,13 +288,15 @@ describe(LibraryService.name, () => { assetMock.getById.mockResolvedValue(assetStub.external); - await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + isOffline: true, + deletedAt: expect.any(Date), + }); }); it('should set assets outside of import paths as offline', async () => { - const mockAssetJob: ILibraryOfflineJob = { + const mockAssetJob: ILibraryAssetJob = { id: assetStub.external.id, importPaths: ['/data/user2'], exclusionPatterns: [], @@ -363,28 +305,74 @@ describe(LibraryService.name, () => { assetMock.getById.mockResolvedValue(assetStub.external); storageMock.checkFileExists.mockResolvedValue(true); - await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + isOffline: true, + deletedAt: expect.any(Date), + }); }); it('should do nothing with online assets', async () => { - const mockAssetJob: ILibraryOfflineJob = { + const mockAssetJob: ILibraryAssetJob = { id: assetStub.external.id, importPaths: ['/'], exclusionPatterns: [], }; assetMock.getById.mockResolvedValue(assetStub.external); - storageMock.checkFileExists.mockResolvedValue(true); + storageMock.stat.mockResolvedValue({ mtime: assetStub.external.fileModifiedAt } as Stats); - await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.update).not.toHaveBeenCalled(); + expect(assetMock.updateAll).not.toHaveBeenCalled(); + }); + + it('should un-trash an asset previously marked as offline', async () => { + const mockAssetJob: ILibraryAssetJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], + }; + + assetMock.getById.mockResolvedValue(assetStub.trashedOffline); + storageMock.stat.mockResolvedValue({ mtime: assetStub.trashedOffline.fileModifiedAt } as Stats); + + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.trashedOffline.id], { + deletedAt: null, + fileCreatedAt: assetStub.trashedOffline.fileModifiedAt, + fileModifiedAt: assetStub.trashedOffline.fileModifiedAt, + isOffline: false, + originalFileName: 'path.jpg', + }); }); }); - describe('handleAssetRefresh', () => { + it('should update file when mtime has changed', async () => { + const mockAssetJob: ILibraryAssetJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: [], + }; + + const newMTime = new Date(); + assetMock.getById.mockResolvedValue(assetStub.external); + storageMock.stat.mockResolvedValue({ mtime: newMTime } as Stats); + + await expect(sut.handleSyncAsset(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.external.id], { + fileModifiedAt: newMTime, + fileCreatedAt: newMTime, + isOffline: false, + originalFileName: 'photo.jpg', + deletedAt: null, + }); + }); + + describe('handleSyncFile', () => { let mockUser: UserEntity; beforeEach(() => { @@ -397,42 +385,18 @@ describe(LibraryService.name, () => { } as Stats); }); - it('should reject an unknown file extension', async () => { - const mockLibraryJob: ILibraryFileJob = { - id: libraryStub.externalLibrary1.id, - ownerId: mockUser.id, - assetPath: '/data/user1/file.xyz', - force: false, - }; - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); - - await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should reject an unknown file type', async () => { - const mockLibraryJob: ILibraryFileJob = { - id: libraryStub.externalLibrary1.id, - ownerId: mockUser.id, - assetPath: '/data/user1/file.xyz', - force: false, - }; - - await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should add a new image', async () => { + it('should import a new asset', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, assetPath: '/data/user1/photo.jpg', - force: false, }; assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.image); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create.mock.calls).toEqual([ [ @@ -467,19 +431,19 @@ describe(LibraryService.name, () => { ]); }); - it('should add a new image with sidecar', async () => { + it('should import a new asset with sidecar', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, assetPath: '/data/user1/photo.jpg', - force: false, }; assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.image); storageMock.checkFileExists.mockResolvedValue(true); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create.mock.calls).toEqual([ [ @@ -514,18 +478,18 @@ describe(LibraryService.name, () => { ]); }); - it('should add a new video', async () => { + it('should import a new video', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, assetPath: '/data/user1/video.mp4', - force: false, }; assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.video); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); expect(assetMock.create.mock.calls).toEqual([ [ @@ -568,29 +532,27 @@ describe(LibraryService.name, () => { ]); }); - it('should not add an image to a soft deleted library', async () => { + it('should not import an asset to a soft deleted library', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, assetPath: '/data/user1/photo.jpg', - force: false, }; assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.image); libraryMock.get.mockResolvedValue({ ...libraryStub.externalLibrary1, deletedAt: new Date() }); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); expect(assetMock.create.mock.calls).toEqual([]); }); - it('should not import an asset when mtime matches db asset', async () => { + it('should not refresh a file whose mtime matches existing asset', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, assetPath: assetStub.hasFileExtension.originalPath, - force: false, }; storageMock.stat.mockResolvedValue({ @@ -601,190 +563,52 @@ describe(LibraryService.name, () => { assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); }); - it('should import an asset when mtime differs from db asset', async () => { + it('should skip existing asset', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, assetPath: '/data/user1/photo.jpg', - force: false, }; assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); - assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(jobMock.queue).toHaveBeenCalledWith({ - name: JobName.METADATA_EXTRACTION, - data: { - id: assetStub.image.id, - source: 'upload', - }, - }); - - expect(jobMock.queue).not.toHaveBeenCalledWith({ - name: JobName.VIDEO_CONVERSION, - data: { - id: assetStub.image.id, - }, - }); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); }); - it('should import an asset that is missing a file extension', async () => { - // This tests for the case where the file extension is missing from the asset path. - // This happened in previous versions of Immich + it('should not refresh an asset trashed by user', async () => { const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: mockUser.id, - assetPath: assetStub.missingFileExtension.originalPath, - force: false, - }; - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.missingFileExtension); - - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.updateAll).toHaveBeenCalledWith( - [assetStub.missingFileExtension.id], - expect.objectContaining({ originalFileName: 'photo.jpg' }), - ); - }); - - it('should set a missing asset to offline', async () => { - storageMock.stat.mockRejectedValue(new Error('Path not found')); - - const mockLibraryJob: ILibraryFileJob = { - id: assetStub.image.id, - ownerId: mockUser.id, - assetPath: '/data/user1/photo.jpg', - force: false, - }; - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); - assetMock.create.mockResolvedValue(assetStub.image); - - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, isOffline: true }); - expect(jobMock.queue).not.toHaveBeenCalled(); - expect(jobMock.queueAll).not.toHaveBeenCalled(); - }); - - it('should online a previously-offline asset', async () => { - const mockLibraryJob: ILibraryFileJob = { - id: assetStub.offline.id, - ownerId: mockUser.id, - assetPath: '/data/user1/photo.jpg', - force: false, - }; - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.offline); - assetMock.create.mockResolvedValue(assetStub.offline); - - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.offline.id, isOffline: false }); - - expect(jobMock.queue).toHaveBeenCalledWith({ - name: JobName.METADATA_EXTRACTION, - data: { - id: assetStub.offline.id, - source: 'upload', - }, - }); - - expect(jobMock.queue).not.toHaveBeenCalledWith({ - name: JobName.VIDEO_CONVERSION, - data: { - id: assetStub.offline.id, - }, - }); - }); - - it('should do nothing when mtime matches existing asset', async () => { - const mockLibraryJob: ILibraryFileJob = { - id: assetStub.image.id, - ownerId: assetStub.image.ownerId, - assetPath: '/data/user1/photo.jpg', - force: false, - }; - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.image); - assetMock.create.mockResolvedValue(assetStub.image); - - expect(assetMock.update).not.toHaveBeenCalled(); - - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - }); - - it('should refresh an existing asset if forced', async () => { - const mockLibraryJob: ILibraryFileJob = { - id: assetStub.image.id, - ownerId: assetStub.hasFileExtension.ownerId, assetPath: assetStub.hasFileExtension.originalPath, - force: true, }; - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.hasFileExtension); - assetMock.create.mockResolvedValue(assetStub.hasFileExtension); + assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.trashed); - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); - expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasFileExtension.id], { - fileCreatedAt: new Date('2023-01-01'), - fileModifiedAt: new Date('2023-01-01'), - originalFileName: assetStub.hasFileExtension.originalFileName, - }); + expect(jobMock.queue).not.toHaveBeenCalled(); + expect(jobMock.queueAll).not.toHaveBeenCalled(); }); - it('should refresh an existing asset with modified mtime', async () => { - const filemtime = new Date(); - filemtime.setSeconds(assetStub.image.fileModifiedAt.getSeconds() + 10); - - const mockLibraryJob: ILibraryFileJob = { - id: libraryStub.externalLibrary1.id, - ownerId: userStub.admin.id, - assetPath: '/data/user1/photo.jpg', - force: false, - }; - - storageMock.stat.mockResolvedValue({ - size: 100, - mtime: filemtime, - ctime: new Date('2023-01-01'), - } as Stats); - - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); - assetMock.create.mockResolvedValue(assetStub.image); - - await expect(sut.handleAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS); - - expect(assetMock.create).toHaveBeenCalled(); - const createdAsset = assetMock.create.mock.calls[0][0]; - - expect(createdAsset.fileModifiedAt).toEqual(filemtime); - }); - - it('should throw error when asset does not exist', async () => { + it('should throw BadRequestException when asset does not exist', async () => { storageMock.stat.mockRejectedValue(new Error("ENOENT, no such file or directory '/data/user1/photo.jpg'")); const mockLibraryJob: ILibraryFileJob = { id: libraryStub.externalLibrary1.id, ownerId: userStub.admin.id, assetPath: '/data/user1/photo.jpg', - force: false, }; assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null); assetMock.create.mockResolvedValue(assetStub.image); - await expect(sut.handleAssetRefresh(mockLibraryJob)).rejects.toBeInstanceOf(BadRequestException); + await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.FAILED); }); }); @@ -857,7 +681,6 @@ describe(LibraryService.name, () => { describe('getStatistics', () => { it('should return library statistics', async () => { - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); libraryMock.getStatistics.mockResolvedValue({ photos: 10, videos: 0, total: 10, usage: 1337 }); await expect(sut.getStatistics(libraryStub.externalLibrary1.id)).resolves.toEqual({ photos: 10, @@ -892,7 +715,7 @@ describe(LibraryService.name, () => { expect.objectContaining({ name: expect.any(String), importPaths: [], - exclusionPatterns: [], + exclusionPatterns: expect.any(Array), }), ); }); @@ -917,7 +740,7 @@ describe(LibraryService.name, () => { expect.objectContaining({ name: 'My Awesome Library', importPaths: [], - exclusionPatterns: [], + exclusionPatterns: expect.any(Array), }), ); }); @@ -947,7 +770,7 @@ describe(LibraryService.name, () => { expect.objectContaining({ name: expect.any(String), importPaths: ['/data/images', '/data/videos'], - exclusionPatterns: [], + exclusionPatterns: expect.any(Array), }), ); }); @@ -1092,12 +915,11 @@ describe(LibraryService.name, () => { expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.LIBRARY_SCAN_ASSET, + name: JobName.LIBRARY_SYNC_FILE, data: { id: libraryStub.externalLibraryWithImportPaths1.id, assetPath: '/foo/photo.jpg', ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id, - force: false, }, }, ]); @@ -1114,30 +936,16 @@ describe(LibraryService.name, () => { expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.LIBRARY_SCAN_ASSET, + name: JobName.LIBRARY_SYNC_FILE, data: { id: libraryStub.externalLibraryWithImportPaths1.id, assetPath: '/foo/photo.jpg', ownerId: libraryStub.externalLibraryWithImportPaths1.owner.id, - force: false, }, }, ]); }); - it('should handle a file unlink event', async () => { - libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); - libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); - storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }), - ); - - await sut.watchAll(); - - expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); - }); - it('should handle an error event', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); @@ -1232,72 +1040,23 @@ describe(LibraryService.name, () => { }); describe('queueScan', () => { - it('should queue a library scan of external library', async () => { + it('should queue a library scan', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - await sut.queueScan(libraryStub.externalLibrary1.id, {}); + await sut.queueScan(libraryStub.externalLibrary1.id); expect(jobMock.queue.mock.calls).toEqual([ [ { - name: JobName.LIBRARY_SCAN, + name: JobName.LIBRARY_QUEUE_SYNC_FILES, data: { id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, }, }, ], - ]); - }); - - it('should queue a library scan of all modified assets', async () => { - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - - await sut.queueScan(libraryStub.externalLibrary1.id, { refreshModifiedFiles: true }); - - expect(jobMock.queue.mock.calls).toEqual([ [ { - name: JobName.LIBRARY_SCAN, - data: { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: true, - refreshAllFiles: false, - }, - }, - ], - ]); - }); - - it('should queue a forced library scan', async () => { - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - - await sut.queueScan(libraryStub.externalLibrary1.id, { refreshAllFiles: true }); - - expect(jobMock.queue.mock.calls).toEqual([ - [ - { - name: JobName.LIBRARY_SCAN, - data: { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: true, - }, - }, - ], - ]); - }); - }); - - describe('queueEmptyTrash', () => { - it('should queue the trash job', async () => { - await sut.queueRemoveOffline(libraryStub.externalLibrary1.id); - - expect(jobMock.queue.mock.calls).toEqual([ - [ - { - name: JobName.LIBRARY_REMOVE_OFFLINE, + name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id: libraryStub.externalLibrary1.id, }, @@ -1311,7 +1070,7 @@ describe(LibraryService.name, () => { it('should queue the refresh job', async () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]); - await expect(sut.handleQueueAllScan({})).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueSyncAll()).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queue.mock.calls).toEqual([ [ @@ -1323,48 +1082,32 @@ describe(LibraryService.name, () => { ]); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.LIBRARY_SCAN, + name: JobName.LIBRARY_QUEUE_SYNC_FILES, data: { id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: true, - refreshAllFiles: false, - }, - }, - ]); - }); - - it('should queue the force refresh job', async () => { - libraryMock.getAll.mockResolvedValue([libraryStub.externalLibrary1]); - - await expect(sut.handleQueueAllScan({ force: true })).resolves.toBe(JobStatus.SUCCESS); - - expect(jobMock.queue).toHaveBeenCalledWith({ - name: JobName.LIBRARY_QUEUE_CLEANUP, - data: {}, - }); - - expect(jobMock.queueAll).toHaveBeenCalledWith([ - { - name: JobName.LIBRARY_SCAN, - data: { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: true, }, }, ]); }); }); - describe('handleRemoveOfflineFiles', () => { - it('should queue trash deletion jobs', async () => { - assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); + describe('handleQueueAssetOfflineCheck', () => { + it('should queue removal jobs', async () => { + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); + assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); assetMock.getById.mockResolvedValue(assetStub.image1); - await expect(sut.handleRemoveOffline({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleQueueSyncAssets({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } }, + { + name: JobName.LIBRARY_SYNC_ASSET, + data: { + id: assetStub.image1.id, + importPaths: libraryStub.externalLibrary1.importPaths, + exclusionPatterns: libraryStub.externalLibrary1.exclusionPatterns, + }, + }, ]); }); }); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 2aa0df402a373..52b786089cbaf 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { R_OK } from 'node:constants'; -import { Stats } from 'node:fs'; import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { StorageCore } from 'src/cores/storage.core'; @@ -10,27 +9,26 @@ import { CreateLibraryDto, LibraryResponseDto, LibraryStatsResponseDto, - ScanLibraryDto, + mapLibrary, UpdateLibraryDto, ValidateLibraryDto, ValidateLibraryImportPathResponseDto, ValidateLibraryResponseDto, - mapLibrary, } from 'src/dtos/library.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { LibraryEntity } from 'src/entities/library.entity'; import { AssetType } from 'src/enum'; -import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; +import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { ArgOf } from 'src/interfaces/event.interface'; import { - IBaseJob, IEntityJob, IJobRepository, + ILibraryAssetJob, ILibraryFileJob, - ILibraryOfflineJob, - ILibraryRefreshJob, - JOBS_LIBRARY_PAGINATION_SIZE, JobName, + JOBS_LIBRARY_PAGINATION_SIZE, JobStatus, } from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; @@ -78,11 +76,7 @@ export class LibraryService { this.jobRepository.addCronJob( 'libraryScan', scan.cronExpression, - () => - handlePromiseError( - this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }), - this.logger, - ), + () => handlePromiseError(this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ALL }), this.logger), scan.enabled, ); @@ -143,7 +137,7 @@ export class LibraryService { const handler = async () => { this.logger.debug(`File add event received for ${path} in library ${library.id}}`); if (matcher(path)) { - await this.scanAssets(library.id, [path], library.ownerId, false); + await this.syncFiles(library, [path]); } }; return handlePromiseError(handler(), this.logger); @@ -151,9 +145,13 @@ export class LibraryService { onChange: (path) => { const handler = async () => { this.logger.debug(`Detected file change for ${path} in library ${library.id}`); + const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); + if (asset) { + await this.syncAssets(library, [asset.id]); + } if (matcher(path)) { // Note: if the changed file was not previously imported, it will be imported now. - await this.scanAssets(library.id, [path], library.ownerId, false); + await this.syncFiles(library, [path]); } }; return handlePromiseError(handler(), this.logger); @@ -162,8 +160,8 @@ export class LibraryService { const handler = async () => { this.logger.debug(`Detected deleted file at ${path} in library ${library.id}`); const asset = await this.assetRepository.getByLibraryIdAndOriginalPath(library.id, path); - if (asset && matcher(path)) { - await this.assetRepository.update({ id: asset.id, isOffline: true }); + if (asset) { + await this.syncAssets(library, [asset.id]); } }; return handlePromiseError(handler(), this.logger); @@ -216,7 +214,7 @@ export class LibraryService { async getStatistics(id: string): Promise { const statistics = await this.repository.getStatistics(id); if (!statistics) { - throw new BadRequestException('Library not found'); + throw new BadRequestException(`Library ${id} not found`); } return statistics; } @@ -245,25 +243,33 @@ export class LibraryService { ownerId: dto.ownerId, name: dto.name ?? 'New External Library', importPaths: dto.importPaths ?? [], - exclusionPatterns: dto.exclusionPatterns ?? [], + exclusionPatterns: dto.exclusionPatterns ?? ['**/@eaDir/**', '**/._*'], }); return mapLibrary(library); } - private async scanAssets(libraryId: string, assetPaths: string[], ownerId: string, force = false) { + private async syncFiles({ id, ownerId }: LibraryEntity, assetPaths: string[]) { await this.jobRepository.queueAll( assetPaths.map((assetPath) => ({ - name: JobName.LIBRARY_SCAN_ASSET, + name: JobName.LIBRARY_SYNC_FILE, data: { - id: libraryId, + id, assetPath, ownerId, - force, }, })), ); } + private async syncAssets({ importPaths, exclusionPatterns }: LibraryEntity, assetIds: string[]) { + await this.jobRepository.queueAll( + assetIds.map((assetId) => ({ + name: JobName.LIBRARY_SYNC_ASSET, + data: { id: assetId, importPaths, exclusionPatterns }, + })), + ); + } + private async validateImportPath(importPath: string): Promise { const validation = new ValidateLibraryImportPathResponseDto(); validation.importPath = importPath; @@ -366,258 +372,182 @@ export class LibraryService { return JobStatus.SUCCESS; } - async handleAssetRefresh(job: ILibraryFileJob): Promise { + async handleSyncFile(job: ILibraryFileJob): Promise { + // Only needs to handle new assets const assetPath = path.normalize(job.assetPath); - const existingAssetEntity = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath); - - let stats: Stats; - try { - stats = await this.storageRepository.stat(assetPath); - } catch (error: Error | any) { - // Can't access file, probably offline - if (existingAssetEntity) { - // Mark asset as offline - this.logger.debug(`Marking asset as offline: ${assetPath}`); - - await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: true }); - return JobStatus.SUCCESS; - } else { - // File can't be accessed and does not already exist in db - throw new BadRequestException('Cannot access file', { cause: error }); - } - } - - let doImport = false; - let doRefresh = false; - - if (job.force) { - doRefresh = true; - } - - const originalFileName = parse(assetPath).base; - - if (!existingAssetEntity) { - // This asset is new to us, read it from disk - this.logger.debug(`Importing new asset: ${assetPath}`); - doImport = true; - } else if (stats.mtime.toISOString() !== existingAssetEntity.fileModifiedAt.toISOString()) { - // File modification time has changed since last time we checked, re-read from disk - this.logger.debug( - `File modification time has changed, re-importing asset: ${assetPath}. Old mtime: ${existingAssetEntity.fileModifiedAt}. New mtime: ${stats.mtime}`, - ); - doRefresh = true; - } else if (existingAssetEntity.originalFileName !== originalFileName) { - // TODO: We can likely remove this check in the second half of 2024 when all assets have likely been re-imported by all users - this.logger.debug( - `Asset is missing file extension, re-importing: ${assetPath}. Current incorrect filename: ${existingAssetEntity.originalFileName}.`, - ); - doRefresh = true; - } else if (!job.force && stats && !existingAssetEntity.isOffline) { - // Asset exists on disk and in db and mtime has not changed. Also, we are not forcing refresn. Therefore, do nothing - this.logger.debug(`Asset already exists in database and on disk, will not import: ${assetPath}`); - } - - if (stats && existingAssetEntity?.isOffline) { - // File was previously offline but is now online - this.logger.debug(`Marking previously-offline asset as online: ${assetPath}`); - await this.assetRepository.update({ id: existingAssetEntity.id, isOffline: false }); - doRefresh = true; - } - - if (!doImport && !doRefresh) { - // If we don't import, exit here + let asset = await this.assetRepository.getByLibraryIdAndOriginalPath(job.id, assetPath); + if (asset) { return JobStatus.SKIPPED; } - let assetType: AssetType; - - if (mimeTypes.isImage(assetPath)) { - assetType = AssetType.IMAGE; - } else if (mimeTypes.isVideo(assetPath)) { - assetType = AssetType.VIDEO; - } else { - throw new BadRequestException(`Unsupported file type ${assetPath}`); + let stat; + try { + stat = await this.storageRepository.stat(assetPath); + } catch (error: any) { + if (error.code === 'ENOENT') { + this.logger.error(`File not found: ${assetPath}`); + return JobStatus.SKIPPED; + } + this.logger.error(`Error reading file: ${assetPath}. Error: ${error}`); + return JobStatus.FAILED; } + this.logger.log(`Importing new library asset: ${assetPath}`); + + const library = await this.repository.get(job.id, true); + if (!library || library.deletedAt) { + this.logger.error('Cannot import asset into deleted library'); + return JobStatus.FAILED; + } + + // TODO: device asset id is deprecated, remove it + const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, ''); + + const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`); + // TODO: doesn't xmp replace the file extension? Will need investigation let sidecarPath: string | null = null; if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) { sidecarPath = `${assetPath}.xmp`; } - // TODO: device asset id is deprecated, remove it - const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, ''); + const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE; - let assetId; - if (doImport) { - const library = await this.repository.get(job.id, true); - if (library?.deletedAt) { - this.logger.error('Cannot import asset into deleted library'); - return JobStatus.FAILED; - } + const mtime = stat.mtime; - const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`); + asset = await this.assetRepository.create({ + ownerId: job.ownerId, + libraryId: job.id, + checksum: pathHash, + originalPath: assetPath, + deviceAssetId, + deviceId: 'Library Import', + fileCreatedAt: mtime, + fileModifiedAt: mtime, + localDateTime: mtime, + type: assetType, + originalFileName: parse(assetPath).base, - // TODO: In wait of refactoring the domain asset service, this function is just manually written like this - const addedAsset = await this.assetRepository.create({ - ownerId: job.ownerId, - libraryId: job.id, - checksum: pathHash, - originalPath: assetPath, - deviceAssetId, - deviceId: 'Library Import', - fileCreatedAt: stats.mtime, - fileModifiedAt: stats.mtime, - localDateTime: stats.mtime, - type: assetType, - originalFileName, - sidecarPath, - isExternal: true, - }); - assetId = addedAsset.id; - } else if (doRefresh && existingAssetEntity) { - assetId = existingAssetEntity.id; - await this.assetRepository.updateAll([existingAssetEntity.id], { - fileCreatedAt: stats.mtime, - fileModifiedAt: stats.mtime, - originalFileName, - }); - } else { - // Not importing and not refreshing, do nothing - return JobStatus.SKIPPED; - } + sidecarPath, + isExternal: true, + }); - this.logger.debug(`Queueing metadata extraction for: ${assetPath}`); - - await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: assetId, source: 'upload' } }); - - if (assetType === AssetType.VIDEO) { - await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: assetId } }); - } + await this.queuePostSyncJobs(asset); return JobStatus.SUCCESS; } - async queueScan(id: string, dto: ScanLibraryDto) { + async queuePostSyncJobs(asset: AssetEntity) { + this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`); + + await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); + + if (asset.type === AssetType.VIDEO) { + await this.jobRepository.queue({ name: JobName.VIDEO_CONVERSION, data: { id: asset.id } }); + } + } + + async queueScan(id: string) { await this.findOrFail(id); await this.jobRepository.queue({ - name: JobName.LIBRARY_SCAN, + name: JobName.LIBRARY_QUEUE_SYNC_FILES, data: { id, - refreshModifiedFiles: dto.refreshModifiedFiles ?? false, - refreshAllFiles: dto.refreshAllFiles ?? false, }, }); + await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } }); } - async queueRemoveOffline(id: string) { - this.logger.verbose(`Queueing offline file removal from library ${id}`); - await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, data: { id } }); - } - - async handleQueueAllScan(job: IBaseJob): Promise { - this.logger.debug(`Refreshing all external libraries: force=${job.force}`); + async handleQueueSyncAll(): Promise { + this.logger.debug(`Refreshing all external libraries`); await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); const libraries = await this.repository.getAll(true); await this.jobRepository.queueAll( libraries.map((library) => ({ - name: JobName.LIBRARY_SCAN, + name: JobName.LIBRARY_QUEUE_SYNC_FILES, + data: { + id: library.id, + }, + })), + ); + await this.jobRepository.queueAll( + libraries.map((library) => ({ + name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id: library.id, - refreshModifiedFiles: !job.force, - refreshAllFiles: job.force ?? false, }, })), ); return JobStatus.SUCCESS; } - async handleOfflineCheck(job: ILibraryOfflineJob): Promise { + async handleSyncAsset(job: ILibraryAssetJob): Promise { const asset = await this.assetRepository.getById(job.id); - if (!asset) { - // Asset is no longer in the database, skip return JobStatus.SKIPPED; } - if (asset.isOffline) { - this.logger.verbose(`Asset is already offline: ${asset.originalPath}`); - return JobStatus.SUCCESS; - } + const markOffline = async (explanation: string) => { + if (!asset.isOffline) { + this.logger.debug(`${explanation}, removing: ${asset.originalPath}`); + await this.assetRepository.updateAll([asset.id], { isOffline: true, deletedAt: new Date() }); + } + }; const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path)); if (!isInPath) { - this.logger.debug(`Asset is no longer in an import path, marking offline: ${asset.originalPath}`); - await this.assetRepository.update({ id: asset.id, isOffline: true }); + await markOffline('Asset is no longer in an import path'); return JobStatus.SUCCESS; } const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern)); if (isExcluded) { - this.logger.debug(`Asset is covered by an exclusion pattern, marking offline: ${asset.originalPath}`); - await this.assetRepository.update({ id: asset.id, isOffline: true }); + await markOffline('Asset is covered by an exclusion pattern'); return JobStatus.SUCCESS; } - const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK); - if (!fileExists) { - this.logger.debug(`Asset is no longer found on disk, marking offline: ${asset.originalPath}`); - await this.assetRepository.update({ id: asset.id, isOffline: true }); + let stat; + try { + stat = await this.storageRepository.stat(asset.originalPath); + } catch { + await markOffline('Asset is no longer on disk or is inaccessible because of permissions'); return JobStatus.SUCCESS; } - this.logger.verbose( - `Asset is found on disk, not covered by an exclusion pattern, and is in an import path, keeping online: ${asset.originalPath}`, - ); + const mtime = stat.mtime; + const isAssetModified = mtime.toISOString() !== asset.fileModifiedAt.toISOString(); + if (asset.isOffline || isAssetModified) { + this.logger.debug(`Asset was offline or modified, updating asset record ${asset.originalPath}`); + //TODO: When we have asset status, we need to leave deletedAt as is when status is trashed + await this.assetRepository.updateAll([asset.id], { + isOffline: false, + deletedAt: null, + fileCreatedAt: mtime, + fileModifiedAt: mtime, + originalFileName: parse(asset.originalPath).base, + }); + } + + if (isAssetModified) { + this.logger.debug(`Asset was modified, queuing metadata extraction for: ${asset.originalPath}`); + await this.queuePostSyncJobs(asset); + } return JobStatus.SUCCESS; } - async handleRemoveOffline(job: IEntityJob): Promise { - this.logger.debug(`Removing offline assets for library ${job.id}`); - - const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => - this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id, true), - ); - - let offlineAssets = 0; - for await (const assets of assetPagination) { - offlineAssets += assets.length; - if (assets.length > 0) { - this.logger.debug(`Discovered ${offlineAssets} offline assets in library ${job.id}`); - await this.jobRepository.queueAll( - assets.map((asset) => ({ - name: JobName.ASSET_DELETION, - data: { - id: asset.id, - deleteOnDisk: false, - }, - })), - ); - this.logger.verbose(`Queued deletion of ${assets.length} offline assets in library ${job.id}`); - } - } - - if (offlineAssets) { - this.logger.debug(`Finished queueing deletion of ${offlineAssets} offline assets for library ${job.id}`); - } else { - this.logger.debug(`Found no offline assets to delete from library ${job.id}`); - } - - return JobStatus.SUCCESS; - } - - async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise { + async handleQueueSyncFiles(job: IEntityJob): Promise { const library = await this.repository.get(job.id); if (!library) { + this.logger.debug(`Library ${job.id} not found, skipping refresh`); return JobStatus.SKIPPED; } - this.logger.log(`Refreshing library ${library.id}`); + this.logger.log(`Refreshing library ${library.id} for new assets`); const validImportPaths: string[] = []; @@ -630,55 +560,66 @@ export class LibraryService { } } - if (validImportPaths.length === 0) { + if (validImportPaths) { + const assetsOnDisk = this.storageRepository.walk({ + pathsToCrawl: validImportPaths, + includeHidden: false, + exclusionPatterns: library.exclusionPatterns, + take: JOBS_LIBRARY_PAGINATION_SIZE, + }); + + let count = 0; + + for await (const assetBatch of assetsOnDisk) { + count += assetBatch.length; + this.logger.debug(`Discovered ${count} asset(s) on disk for library ${library.id}...`); + await this.syncFiles(library, assetBatch); + this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`); + } + + if (count > 0) { + this.logger.debug(`Finished queueing scan of ${count} assets on disk for library ${library.id}`); + } else { + this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`); + } + } else { this.logger.warn(`No valid import paths found for library ${library.id}`); } - const assetsOnDisk = this.storageRepository.walk({ - pathsToCrawl: validImportPaths, - includeHidden: false, - exclusionPatterns: library.exclusionPatterns, - take: JOBS_LIBRARY_PAGINATION_SIZE, - }); + await this.repository.update({ id: job.id, refreshedAt: new Date() }); - let crawledAssets = 0; + return JobStatus.SUCCESS; + } - for await (const assetBatch of assetsOnDisk) { - crawledAssets += assetBatch.length; - this.logger.debug(`Discovered ${crawledAssets} asset(s) on disk for library ${library.id}...`); - await this.scanAssets(job.id, assetBatch, library.ownerId, job.refreshAllFiles ?? false); - this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`); + async handleQueueSyncAssets(job: IEntityJob): Promise { + const library = await this.repository.get(job.id); + if (!library) { + return JobStatus.SKIPPED; } - if (crawledAssets) { - this.logger.debug(`Finished queueing scan of ${crawledAssets} assets on disk for library ${library.id}`); - } else { - this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`); - } + this.logger.log(`Scanning library ${library.id} for removed assets`); const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => - this.assetRepository.getWith(pagination, WithProperty.IS_ONLINE, job.id), + this.assetRepository.getAll(pagination, { libraryId: job.id }), ); - let onlineAssetCount = 0; + let assetCount = 0; for await (const assets of onlineAssets) { - onlineAssetCount += assets.length; - this.logger.debug(`Discovered ${onlineAssetCount} asset(s) in library ${library.id}...`); + assetCount += assets.length; + this.logger.debug(`Discovered ${assetCount} asset(s) in library ${library.id}...`); await this.jobRepository.queueAll( assets.map((asset) => ({ - name: JobName.LIBRARY_CHECK_OFFLINE, - data: { id: asset.id, importPaths: validImportPaths, exclusionPatterns: library.exclusionPatterns }, + name: JobName.LIBRARY_SYNC_ASSET, + data: { id: asset.id, importPaths: library.importPaths, exclusionPatterns: library.exclusionPatterns }, })), ); - this.logger.debug(`Queued online check of ${assets.length} asset(s) in library ${library.id}...`); + this.logger.debug(`Queued check of ${assets.length} asset(s) in library ${library.id}...`); } - if (onlineAssetCount) { - this.logger.log(`Finished queueing online check of ${onlineAssetCount} assets for library ${library.id}`); + if (assetCount) { + this.logger.log(`Finished queueing check of ${assetCount} assets for library ${library.id}`); } - await this.repository.update({ id: job.id, refreshedAt: new Date() }); - return JobStatus.SUCCESS; } diff --git a/server/src/services/map.service.ts b/server/src/services/map.service.ts index ffd84a3e02bf6..5836505e54893 100644 --- a/server/src/services/map.service.ts +++ b/server/src/services/map.service.ts @@ -43,17 +43,6 @@ export class MapService { return this.mapRepository.getMapMarkers(userIds, albumIds, options); } - async getMapStyle(theme: 'light' | 'dark') { - const { map } = await this.configCore.getConfig({ withCache: false }); - const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle; - - if (styleUrl) { - return this.mapRepository.fetchStyle(styleUrl); - } - - return JSON.parse(await this.systemMetadataRepository.readFile(`./resources/style-${theme}.json`)); - } - async reverseGeocode(dto: MapReverseGeocodeDto) { const { lat: latitude, lon: longitude } = dto; // eventually this should probably return an array of results diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index bf493de0f39d1..88e9f478bdf35 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,5 +1,8 @@ import { Stats } from 'node:fs'; +import { ExifEntity } from 'src/entities/exif.entity'; import { + AssetFileType, + AssetType, AudioCodec, Colorspace, ImageFormat, @@ -7,14 +10,12 @@ import { TranscodeHWAccel, TranscodePolicy, VideoCodec, -} from 'src/config'; -import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetFileType, AssetType } from 'src/enum'; +} from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { IMediaRepository } from 'src/interfaces/media.interface'; +import { IMediaRepository, RawImageInfo } from 'src/interfaces/media.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -93,7 +94,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PREVIEW, + name: JobName.GENERATE_THUMBNAILS, data: { id: assetStub.image.id }, }, ]); @@ -126,7 +127,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PREVIEW, + name: JobName.GENERATE_THUMBNAILS, data: { id: assetStub.trashed.id }, }, ]); @@ -151,7 +152,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).not.toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PREVIEW, + name: JobName.GENERATE_THUMBNAILS, data: { id: assetStub.archived.id }, }, ]); @@ -201,7 +202,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_PREVIEW, + name: JobName.GENERATE_THUMBNAILS, data: { id: assetStub.image.id }, }, ]); @@ -225,7 +226,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBNAIL, + name: JobName.GENERATE_THUMBNAILS, data: { id: assetStub.image.id }, }, ]); @@ -249,7 +250,7 @@ describe(MediaService.name, () => { expect(assetMock.getWithout).toHaveBeenCalledWith({ skip: 0, take: 1000 }, WithoutProperty.THUMBNAIL); expect(jobMock.queueAll).toHaveBeenCalledWith([ { - name: JobName.GENERATE_THUMBHASH, + name: JobName.GENERATE_THUMBNAILS, data: { id: assetStub.image.id }, }, ]); @@ -258,10 +259,19 @@ describe(MediaService.name, () => { }); }); - describe('handleGeneratePreview', () => { + describe('handleGenerateThumbnails', () => { + let rawBuffer: Buffer; + let rawInfo: RawImageInfo; + + beforeEach(() => { + rawBuffer = Buffer.from('image data'); + rawInfo = { width: 100, height: 100, channels: 3 }; + mediaMock.decodeImage.mockResolvedValue({ data: rawBuffer, info: rawInfo }); + }); + it('should skip thumbnail generation if asset not found', async () => { - assetMock.getByIds.mockResolvedValue([]); - await sut.handleGeneratePreview({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); @@ -269,86 +279,106 @@ describe(MediaService.name, () => { it('should skip video thumbnail generation if no video stream', async () => { mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGeneratePreview({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); it('should skip invisible assets', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + assetMock.getById.mockResolvedValue(assetStub.livePhotoMotionAsset); - expect(await sut.handleGeneratePreview({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); + expect(await sut.handleGenerateThumbnails({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); - it.each(Object.values(ImageFormat))('should generate a %s preview for an image when specified', async (format) => { - systemMock.get.mockResolvedValue({ image: { previewFormat: format } }); - assetMock.getByIds.mockResolvedValue([assetStub.image]); - const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`; - - await sut.handleGeneratePreview({ id: assetStub.image.id }); - - expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith('/original/path.jpg', previewPath, { - size: 1440, - format, - quality: 80, - colorspace: Colorspace.SRGB, - processInvalidImages: false, - }); - expect(assetMock.upsertFile).toHaveBeenCalledWith({ - assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: previewPath, - }); - }); - it('should delete previous preview if different path', async () => { - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); - assetMock.getByIds.mockResolvedValue([assetStub.image]); + systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } }); + assetMock.getById.mockResolvedValue(assetStub.image); - await sut.handleGeneratePreview({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); }); - it('should generate a P3 thumbnail for a wide gamut image', async () => { - assetMock.getByIds.mockResolvedValue([ - { ...assetStub.image, exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity }, - ]); - await sut.handleGeneratePreview({ id: assetStub.image.id }); + it('should generate P3 thumbnails for a wide gamut image', async () => { + assetMock.getById.mockResolvedValue({ + ...assetStub.image, + exifInfo: { profileDescription: 'Adobe RGB', bitsPerSample: 14 } as ExifEntity, + }); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - '/original/path.jpg', - 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - { - size: 1440, - format: ImageFormat.JPEG, - quality: 80, - colorspace: Colorspace.P3, - processInvalidImages: false, - }, - ); - expect(assetMock.upsertFile).toHaveBeenCalledWith({ - assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + size: 1440, }); + + expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, + size: 1440, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + ); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.P3, + format: ImageFormat.WEBP, + size: 250, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + ); + + expect(mediaMock.generateThumbhash).toHaveBeenCalledOnce(); + expect(mediaMock.generateThumbhash).toHaveBeenCalledWith(rawBuffer, { + colorspace: Colorspace.P3, + processInvalidImages: false, + raw: rawInfo, + }); + + expect(assetMock.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + }, + { + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + }, + ]); + expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); }); it('should generate a thumbnail for a video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGeneratePreview({ id: assetStub.video.id }); + assetMock.getById.mockResolvedValue(assetStub.video); + await sut.handleGenerateThumbnails({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - { + expect.objectContaining({ inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'], outputOptions: [ '-fps_mode vfr', @@ -358,25 +388,32 @@ describe(MediaService.name, () => { String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_color_matrix=601:out_range=pc,format=yuv420p`, ], twoPass: false, - }, + }), ); - expect(assetMock.upsertFile).toHaveBeenCalledWith({ - assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - }); + expect(assetMock.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + }, + { + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + }, + ]); }); it('should tonemap thumbnail for hdr video', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGeneratePreview({ id: assetStub.video.id }); + assetMock.getById.mockResolvedValue(assetStub.video); + await sut.handleGenerateThumbnails({ id: assetStub.video.id }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - { + expect.objectContaining({ inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'], outputOptions: [ '-fps_mode vfr', @@ -386,13 +423,20 @@ describe(MediaService.name, () => { String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`, ], twoPass: false, - }, + }), ); - expect(assetMock.upsertFile).toHaveBeenCalledWith({ - assetId: 'asset-id', - type: AssetFileType.PREVIEW, - path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - }); + expect(assetMock.upsertFiles).toHaveBeenCalledWith([ + { + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + }, + { + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + }, + ]); }); it('should always generate video thumbnail in one pass', async () => { @@ -400,13 +444,13 @@ describe(MediaService.name, () => { systemMock.get.mockResolvedValue({ ffmpeg: { twoPass: true, maxBitrate: '5000k' }, }); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGeneratePreview({ id: assetStub.video.id }); + assetMock.getById.mockResolvedValue(assetStub.video); + await sut.handleGenerateThumbnails({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - { + expect.objectContaining({ inputOptions: ['-skip_frame nointra', '-sws_flags accurate_rnd+full_chroma_int'], outputOptions: [ '-fps_mode vfr', @@ -416,254 +460,228 @@ describe(MediaService.name, () => { String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=601:m=bt470bg:range=pc,format=yuv420p`, ], twoPass: false, - }, + }), ); }); it('should use scaling divisible by 2 even when using quick sync', async () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleGeneratePreview({ id: assetStub.video.id }); + assetMock.getById.mockResolvedValue(assetStub.video); + await sut.handleGenerateThumbnails({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringContaining('scale=-2:1440')]), twoPass: false, - }, + }), ); }); - it('should run successfully', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - await sut.handleGeneratePreview({ id: assetStub.image.id }); - }); - }); + it.each(Object.values(ImageFormat))('should generate an image preview in %s format', async (format) => { + systemMock.get.mockResolvedValue({ image: { preview: { format } } }); + assetMock.getById.mockResolvedValue(assetStub.image); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); + const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.${format}`; + const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.webp`; - describe('handleGenerateThumbnail', () => { - it('should skip thumbnail generation if asset not found', async () => { - assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); - expect(assetMock.update).not.toHaveBeenCalledWith(); - }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - it('should skip invisible assets', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); + expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + colorspace: Colorspace.SRGB, + processInvalidImages: false, + size: 1440, + }); - expect(await sut.handleGenerateThumbnail({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); - - expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); - expect(assetMock.update).not.toHaveBeenCalledWith(); - }); - - it.each(Object.values(ImageFormat))( - 'should generate a %s thumbnail for an image when specified', - async (format) => { - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: format } }); - assetMock.getByIds.mockResolvedValue([assetStub.image]); - const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`; - - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - - expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith('/original/path.jpg', thumbnailPath, { - size: 250, - format, - quality: 80, + expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { colorspace: Colorspace.SRGB, + format, + size: 1440, + quality: 80, processInvalidImages: false, - }); - expect(assetMock.upsertFile).toHaveBeenCalledWith({ - assetId: 'asset-id', - type: AssetFileType.THUMBNAIL, - path: thumbnailPath, - }); - }, - ); + raw: rawInfo, + }, + previewPath, + ); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.SRGB, + format: ImageFormat.WEBP, + size: 250, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + thumbnailPath, + ); + }); + + it.each(Object.values(ImageFormat))('should generate an image thumbnail in %s format', async (format) => { + systemMock.get.mockResolvedValue({ image: { thumbnail: { format } } }); + assetMock.getById.mockResolvedValue(assetStub.image); + const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); + mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); + const previewPath = `upload/thumbs/user-id/as/se/asset-id-preview.jpeg`; + const thumbnailPath = `upload/thumbs/user-id/as/se/asset-id-thumbnail.${format}`; + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.image.originalPath, { + colorspace: Colorspace.SRGB, + processInvalidImages: false, + size: 1440, + }); + + expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.SRGB, + format: ImageFormat.JPEG, + size: 1440, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + previewPath, + ); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + { + colorspace: Colorspace.SRGB, + format, + size: 250, + quality: 80, + processInvalidImages: false, + raw: rawInfo, + }, + thumbnailPath, + ); + }); it('should delete previous thumbnail if different path', async () => { - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); - assetMock.getByIds.mockResolvedValue([assetStub.image]); + systemMock.get.mockResolvedValue({ image: { thumbnail: { format: ImageFormat.WEBP } } }); + assetMock.getById.mockResolvedValue(assetStub.image); - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext'); }); - }); - it('should generate a P3 thumbnail for a wide gamut image', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + it('should extract embedded image if enabled and available', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); + systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } }); + assetMock.getById.mockResolvedValue(assetStub.imageDng); - expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id/as/se'); - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.imageDng.originalPath, - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', - { - format: ImageFormat.WEBP, - size: 250, - quality: 80, + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(extractedPath, { colorspace: Colorspace.P3, processInvalidImages: false, - }, - ); - expect(assetMock.upsertFile).toHaveBeenCalledWith({ - assetId: 'asset-id', - type: AssetFileType.THUMBNAIL, - path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + size: 1440, + }); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); }); - }); - it('should extract embedded image if enabled and available', async () => { - mediaMock.extract.mockResolvedValue(true); - mediaMock.getImageDimensions.mockResolvedValue({ width: 3840, height: 2160 }); - systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } }); - assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + it('should resize original image if embedded image is too small', async () => { + mediaMock.extract.mockResolvedValue(true); + mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); + systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } }); + assetMock.getById.mockResolvedValue(assetStub.imageDng); - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); - expect(mediaMock.generateThumbnail.mock.calls).toEqual([ - [ - extractedPath, - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', - { - format: ImageFormat.WEBP, - size: 250, - quality: 80, - colorspace: Colorspace.P3, - processInvalidImages: false, - }, - ], - ]); - expect(extractedPath?.endsWith('.tmp')).toBe(true); - expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); - }); + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + size: 1440, + }); + const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); + expect(extractedPath?.endsWith('.tmp')).toBe(true); + expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); + }); - it('should resize original image if embedded image is too small', async () => { - mediaMock.extract.mockResolvedValue(true); - mediaMock.getImageDimensions.mockResolvedValue({ width: 1000, height: 1000 }); - systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } }); - assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + it('should resize original image if embedded image not found', async () => { + systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } }); + assetMock.getById.mockResolvedValue(assetStub.imageDng); - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); - expect(mediaMock.generateThumbnail.mock.calls).toEqual([ - [ + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + size: 1440, + }); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + it('should resize original image if embedded image extraction is not enabled', async () => { + systemMock.get.mockResolvedValue({ image: { extractEmbedded: false } }); + assetMock.getById.mockResolvedValue(assetStub.imageDng); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(mediaMock.extract).not.toHaveBeenCalled(); + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith(assetStub.imageDng.originalPath, { + colorspace: Colorspace.P3, + processInvalidImages: false, + size: 1440, + }); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + }); + + it('should process invalid images if enabled', async () => { + vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true'); + + assetMock.getById.mockResolvedValue(assetStub.imageDng); + + await sut.handleGenerateThumbnails({ id: assetStub.image.id }); + + expect(mediaMock.decodeImage).toHaveBeenCalledOnce(); + expect(mediaMock.decodeImage).toHaveBeenCalledWith( assetStub.imageDng.originalPath, + expect.objectContaining({ processInvalidImages: true }), + ); + + expect(mediaMock.generateThumbnail).toHaveBeenCalledTimes(2); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ processInvalidImages: true }), + 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + ); + expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ processInvalidImages: true }), 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', - { - format: ImageFormat.WEBP, - size: 250, - quality: 80, - colorspace: Colorspace.P3, - processInvalidImages: false, - }, - ], - ]); - const extractedPath = mediaMock.extract.mock.calls.at(-1)?.[1].toString(); - expect(extractedPath?.endsWith('.tmp')).toBe(true); - expect(storageMock.unlink).toHaveBeenCalledWith(extractedPath); - }); + ); - it('should resize original image if embedded image not found', async () => { - systemMock.get.mockResolvedValue({ image: { extractEmbedded: true } }); - assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); + expect(mediaMock.generateThumbhash).toHaveBeenCalledOnce(); + expect(mediaMock.generateThumbhash).toHaveBeenCalledWith( + rawBuffer, + expect.objectContaining({ processInvalidImages: true }), + ); - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.imageDng.originalPath, - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', - { - format: ImageFormat.WEBP, - size: 250, - quality: 80, - colorspace: Colorspace.P3, - processInvalidImages: false, - }, - ); - expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); - }); - - it('should resize original image if embedded image extraction is not enabled', async () => { - systemMock.get.mockResolvedValue({ image: { extractEmbedded: false } }); - assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); - - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - - expect(mediaMock.extract).not.toHaveBeenCalled(); - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.imageDng.originalPath, - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', - { - format: ImageFormat.WEBP, - size: 250, - quality: 80, - colorspace: Colorspace.P3, - processInvalidImages: false, - }, - ); - expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); - }); - - it('should process invalid images if enabled', async () => { - vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true'); - - assetMock.getByIds.mockResolvedValue([assetStub.imageDng]); - - await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.imageDng.originalPath, - 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', - { - format: ImageFormat.WEBP, - size: 250, - quality: 80, - colorspace: Colorspace.P3, - processInvalidImages: true, - }, - ); - expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); - vi.unstubAllEnvs(); - }); - - describe('handleGenerateThumbhash', () => { - it('should skip thumbhash generation if asset not found', async () => { - assetMock.getByIds.mockResolvedValue([]); - await sut.handleGenerateThumbhash({ id: assetStub.image.id }); - expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); - }); - - it('should skip thumbhash generation if resize path is missing', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.noResizePath]); - await sut.handleGenerateThumbhash({ id: assetStub.noResizePath.id }); - expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); - }); - - it('should skip invisible assets', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.livePhotoMotionAsset]); - - expect(await sut.handleGenerateThumbhash({ id: assetStub.livePhotoMotionAsset.id })).toEqual(JobStatus.SKIPPED); - - expect(mediaMock.generateThumbhash).not.toHaveBeenCalled(); - expect(assetMock.update).not.toHaveBeenCalledWith(); - }); - - it('should generate a thumbhash', async () => { - const thumbhashBuffer = Buffer.from('a thumbhash', 'utf8'); - assetMock.getByIds.mockResolvedValue([assetStub.image]); - mediaMock.generateThumbhash.mockResolvedValue(thumbhashBuffer); - - await sut.handleGenerateThumbhash({ id: assetStub.image.id }); - - expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer }); + expect(mediaMock.getImageDimensions).not.toHaveBeenCalled(); + vi.unstubAllEnvs(); }); }); @@ -730,21 +748,22 @@ describe(MediaService.name, () => { it('should transcode the longest stream', async () => { assetMock.getByIds.mockResolvedValue([assetStub.video]); + loggerMock.isLevelEnabled.mockReturnValue(false); mediaMock.probe.mockResolvedValue(probeStub.multipleVideoStreams); await sut.handleVideoConversion({ id: assetStub.video.id }); - expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext'); + expect(mediaMock.probe).toHaveBeenCalledWith('/original/path.ext', { countFrames: false }); expect(systemMock.get).toHaveBeenCalled(); expect(storageMock.mkdirSync).toHaveBeenCalled(); expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-map 0:0', '-map 0:1']), twoPass: false, - }, + }), ); }); @@ -770,11 +789,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), twoPass: false, - }, + }), ); }); @@ -785,11 +804,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), twoPass: false, - }, + }), ); }); @@ -800,11 +819,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.any(Array), twoPass: false, - }, + }), ); }); @@ -815,11 +834,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('scale')]), twoPass: false, - }, + }), ); }); @@ -831,11 +850,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:720/)]), twoPass: false, - }, + }), ); }); @@ -847,11 +866,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=720:-2/)]), twoPass: false, - }, + }), ); }); @@ -863,11 +882,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=-2:354/)]), twoPass: false, - }, + }), ); }); @@ -879,11 +898,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([expect.stringMatching(/scale(_.+)?=354:-2/)]), twoPass: false, - }, + }), ); }); @@ -897,11 +916,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v copy', '-c:a aac']), twoPass: false, - }, + }), ); }); @@ -919,11 +938,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining(['-tag:v hvc1']), twoPass: false, - }, + }), ); }); @@ -941,11 +960,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v copy', '-tag:v hvc1']), twoPass: false, - }, + }), ); }); @@ -957,11 +976,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']), twoPass: false, - }, + }), ); }); @@ -972,11 +991,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v copy', '-c:a copy']), twoPass: false, - }, + }), ); }); @@ -1035,11 +1054,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-maxrate 4500k', '-bufsize 9000k']), twoPass: false, - }, + }), ); }); @@ -1051,11 +1070,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']), twoPass: true, - }, + }), ); }); @@ -1067,11 +1086,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy']), twoPass: false, - }, + }), ); }); @@ -1089,11 +1108,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-b:v 3104k', '-minrate 1552k', '-maxrate 4500k']), twoPass: true, - }, + }), ); }); @@ -1111,11 +1130,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-maxrate')]), twoPass: true, - }, + }), ); }); @@ -1127,11 +1146,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-cpu-used 2']), twoPass: false, - }, + }), ); }); @@ -1143,11 +1162,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-cpu-used')]), twoPass: false, - }, + }), ); }); @@ -1159,11 +1178,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-threads 2']), twoPass: false, - }, + }), ); }); @@ -1175,11 +1194,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-threads 1', '-x264-params frame-threads=1:pools=none']), twoPass: false, - }, + }), ); }); @@ -1191,11 +1210,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]), twoPass: false, - }, + }), ); }); @@ -1207,11 +1226,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v hevc', '-threads 1', '-x265-params frame-threads=1:pools=none']), twoPass: false, - }, + }), ); }); @@ -1223,11 +1242,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.not.arrayContaining([expect.stringContaining('-threads')]), twoPass: false, - }, + }), ); }); @@ -1239,7 +1258,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining([ '-c:v av1', @@ -1254,7 +1273,7 @@ describe(MediaService.name, () => { '-crf 23', ]), twoPass: false, - }, + }), ); }); @@ -1266,11 +1285,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-preset 4']), twoPass: false, - }, + }), ); }); @@ -1282,11 +1301,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-svtav1-params mbr=2M']), twoPass: false, - }, + }), ); }); @@ -1298,11 +1317,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-svtav1-params lp=4']), twoPass: false, - }, + }), ); }); @@ -1314,11 +1333,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-svtav1-params lp=4:mbr=2M']), twoPass: false, - }, + }), ); }); @@ -1360,7 +1379,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.arrayContaining([ '-tune hq', @@ -1381,7 +1400,7 @@ describe(MediaService.name, () => { '-cq:v 23', ]), twoPass: false, - }, + }), ); }); @@ -1399,11 +1418,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.arrayContaining([expect.stringContaining('-multipass')]), twoPass: false, - }, + }), ); }); @@ -1415,11 +1434,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.arrayContaining(['-cq:v 23', '-maxrate 10000k', '-bufsize 6897k']), twoPass: false, - }, + }), ); }); @@ -1431,11 +1450,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.not.stringContaining('-maxrate'), twoPass: false, - }, + }), ); }); @@ -1447,11 +1466,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]), twoPass: false, - }, + }), ); }); @@ -1463,11 +1482,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda']), outputOptions: expect.not.arrayContaining([expect.stringContaining('-multipass')]), twoPass: false, - }, + }), ); }); @@ -1481,7 +1500,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel cuda', '-hwaccel_output_format cuda', @@ -1490,7 +1509,7 @@ describe(MediaService.name, () => { ]), outputOptions: expect.arrayContaining([expect.stringContaining('scale_cuda=-2:720:format=nv12')]), twoPass: false, - }, + }), ); }); @@ -1504,7 +1523,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']), outputOptions: expect.arrayContaining([ expect.stringContaining( @@ -1512,7 +1531,7 @@ describe(MediaService.name, () => { ), ]), twoPass: false, - }, + }), ); }); @@ -1525,7 +1544,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), outputOptions: expect.arrayContaining([ `-c:v h264_qsv`, @@ -1546,7 +1565,7 @@ describe(MediaService.name, () => { '-bufsize 20000k', ]), twoPass: false, - }, + }), ); }); @@ -1565,14 +1584,14 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', '-filter_hw_device hw', ]), outputOptions: expect.any(Array), twoPass: false, - }, + }), ); }); @@ -1585,11 +1604,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), outputOptions: expect.not.arrayContaining([expect.stringContaining('-preset')]), twoPass: false, - }, + }), ); }); @@ -1602,11 +1621,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-init_hw_device qsv=hw', '-filter_hw_device hw']), outputOptions: expect.arrayContaining(['-low_power 1']), twoPass: false, - }, + }), ); }); @@ -1632,7 +1651,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel qsv', '-hwaccel_output_format qsv', @@ -1644,7 +1663,7 @@ describe(MediaService.name, () => { expect.stringContaining('scale_qsv=-1:720:async_depth=4:mode=hq:format=nv12'), ]), twoPass: false, - }, + }), ); }); @@ -1661,7 +1680,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel qsv', '-hwaccel_output_format qsv', @@ -1674,7 +1693,7 @@ describe(MediaService.name, () => { ), ]), twoPass: false, - }, + }), ); }); @@ -1690,11 +1709,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel qsv', '-qsv_device /dev/dri/renderD129']), outputOptions: expect.any(Array), twoPass: false, - }, + }), ); }); @@ -1707,7 +1726,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel', @@ -1727,7 +1746,7 @@ describe(MediaService.name, () => { '-rc_mode 1', ]), twoPass: false, - }, + }), ); }); @@ -1740,7 +1759,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel', @@ -1753,7 +1772,7 @@ describe(MediaService.name, () => { '-rc_mode 3', ]), twoPass: false, - }, + }), ); }); @@ -1766,7 +1785,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel', @@ -1779,7 +1798,7 @@ describe(MediaService.name, () => { '-rc_mode 1', ]), twoPass: false, - }, + }), ); }); @@ -1792,14 +1811,14 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel', ]), outputOptions: expect.not.arrayContaining([expect.stringContaining('-compression_level')]), twoPass: false, - }, + }), ); }); @@ -1812,14 +1831,14 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/card1', '-filter_hw_device accel', ]), outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), twoPass: false, - }, + }), ); }); @@ -1832,14 +1851,14 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD130', '-filter_hw_device accel', ]), outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), twoPass: false, - }, + }), ); }); @@ -1854,14 +1873,14 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel', ]), outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), twoPass: false, - }, + }), ); }); @@ -1876,11 +1895,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenLastCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.any(Array), outputOptions: expect.arrayContaining(['-c:v h264']), twoPass: false, - }, + }), ); }); @@ -1903,7 +1922,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining([ '-hwaccel rkmpp', '-hwaccel_output_format drm_prime', @@ -1926,7 +1945,7 @@ describe(MediaService.name, () => { '-qp_init 23', ]), twoPass: false, - }, + }), ); }); @@ -1947,11 +1966,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([`-c:v hevc_rkmpp`, '-level 153', '-rc_mode AVBR', '-b:v 10000k']), twoPass: false, - }, + }), ); }); @@ -1967,11 +1986,11 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([`-c:v h264_rkmpp`, '-level 51', '-rc_mode CQP', '-qp_init 30']), twoPass: false, - }, + }), ); }); @@ -1987,7 +2006,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']), outputOptions: expect.arrayContaining([ expect.stringContaining( @@ -1995,7 +2014,7 @@ describe(MediaService.name, () => { ), ]), twoPass: false, - }, + }), ); }); @@ -2011,7 +2030,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: [], outputOptions: expect.arrayContaining([ expect.stringContaining( @@ -2019,7 +2038,7 @@ describe(MediaService.name, () => { ), ]), twoPass: false, - }, + }), ); }); @@ -2035,7 +2054,7 @@ describe(MediaService.name, () => { expect(mediaMock.transcode).toHaveBeenCalledWith( '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { + expect.objectContaining({ inputOptions: [], outputOptions: expect.arrayContaining([ expect.stringContaining( @@ -2043,69 +2062,101 @@ describe(MediaService.name, () => { ), ]), twoPass: false, + }), + ); + }); + + it('should tonemap when policy is required and video is hdr', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); + systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining([ + '-c:v h264', + '-c:a copy', + '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', + ]), + twoPass: false, + }), + ); + }); + + it('should tonemap when policy is optimal and video is hdr', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); + systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining([ + '-c:v h264', + '-c:a copy', + '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', + ]), + twoPass: false, + }), + ); + }); + + it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); + systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining([ + '-c:v h264', + '-c:a copy', + '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', + ]), + twoPass: false, + }), + ); + }); + + it('should count frames for progress when log level is debug', async () => { + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + loggerMock.isLevelEnabled.mockReturnValue(true); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + + await sut.handleVideoConversion({ id: assetStub.video.id }); + + expect(mediaMock.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: true }); + expect(mediaMock.transcode).toHaveBeenCalledWith( + assetStub.video.originalPath, + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + { + inputOptions: expect.any(Array), + outputOptions: expect.any(Array), + twoPass: false, + progress: { + frameCount: probeStub.videoStream2160p.videoStreams[0].frameCount, + percentInterval: expect.any(Number), + }, }, ); }); - }); - it('should tonemap when policy is required and video is hdr', async () => { - mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); - systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } }); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); - expect(mediaMock.transcode).toHaveBeenCalledWith( - '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { - inputOptions: expect.any(Array), - outputOptions: expect.arrayContaining([ - '-c:v h264', - '-c:a copy', - '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', - ]), - twoPass: false, - }, - ); - }); + it('should not count frames for progress when log level is not debug', async () => { + mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); + loggerMock.isLevelEnabled.mockReturnValue(false); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + await sut.handleVideoConversion({ id: assetStub.video.id }); - it('should tonemap when policy is optimal and video is hdr', async () => { - mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); - systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.OPTIMAL } }); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); - expect(mediaMock.transcode).toHaveBeenCalledWith( - '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { - inputOptions: expect.any(Array), - outputOptions: expect.arrayContaining([ - '-c:v h264', - '-c:a copy', - '-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', - ]), - twoPass: false, - }, - ); - }); - - it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => { - mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); - systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } }); - assetMock.getByIds.mockResolvedValue([assetStub.video]); - await sut.handleVideoConversion({ id: assetStub.video.id }); - expect(mediaMock.transcode).toHaveBeenCalledWith( - '/original/path.ext', - 'upload/encoded-video/user-id/as/se/asset-id.mp4', - { - inputOptions: expect.any(Array), - outputOptions: expect.arrayContaining([ - '-c:v h264', - '-c:a copy', - '-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=bt709:t=bt709:m=bt709:range=pc,format=yuv420p', - ]), - twoPass: false, - }, - ); + expect(mediaMock.probe).toHaveBeenCalledWith(assetStub.video.originalPath, { countFrames: false }); + }); }); describe('isSRGB', () => { diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index e74335bdc391c..71f432e040c43 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,22 +1,25 @@ -import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { dirname } from 'node:path'; + +import { StorageCore } from 'src/cores/storage.core'; +import { SystemConfigCore } from 'src/cores/system-config.core'; +import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { AssetEntity } from 'src/entities/asset.entity'; import { + AssetFileType, + AssetPathType, + AssetType, AudioCodec, Colorspace, - ImageFormat, + LogLevel, + StorageFolder, TranscodeHWAccel, TranscodePolicy, TranscodeTarget, VideoCodec, VideoContainer, -} from 'src/config'; -import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; -import { SystemConfigCore } from 'src/cores/system-config.core'; -import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; -import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetPathType } from 'src/entities/move.entity'; -import { AssetFileType, AssetType } from 'src/enum'; -import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; +} from 'src/enum'; +import { IAssetRepository, UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IBaseJob, @@ -29,7 +32,13 @@ import { QueueName, } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { AudioStreamInfo, IMediaRepository, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface'; +import { + AudioStreamInfo, + IMediaRepository, + TranscodeCommand, + VideoFormat, + VideoStreamInfo, +} from 'src/interfaces/media.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -87,18 +96,10 @@ export class MediaService { for (const asset of assets) { const { previewFile, thumbnailFile } = getAssetFiles(asset.files); - if (!previewFile || force) { - jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); + if (!previewFile || !thumbnailFile || !asset.thumbhash || force) { + jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id: asset.id } }); continue; } - - if (!thumbnailFile) { - jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); - } - - if (!asset.thumbhash) { - jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); - } } await this.jobRepository.queueAll(jobs); @@ -166,154 +167,134 @@ export class MediaService { return JobStatus.FAILED; } - await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.previewFormat); - await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); + await this.storageCore.moveAssetImage(asset, AssetPathType.PREVIEW, image.preview.format); + await this.storageCore.moveAssetImage(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); await this.storageCore.moveAssetVideo(asset); return JobStatus.SUCCESS; } - async handleGeneratePreview({ id }: IEntityJob): Promise { - const [{ image }, [asset]] = await Promise.all([ - this.configCore.getConfig({ withCache: true }), - this.assetRepository.getByIds([id], { exifInfo: true, files: true }), - ]); + async handleGenerateThumbnails({ id }: IEntityJob): Promise { + const asset = await this.assetRepository.getById(id, { exifInfo: true, files: true }); if (!asset) { + this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`); return JobStatus.FAILED; } if (!asset.isVisible) { + this.logger.verbose(`Thumbnail generation skipped for asset ${id}: not visible`); return JobStatus.SKIPPED; } - const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat); - if (!previewPath) { + let generated: { previewPath: string; thumbnailPath: string; thumbhash: Buffer }; + if (asset.type === AssetType.IMAGE) { + generated = await this.generateImageThumbnails(asset); + } else if (asset.type === AssetType.VIDEO) { + generated = await this.generateVideoThumbnails(asset); + } else { + this.logger.warn(`Skipping thumbnail generation for asset ${id}: ${asset.type} is not an image or video`); return JobStatus.SKIPPED; } - const { previewFile } = getAssetFiles(asset.files); - if (previewFile && previewFile.path !== previewPath) { + const { previewFile, thumbnailFile } = getAssetFiles(asset.files); + const toUpsert: UpsertFileOptions[] = []; + if (previewFile?.path !== generated.previewPath) { + toUpsert.push({ assetId: asset.id, path: generated.previewPath, type: AssetFileType.PREVIEW }); + } + + if (thumbnailFile?.path !== generated.thumbnailPath) { + toUpsert.push({ assetId: asset.id, path: generated.thumbnailPath, type: AssetFileType.THUMBNAIL }); + } + + if (toUpsert.length > 0) { + await this.assetRepository.upsertFiles(toUpsert); + } + + const pathsToDelete = []; + if (previewFile && previewFile.path !== generated.previewPath) { this.logger.debug(`Deleting old preview for asset ${asset.id}`); - await this.storageRepository.unlink(previewFile.path); + pathsToDelete.push(previewFile.path); } - await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.PREVIEW, path: previewPath }); - await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); - await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() }); - - return JobStatus.SUCCESS; - } - - private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) { - const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true }); - const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize; - const path = StorageCore.getImagePath(asset, type, format); - this.storageCore.ensureFolders(path); - - switch (asset.type) { - case AssetType.IMAGE: { - const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath); - const extractedPath = StorageCore.getTempPathInDir(dirname(path)); - const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath)); - - try { - const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize)); - const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; - const imageOptions = { - format, - size, - colorspace, - quality: image.quality, - processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', - }; - - const outputPath = useExtracted ? extractedPath : asset.originalPath; - await this.mediaRepository.generateThumbnail(outputPath, path, imageOptions); - } finally { - if (didExtract) { - await this.storageRepository.unlink(extractedPath); - } - } - break; - } - - case AssetType.VIDEO: { - const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); - const mainVideoStream = this.getMainStream(videoStreams); - if (!mainVideoStream) { - this.logger.warn(`Skipped thumbnail generation for asset ${asset.id}: no video streams found`); - return; - } - const mainAudioStream = this.getMainStream(audioStreams); - const config = ThumbnailConfig.create({ ...ffmpeg, targetResolution: size.toString() }); - const options = config.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream); - await this.mediaRepository.transcode(asset.originalPath, path, options); - break; - } - - default: { - throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`); - } - } - - const assetLabel = asset.isExternal ? asset.originalPath : asset.id; - this.logger.log( - `Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} ${type} for asset ${assetLabel}`, - ); - - return path; - } - - async handleGenerateThumbnail({ id }: IEntityJob): Promise { - const [{ image }, [asset]] = await Promise.all([ - this.configCore.getConfig({ withCache: true }), - this.assetRepository.getByIds([id], { exifInfo: true, files: true }), - ]); - if (!asset) { - return JobStatus.FAILED; - } - - if (!asset.isVisible) { - return JobStatus.SKIPPED; - } - - const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); - if (!thumbnailPath) { - return JobStatus.SKIPPED; - } - - const { thumbnailFile } = getAssetFiles(asset.files); - if (thumbnailFile && thumbnailFile.path !== thumbnailPath) { + if (thumbnailFile && thumbnailFile.path !== generated.thumbnailPath) { this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); - await this.storageRepository.unlink(thumbnailFile.path); + pathsToDelete.push(thumbnailFile.path); } - await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: thumbnailPath }); - await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); - await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() }); + if (pathsToDelete.length > 0) { + await Promise.all(pathsToDelete.map((path) => this.storageRepository.unlink(path))); + } + + if (asset.thumbhash != generated.thumbhash) { + await this.assetRepository.update({ id: asset.id, thumbhash: generated.thumbhash }); + } + + await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date(), thumbnailAt: new Date() }); return JobStatus.SUCCESS; } - async handleGenerateThumbhash({ id }: IEntityJob): Promise { - const [asset] = await this.assetRepository.getByIds([id], { files: true }); - if (!asset) { - return JobStatus.FAILED; + private async generateImageThumbnails(asset: AssetEntity) { + const { image } = await this.configCore.getConfig({ withCache: true }); + const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format); + const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); + this.storageCore.ensureFolders(previewPath); + + const shouldExtract = image.extractEmbedded && mimeTypes.isRaw(asset.originalPath); + const extractedPath = StorageCore.getTempPathInDir(dirname(previewPath)); + const didExtract = shouldExtract && (await this.mediaRepository.extract(asset.originalPath, extractedPath)); + + try { + const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.preview.size)); + const inputPath = useExtracted ? extractedPath : asset.originalPath; + const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace; + const processInvalidImages = process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true'; + + const decodeOptions = { colorspace, processInvalidImages, size: image.preview.size }; + const { data, info } = await this.mediaRepository.decodeImage(inputPath, decodeOptions); + + const options = { colorspace, processInvalidImages, raw: info }; + const outputs = await Promise.all([ + this.mediaRepository.generateThumbnail(data, { ...image.thumbnail, ...options }, thumbnailPath), + this.mediaRepository.generateThumbnail(data, { ...image.preview, ...options }, previewPath), + this.mediaRepository.generateThumbhash(data, options), + ]); + + return { previewPath, thumbnailPath, thumbhash: outputs[2] }; + } finally { + if (didExtract) { + await this.storageRepository.unlink(extractedPath); + } } + } - if (!asset.isVisible) { - return JobStatus.SKIPPED; + private async generateVideoThumbnails(asset: AssetEntity) { + const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true }); + const previewPath = StorageCore.getImagePath(asset, AssetPathType.PREVIEW, image.preview.format); + const thumbnailPath = StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, image.thumbnail.format); + this.storageCore.ensureFolders(previewPath); + + const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath); + const mainVideoStream = this.getMainStream(videoStreams); + if (!mainVideoStream) { + throw new Error(`No video streams found for asset ${asset.id}`); } + const mainAudioStream = this.getMainStream(audioStreams); - const { previewFile } = getAssetFiles(asset.files); - if (!previewFile) { - return JobStatus.FAILED; - } + const previewConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.preview.size.toString() }); + const thumbnailConfig = ThumbnailConfig.create({ ...ffmpeg, targetResolution: image.thumbnail.size.toString() }); - const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path); - await this.assetRepository.update({ id: asset.id, thumbhash }); + const previewOptions = previewConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream); + const thumbnailOptions = thumbnailConfig.getCommand(TranscodeTarget.VIDEO, mainVideoStream, mainAudioStream); + await this.mediaRepository.transcode(asset.originalPath, previewPath, previewOptions); + await this.mediaRepository.transcode(asset.originalPath, thumbnailPath, thumbnailOptions); - return JobStatus.SUCCESS; + const thumbhash = await this.mediaRepository.generateThumbhash(previewPath, { + colorspace: image.colorspace, + processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', + }); + + return { previewPath, thumbnailPath, thumbhash }; } async handleQueueVideoConversion(job: IBaseJob): Promise { @@ -344,7 +325,9 @@ export class MediaService { const output = StorageCore.getEncodedVideoPath(asset); this.storageCore.ensureFolders(output); - const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input); + const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input, { + countFrames: this.logger.isLevelEnabled(LogLevel.DEBUG), // makes frame count more reliable for progress logs + }); const mainVideoStream = this.getMainStream(videoStreams); const mainAudioStream = this.getMainStream(audioStreams); if (!mainVideoStream || !format.formatName) { @@ -363,12 +346,14 @@ export class MediaService { this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [asset.encodedVideoPath] } }); await this.assetRepository.update({ id: asset.id, encodedVideoPath: null }); + } else { + this.logger.verbose(`Asset ${asset.id} does not require transcoding based on current policy, skipping`); } return JobStatus.SKIPPED; } - let command; + let command: TranscodeCommand; try { const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL()); command = config.getCommand(target, mainVideoStream, mainAudioStream); @@ -377,16 +362,20 @@ export class MediaService { return JobStatus.FAILED; } - this.logger.log(`Started encoding video ${asset.id} ${JSON.stringify(command)}`); + if (ffmpeg.accel === TranscodeHWAccel.DISABLED) { + this.logger.log(`Encoding video ${asset.id} without hardware acceleration`); + } else { + this.logger.log(`Encoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()} acceleration`); + } + try { await this.mediaRepository.transcode(input, output, command); - } catch (error) { - this.logger.error(error); - if (ffmpeg.accel !== TranscodeHWAccel.DISABLED) { - this.logger.error( - `Error occurred during transcoding. Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled.`, - ); + } catch (error: any) { + this.logger.error(`Error occurred during transcoding: ${error.message}`); + if (ffmpeg.accel === TranscodeHWAccel.DISABLED) { + return JobStatus.FAILED; } + this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`); const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED }); command = config.getCommand(target, mainVideoStream, mainAudioStream); await this.mediaRepository.transcode(input, output, command); @@ -553,7 +542,7 @@ export class MediaService { const maliDeviceStat = await this.storageRepository.stat('/dev/mali0'); this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice(); } catch { - this.logger.debug('OpenCL not available for transcoding, using CPU decoding instead.'); + this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU decoding'); this.maliOpenCL = false; } } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index ea7254f53f4d8..c74883c283bb2 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -316,7 +316,7 @@ describe(MetadataService.name, () => { it('should handle lists of numbers', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ ISO: [160] as any }); + metadataMock.readTags.mockResolvedValue({ ISO: [160] }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); @@ -411,7 +411,7 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a list with a number', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] }); + metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] }); tagMock.upsertValue.mockResolvedValue(tagStub.parent); await sut.handleMetadataExtraction({ id: assetStub.image.id }); @@ -467,6 +467,17 @@ describe(MetadataService.name, () => { expect(tagMock.upsertValue).toHaveBeenNthCalledWith(3, { userId: 'user-id', value: 'TagA', parent: undefined }); }); + it('should extract tags from HierarchicalSubject as a list with a number', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent', 2024] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined }); + }); + it('should extract ignore / characters in a HierarchicalSubject tag', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] }); @@ -511,7 +522,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.livePhotoMotionAsset.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.livePhotoMotionAsset.id]); - expect(storageMock.writeFile).not.toHaveBeenCalled(); + expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled(); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith( @@ -581,7 +592,7 @@ describe(MetadataService.name, () => { type: AssetType.VIDEO, }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, @@ -624,7 +635,7 @@ describe(MetadataService.name, () => { type: AssetType.VIDEO, }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, @@ -668,7 +679,7 @@ describe(MetadataService.name, () => { type: AssetType.VIDEO, }); expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.ownerId, 512); - expect(storageMock.writeFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); + expect(storageMock.createFile).toHaveBeenCalledWith(assetStub.livePhotoMotionAsset.originalPath, video); expect(assetMock.update).toHaveBeenNthCalledWith(1, { id: assetStub.livePhotoWithOriginalFileName.id, livePhotoVideoId: fileStub.livePhotoMotion.uuid, @@ -716,7 +727,7 @@ describe(MetadataService.name, () => { await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id }); expect(assetMock.create).toHaveBeenCalledTimes(0); - expect(storageMock.writeFile).toHaveBeenCalledTimes(0); + expect(storageMock.createOrOverwriteFile).toHaveBeenCalledTimes(0); // The still asset gets saved by handleMetadataExtraction, but not the video expect(assetMock.update).toHaveBeenCalledTimes(1); expect(jobMock.queue).toHaveBeenCalledTimes(0); @@ -1095,6 +1106,42 @@ describe(MetadataService.name, () => { expect(personMock.updateAll).toHaveBeenCalledWith([]); expect(jobMock.queueAll).toHaveBeenCalledWith([]); }); + + it('should handle invalid modify date', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ ModifyDate: '00:00:00.000' }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + modifyDate: expect.any(Date), + }), + ); + }); + + it('should handle invalid rating value', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Rating: 6 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + rating: null, + }), + ); + }); + + it('should handle valid rating value', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Rating: 5 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + rating: 5, + }), + ); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 4ffbd7f09b4f6..224ef03b3b019 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -11,6 +11,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { ExifEntity } from 'src/entities/exif.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { AssetType, SourceType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; @@ -83,6 +84,18 @@ const validate = (value: T): NonNullable | null => { return value ?? null; }; +const validateRange = (value: number | undefined, min: number, max: number): NonNullable | null => { + // reutilizes the validate function + const val = validate(value); + + // check if the value is within the range + if (val == null || val < min || val > max) { + return null; + } + + return val; +}; + @Injectable() export class MetadataService { private storageCore: StorageCore; @@ -224,7 +237,7 @@ export class MetadataService { const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags); const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding); - const exifData = { + const exifData: Partial = { assetId: asset.id, // dates @@ -252,7 +265,7 @@ export class MetadataService { make: exifTags.Make ?? null, model: exifTags.Model ?? null, fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), - iso: validate(exifTags.ISO), + iso: validate(exifTags.ISO) as number, exposureTime: exifTags.ExposureTime ?? null, lensModel: exifTags.LensModel ?? null, fNumber: validate(exifTags.FNumber), @@ -261,7 +274,7 @@ export class MetadataService { // comments description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), profileDescription: exifTags.ProfileDescription || null, - rating: exifTags.Rating ?? null, + rating: validateRange(exifTags.Rating, 0, 5), // grouping livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, @@ -383,13 +396,13 @@ export class MetadataService { } private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { - const tags: Array = []; + const tags: string[] = []; if (exifTags.TagsList) { - tags.push(...exifTags.TagsList); + tags.push(...exifTags.TagsList.map(String)); } else if (exifTags.HierarchicalSubject) { tags.push( ...exifTags.HierarchicalSubject.map((tag) => - tag + String(tag) // convert | to / .replaceAll('/', '') .replaceAll('|', '/') @@ -401,10 +414,10 @@ export class MetadataService { if (!Array.isArray(keywords)) { keywords = [keywords]; } - tags.push(...keywords); + tags.push(...keywords.map(String)); } - const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) }); + const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) }); } @@ -529,7 +542,7 @@ export class MetadataService { const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath); if (!existsOnDisk) { this.storageCore.ensureFolders(motionAsset.originalPath); - await this.storageRepository.writeFile(motionAsset.originalPath, video); + await this.storageRepository.createFile(motionAsset.originalPath, video); this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: motionAsset.id } }); } @@ -629,11 +642,16 @@ export class MetadataService { this.logger.debug(`Asset ${asset.id} local time is offset by ${offsetMinutes} minutes`); } + let modifyDate = asset.fileModifiedAt; + try { + modifyDate = (exifTags.ModifyDate as ExifDateTime)?.toDate() ?? modifyDate; + } catch {} + return { dateTimeOriginal, timeZone, localDateTime, - modifyDate: (exifTags.ModifyDate as ExifDateTime)?.toDate() ?? asset.fileModifiedAt, + modifyDate, }; } diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index 025400cc9bde3..0afefefff3402 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -15,6 +15,8 @@ import { SessionService } from 'src/services/session.service'; import { SmartInfoService } from 'src/services/smart-info.service'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; +import { TagService } from 'src/services/tag.service'; +import { TrashService } from 'src/services/trash.service'; import { UserService } from 'src/services/user.service'; import { VersionService } from 'src/services/version.service'; import { otelShutdown } from 'src/utils/instrumentation'; @@ -34,6 +36,8 @@ export class MicroservicesService { private sessionService: SessionService, private storageTemplateService: StorageTemplateService, private storageService: StorageService, + private tagService: TagService, + private trashService: TrashService, private userService: UserService, private duplicateService: DuplicateService, private versionService: VersionService, @@ -64,9 +68,7 @@ export class MicroservicesService { [JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data), [JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data), [JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data), - [JobName.GENERATE_PREVIEW]: (data) => this.mediaService.handleGeneratePreview(data), - [JobName.GENERATE_THUMBNAIL]: (data) => this.mediaService.handleGenerateThumbnail(data), - [JobName.GENERATE_THUMBHASH]: (data) => this.mediaService.handleGenerateThumbhash(data), + [JobName.GENERATE_THUMBNAILS]: (data) => this.mediaService.handleGenerateThumbnails(data), [JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data), [JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data), [JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data), @@ -82,18 +84,20 @@ export class MicroservicesService { [JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data), [JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data), [JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data), - [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data), - [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data), + [JobName.LIBRARY_QUEUE_SYNC_ALL]: () => this.libraryService.handleQueueSyncAll(), + [JobName.LIBRARY_QUEUE_SYNC_FILES]: (data) => this.libraryService.handleQueueSyncFiles(data), //Queues all files paths on disk + [JobName.LIBRARY_SYNC_FILE]: (data) => this.libraryService.handleSyncFile(data), //Handles a single path on disk //Watcher calls for new files + [JobName.LIBRARY_QUEUE_SYNC_ASSETS]: (data) => this.libraryService.handleQueueSyncAssets(data), //Queues all library assets + [JobName.LIBRARY_SYNC_ASSET]: (data) => this.libraryService.handleSyncAsset(data), //Handles all library assets // Watcher calls for unlink and changed [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data), - [JobName.LIBRARY_CHECK_OFFLINE]: (data) => this.libraryService.handleOfflineCheck(data), - [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleRemoveOffline(data), - [JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data), [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(), [JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data), [JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data), [JobName.NOTIFY_ALBUM_UPDATE]: (data) => this.notificationService.handleAlbumUpdate(data), [JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data), + [JobName.TAG_CLEANUP]: () => this.tagService.handleTagCleanup(), [JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(), + [JobName.QUEUE_TRASH_EMPTY]: () => this.trashService.handleQueueEmptyTrash(), }); } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 9d9f8f5fcfe2f..b3a1e73541527 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -144,6 +144,23 @@ describe(NotificationService.name, () => { }); }); + describe('onAssetHide', () => { + it('should send connected clients an event', () => { + sut.onAssetHide({ assetId: 'asset-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_hidden', 'user-id', 'asset-id'); + }); + }); + + describe('onAssetShow', () => { + it('should queue the generate thumbnail job', async () => { + await sut.onAssetShow({ assetId: 'asset-id', userId: 'user-id' }); + expect(jobMock.queue).toHaveBeenCalledWith({ + name: JobName.GENERATE_THUMBNAILS, + data: { id: 'asset-id', notify: true }, + }); + }); + }); + describe('onUserSignupEvent', () => { it('skips when notify is false', async () => { await sut.onUserSignup({ id: '', notify: false }); @@ -179,6 +196,62 @@ describe(NotificationService.name, () => { }); }); + describe('onAssetTrash', () => { + it('should send connected clients an event', () => { + sut.onAssetTrash({ assetId: 'asset-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']); + }); + }); + + describe('onAssetDelete', () => { + it('should send connected clients an event', () => { + sut.onAssetDelete({ assetId: 'asset-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_delete', 'user-id', 'asset-id'); + }); + }); + + describe('onAssetsTrash', () => { + it('should send connected clients an event', () => { + sut.onAssetsTrash({ assetIds: ['asset-id'], userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_trash', 'user-id', ['asset-id']); + }); + }); + + describe('onAssetsRestore', () => { + it('should send connected clients an event', () => { + sut.onAssetsRestore({ assetIds: ['asset-id'], userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_restore', 'user-id', ['asset-id']); + }); + }); + + describe('onStackCreate', () => { + it('should send connected clients an event', () => { + sut.onStackCreate({ stackId: 'stack-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + + describe('onStackUpdate', () => { + it('should send connected clients an event', () => { + sut.onStackUpdate({ stackId: 'stack-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + + describe('onStackDelete', () => { + it('should send connected clients an event', () => { + sut.onStackDelete({ stackId: 'stack-id', userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + + describe('onStacksDelete', () => { + it('should send connected clients an event', () => { + sut.onStacksDelete({ stackIds: ['stack-id'], userId: 'user-id' }); + expect(eventMock.clientSend).toHaveBeenCalledWith('on_asset_stack_update', 'user-id', []); + }); + }); + describe('sendTestEmail', () => { it('should throw error if user could not be found', async () => { await expect(sut.sendTestEmail('', configs.smtpTransport.notifications.smtp)).rejects.toThrow('User not found'); @@ -543,11 +616,6 @@ describe(NotificationService.name, () => { await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.SKIPPED); }); - it('should fail if email could not be sent', async () => { - systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true } } }); - await expect(sut.handleSendEmail({ html: '', subject: '', text: '', to: '' })).resolves.toBe(JobStatus.FAILED); - }); - it('should send mail successfully', async () => { systemMock.get.mockResolvedValue({ notifications: { smtp: { enabled: true, from: 'test@immich.app' } } }); notificationMock.sendEmail.mockResolvedValue({ messageId: '', response: '' }); diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index b1c862dc1225c..fdb8257ffad89 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; @@ -60,10 +60,54 @@ export class NotificationService { @OnEmit({ event: 'asset.hide' }) onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) { - // Notify clients to hide the linked live photo asset this.eventRepository.clientSend(ClientEvent.ASSET_HIDDEN, userId, assetId); } + @OnEmit({ event: 'asset.show' }) + async onAssetShow({ assetId }: ArgOf<'asset.show'>) { + await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id: assetId, notify: true } }); + } + + @OnEmit({ event: 'asset.trash' }) + onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, [assetId]); + } + + @OnEmit({ event: 'asset.delete' }) + onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_DELETE, userId, assetId); + } + + @OnEmit({ event: 'assets.trash' }) + onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, userId, assetIds); + } + + @OnEmit({ event: 'assets.restore' }) + onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, userId, assetIds); + } + + @OnEmit({ event: 'stack.create' }) + onStackCreate({ userId }: ArgOf<'stack.create'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + + @OnEmit({ event: 'stack.update' }) + onStackUpdate({ userId }: ArgOf<'stack.update'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + + @OnEmit({ event: 'stack.delete' }) + onStackDelete({ userId }: ArgOf<'stack.delete'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + + @OnEmit({ event: 'stacks.delete' }) + onStacksDelete({ userId }: ArgOf<'stacks.delete'>) { + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, userId, []); + } + @OnEmit({ event: 'user.signup' }) async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) { if (notify) { @@ -96,7 +140,7 @@ export class NotificationService { try { await this.notificationRepository.verifySmtp(dto.transport); } catch (error) { - throw new HttpException('Failed to verify SMTP configuration', HttpStatus.BAD_REQUEST, { cause: error }); + throw new BadRequestException('Failed to verify SMTP configuration', { cause: error }); } const { server } = await this.configCore.getConfig({ withCache: false }); @@ -108,7 +152,7 @@ export class NotificationService { }, }); - await this.notificationRepository.sendEmail({ + const { messageId } = await this.notificationRepository.sendEmail({ to: user.email, subject: 'Test email from Immich', html, @@ -117,6 +161,8 @@ export class NotificationService { replyTo: dto.replyTo || dto.from, smtp: dto.transport, }); + + return { messageId }; } async handleUserSignup({ id, tempPassword }: INotifySignupJob) { @@ -268,10 +314,6 @@ export class NotificationService { imageAttachments: data.imageAttachments, }); - if (!response) { - return JobStatus.FAILED; - } - this.logger.log(`Sent mail with id: ${response.messageId} status: ${response.response}`); return JobStatus.SUCCESS; diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 2b111706f1ea6..5214808de0344 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -1,9 +1,8 @@ import { BadRequestException, NotFoundException } from '@nestjs/common'; -import { Colorspace } from 'src/config'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { SourceType, SystemMetadataKey } from 'src/enum'; +import { CacheControl, Colorspace, ImageFormat, SourceType, SystemMetadataKey } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -16,7 +15,7 @@ import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.inter import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { PersonService } from 'src/services/person.service'; -import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichFileResponse } from 'src/utils/file'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; @@ -661,7 +660,7 @@ describe(PersonService.name, () => { expect(systemMock.set).not.toHaveBeenCalled(); }); - it('should delete existing people and faces if forced', async () => { + it('should delete existing people if forced', async () => { jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 }); personMock.getAll.mockResolvedValue({ items: [faceStub.face1.person, personStub.randomPerson], @@ -676,7 +675,8 @@ describe(PersonService.name, () => { await sut.handleQueueRecognizeFaces({ force: true }); - expect(personMock.deleteAllFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); + expect(personMock.deleteFaces).not.toHaveBeenCalled(); + expect(personMock.unassignFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING }); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.FACIAL_RECOGNITION, @@ -962,12 +962,11 @@ describe(PersonService.name, () => { expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( assetStub.primaryImage.originalPath, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', { - format: 'jpeg', + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, size: 250, quality: 80, - colorspace: Colorspace.P3, crop: { left: 238, top: 163, @@ -976,6 +975,7 @@ describe(PersonService.name, () => { }, processInvalidImages: false, }, + 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', ); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', @@ -991,13 +991,12 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.image.originalPath, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', + assetStub.primaryImage.originalPath, { - format: 'jpeg', + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, size: 250, quality: 80, - colorspace: Colorspace.P3, crop: { left: 0, top: 85, @@ -1006,6 +1005,7 @@ describe(PersonService.name, () => { }, processInvalidImages: false, }, + 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', ); }); @@ -1018,12 +1018,11 @@ describe(PersonService.name, () => { expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( assetStub.primaryImage.originalPath, - 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', { - format: 'jpeg', + colorspace: Colorspace.P3, + format: ImageFormat.JPEG, size: 250, quality: 80, - colorspace: Colorspace.P3, crop: { left: 591, top: 591, @@ -1032,33 +1031,7 @@ describe(PersonService.name, () => { }, processInvalidImages: false, }, - ); - }); - - it('should use preview path for videos', async () => { - personMock.getById.mockResolvedValue({ ...personStub.primaryPerson, faceAssetId: faceStub.end.assetId }); - personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.end); - assetMock.getById.mockResolvedValue(assetStub.video); - mediaMock.getImageDimensions.mockResolvedValue({ width: 2560, height: 1440 }); - - await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); - - expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - '/uploads/user-id/thumbs/path.jpg', 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', - { - format: 'jpeg', - size: 250, - quality: 80, - colorspace: Colorspace.P3, - crop: { - left: 1741, - top: 851, - width: 588, - height: 588, - }, - processInvalidImages: false, - }, ); }); }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index dd4a4cecf2b56..b009696b637fe 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; -import { ImageFormat } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; @@ -23,9 +22,16 @@ import { } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum'; +import { + AssetType, + CacheControl, + ImageFormat, + Permission, + PersonPathType, + SourceType, + SystemMetadataKey, +} from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -51,7 +57,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { checkAccess, requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; -import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -270,16 +276,6 @@ export class PersonService { this.logger.debug(`Deleted ${people.length} people`); } - private async deleteAllPeople() { - const personPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.repository.getAll({ ...pagination, skip: 0 }), - ); - - for await (const people of personPagination) { - await this.delete(people); // deletes thumbnails too - } - } - async handlePersonCleanup(): Promise { const people = await this.repository.getAllWithoutFaces(); await this.delete(people); @@ -293,7 +289,7 @@ export class PersonService { } if (force) { - await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.repository.deleteFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.handlePersonCleanup(); } @@ -401,7 +397,7 @@ export class PersonService { const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION); if (force) { - await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING }); + await this.repository.unassignFaces({ sourceType: SourceType.MACHINE_LEARNING }); await this.handlePersonCleanup(); } else if (waiting) { this.logger.debug( @@ -565,15 +561,15 @@ export class PersonService { this.storageCore.ensureFolders(thumbnailPath); const thumbnailOptions = { + colorspace: image.colorspace, format: ImageFormat.JPEG, size: FACE_THUMBNAIL_SIZE, - colorspace: image.colorspace, - quality: image.quality, + quality: image.thumbnail.quality, crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }), processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true', - } as const; + }; - await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions); + await this.mediaRepository.generateThumbnail(inputPath, thumbnailOptions, thumbnailPath); await this.repository.update({ id: person.id, thumbnailPath }); return JobStatus.SUCCESS; diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index ded087b8b5dc4..c30de68e05944 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -3,7 +3,6 @@ import { SearchSuggestionType } from 'src/dtos/search.dto'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; @@ -15,7 +14,6 @@ import { personStub } from 'test/fixtures/person.stub'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newMachineLearningRepositoryMock } from 'test/repositories/machine-learning.repository.mock'; -import { newMetadataRepositoryMock } from 'test/repositories/metadata.repository.mock'; import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock'; import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; import { newSearchRepositoryMock } from 'test/repositories/search.repository.mock'; @@ -32,7 +30,6 @@ describe(SearchService.name, () => { let personMock: Mocked; let searchMock: Mocked; let partnerMock: Mocked; - let metadataMock: Mocked; let loggerMock: Mocked; beforeEach(() => { @@ -42,19 +39,9 @@ describe(SearchService.name, () => { personMock = newPersonRepositoryMock(); searchMock = newSearchRepositoryMock(); partnerMock = newPartnerRepositoryMock(); - metadataMock = newMetadataRepositoryMock(); loggerMock = newLoggerRepositoryMock(); - sut = new SearchService( - systemMock, - machineMock, - personMock, - searchMock, - assetMock, - partnerMock, - metadataMock, - loggerMock, - ); + sut = new SearchService(systemMock, machineMock, personMock, searchMock, assetMock, partnerMock, loggerMock); }); it('should work', () => { @@ -99,19 +86,19 @@ describe(SearchService.name, () => { describe('getSearchSuggestions', () => { it('should return search suggestions (including null)', async () => { - metadataMock.getCountries.mockResolvedValue(['USA', null]); + searchMock.getCountries.mockResolvedValue(['USA', null]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA', null]); - expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); + expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); }); it('should return search suggestions (without null)', async () => { - metadataMock.getCountries.mockResolvedValue(['USA', null]); + searchMock.getCountries.mockResolvedValue(['USA', null]); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA']); - expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); + expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); }); }); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 4c86d4ad75ebe..c3cc5399c8d73 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -6,6 +6,7 @@ import { PersonResponseDto } from 'src/dtos/person.dto'; import { MetadataSearchDto, PlacesResponseDto, + RandomSearchDto, SearchPeopleDto, SearchPlacesDto, SearchResponseDto, @@ -19,7 +20,6 @@ import { AssetOrder } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; -import { IMetadataRepository } from 'src/interfaces/metadata.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { ISearchRepository, SearchExploreItem } from 'src/interfaces/search.interface'; @@ -38,7 +38,6 @@ export class SearchService { @Inject(ISearchRepository) private searchRepository: ISearchRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - @Inject(IMetadataRepository) private metadataRepository: IMetadataRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(SearchService.name); @@ -95,6 +94,12 @@ export class SearchService { return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); } + async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { + const userIds = await this.getUserIdsToSearch(auth); + const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds }); + return items.map((item) => mapAsset(item, { auth })); + } + async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise { const { machineLearning } = await this.configCore.getConfig({ withCache: false }); if (!isSmartSearchEnabled(machineLearning)) { @@ -129,19 +134,19 @@ export class SearchService { private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) { switch (dto.type) { case SearchSuggestionType.COUNTRY: { - return this.metadataRepository.getCountries(userIds); + return this.searchRepository.getCountries(userIds); } case SearchSuggestionType.STATE: { - return this.metadataRepository.getStates(userIds, dto.country); + return this.searchRepository.getStates(userIds, dto.country); } case SearchSuggestionType.CITY: { - return this.metadataRepository.getCities(userIds, dto.country, dto.state); + return this.searchRepository.getCities(userIds, dto.country, dto.state); } case SearchSuggestionType.CAMERA_MAKE: { - return this.metadataRepository.getCameraMakes(userIds, dto.model); + return this.searchRepository.getCameraMakes(userIds, dto.model); } case SearchSuggestionType.CAMERA_MODEL: { - return this.metadataRepository.getCameraModels(userIds, dto.make); + return this.searchRepository.getCameraModels(userIds, dto.make); } default: { return []; diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index ac899f7b13ba8..4e6a8972b008c 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -186,6 +186,8 @@ describe(ServerService.name, () => { isInitialized: undefined, isOnboarded: false, externalDomain: '', + mapDarkStyleUrl: 'https://tiles.immich.cloud/v1/style/dark.json', + mapLightStyleUrl: 'https://tiles.immich.cloud/v1/style/light.json', }); expect(systemMock.get).toHaveBeenCalled(); }); diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index e57a206765f96..a192c2f308ba0 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; import { serverVersion } from 'src/constants'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; @@ -15,7 +15,7 @@ import { ServerStorageResponseDto, UsageByUserDto, } from 'src/dtos/server.dto'; -import { SystemMetadataKey } from 'src/enum'; +import { StorageFolder, SystemMetadataKey } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -129,6 +129,8 @@ export class ServerService { isInitialized, isOnboarded: onboarding?.isOnboarded || false, externalDomain: config.server.externalDomain, + mapDarkStyleUrl: config.map.darkStyle, + mapLightStyleUrl: config.map.lightStyle, }; } diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index bebc8517d6b7a..29a598d4b413a 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -4,7 +4,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { requireAccess } from 'src/utils/access'; @@ -30,7 +30,7 @@ export class StackService { const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stack.create', { stackId: stack.id, userId: auth.user.id }); return mapStack(stack, { auth }); } @@ -50,7 +50,7 @@ export class StackService { const updatedStack = await this.stackRepository.update({ id, primaryAssetId: dto.primaryAssetId }); - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stack.update', { stackId: id, userId: auth.user.id }); return mapStack(updatedStack, { auth }); } @@ -58,15 +58,13 @@ export class StackService { async delete(auth: AuthDto, id: string): Promise { await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] }); await this.stackRepository.delete(id); - - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id }); } async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); await this.stackRepository.deleteAll(dto.ids); - - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id }); } private async findOrFail(id: string) { diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 093cc5b2ff1d6..e8e222c7b2491 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -2,7 +2,7 @@ import { Stats } from 'node:fs'; import { SystemConfig, defaults } from 'src/config'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetPathType } from 'src/entities/move.entity'; +import { AssetPathType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 9836ad40ace47..30d0eb575f1de 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -13,12 +13,11 @@ import { supportedWeekTokens, supportedYearTokens, } from 'src/constants'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AssetPathType } from 'src/entities/move.entity'; -import { AssetType } from 'src/enum'; +import { AssetPathType, AssetType, StorageFolder } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index b0f38554cb032..930fb3c726e2f 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -41,6 +41,11 @@ describe(StorageService.name, () => { expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library'); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/profile'); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs'); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/encoded-video/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/library/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/profile/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/thumbs/.immich', expect.any(Buffer)); + expect(storageMock.createFile).toHaveBeenCalledWith('upload/upload/.immich', expect.any(Buffer)); }); it('should throw an error if .immich is missing', async () => { @@ -49,13 +54,13 @@ describe(StorageService.name, () => { await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount'); - expect(storageMock.writeFile).not.toHaveBeenCalled(); + expect(storageMock.createOrOverwriteFile).not.toHaveBeenCalled(); expect(systemMock.set).not.toHaveBeenCalled(); }); it('should throw an error if .immich is present but read-only', async () => { systemMock.get.mockResolvedValue({ mountFiles: true }); - storageMock.writeFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); + storageMock.overwriteFile.mockRejectedValue(new Error("ENOENT: no such file or directory, open '/app/.immich'")); await expect(sut.onBootstrap()).rejects.toThrow('Failed to validate folder mount'); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index a8f6a76e747e1..6d15f097d3956 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { join } from 'node:path'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { OnEmit } from 'src/decorators'; -import { SystemMetadataKey } from 'src/enum'; +import { StorageFolder, SystemMetadataKey } from 'src/enum'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -25,14 +25,15 @@ export class StorageService { async onBootstrap() { await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => { const flags = (await this.systemMetadata.get(SystemMetadataKey.SYSTEM_FLAGS)) || { mountFiles: false }; + const enabled = flags.mountFiles ?? false; - this.logger.log('Verifying system mount folder checks'); + this.logger.log(`Verifying system mount folder checks (enabled=${enabled})`); // check each folder exists and is writable for (const folder of Object.values(StorageFolder)) { - if (!flags.mountFiles) { + if (!enabled) { this.logger.log(`Writing initial mount file for the ${folder} folder`); - await this.verifyWriteAccess(folder); + await this.createMountFile(folder); } await this.verifyReadAccess(folder); @@ -81,17 +82,30 @@ export class StorageService { } } - private async verifyWriteAccess(folder: StorageFolder) { + private async createMountFile(folder: StorageFolder) { const { folderPath, filePath } = this.getMountFilePaths(folder); try { this.storageRepository.mkdirSync(folderPath); - await this.storageRepository.writeFile(filePath, Buffer.from(`${Date.now()}`)); + await this.storageRepository.createFile(filePath, Buffer.from(`${Date.now()}`)); + } catch (error) { + this.logger.error(`Failed to create ${filePath}: ${error}`); + this.logger.error( + `The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`, + ); + throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); + } + } + + private async verifyWriteAccess(folder: StorageFolder) { + const { filePath } = this.getMountFilePaths(folder); + try { + await this.storageRepository.overwriteFile(filePath, Buffer.from(`${Date.now()}`)); } catch (error) { this.logger.error(`Failed to write ${filePath}: ${error}`); this.logger.error( `The "${folder}" folder cannot be written to, please make sure the volume is mounted with the correct permissions`, ); - throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); + throw new ImmichStartupError(`Failed to validate folder mount (write to "/${folder}")`); } } diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index 409cd6a52f360..514d8aa0f8d58 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -1,19 +1,18 @@ import { BadRequestException } from '@nestjs/common'; +import { defaults, SystemConfig } from 'src/config'; import { AudioCodec, - CQMode, Colorspace, + CQMode, ImageFormat, LogLevel, - SystemConfig, + SystemMetadataKey, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec, VideoContainer, - defaults, -} from 'src/config'; -import { SystemMetadataKey } from 'src/enum'; +} from 'src/enum'; import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { QueueName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -100,8 +99,8 @@ const updatedConfig = Object.freeze({ }, map: { enabled: true, - lightStyle: '', - darkStyle: '', + lightStyle: 'https://tiles.immich.cloud/v1/style/light.json', + darkStyle: 'https://tiles.immich.cloud/v1/style/dark.json', }, reverseGeocoding: { enabled: true, @@ -136,11 +135,16 @@ const updatedConfig = Object.freeze({ template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}', }, image: { - thumbnailFormat: ImageFormat.WEBP, - thumbnailSize: 250, - previewFormat: ImageFormat.JPEG, - previewSize: 1440, - quality: 80, + thumbnail: { + size: 250, + format: ImageFormat.WEBP, + quality: 80, + }, + preview: { + size: 1440, + format: ImageFormat.JPEG, + quality: 80, + }, colorspace: Colorspace.P3, extractEmbedded: false, }, @@ -289,6 +293,23 @@ describe(SystemConfigService.name, () => { expect(config.machineLearning.url).toEqual('immich_machine_learning'); }); + const externalDomainTests = [ + { should: 'with a trailing slash', externalDomain: 'https://demo.immich.app/' }, + { should: 'without a trailing slash', externalDomain: 'https://demo.immich.app' }, + { should: 'with a port', externalDomain: 'https://demo.immich.app:42', result: 'https://demo.immich.app:42' }, + ]; + + for (const { should, externalDomain, result } of externalDomainTests) { + it(`should normalize an external domain ${should}`, async () => { + process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; + const partialConfig = { server: { externalDomain } }; + systemMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); + + const config = await sut.getConfig(); + expect(config.server.externalDomain).toEqual(result ?? 'https://demo.immich.app'); + }); + } + it('should warn for unknown options in yaml', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.yaml'; const partialConfig = ` diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 5ec9ab7a5db05..8a7f9123e00c2 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; -import { LogLevel, SystemConfig, defaults } from 'src/config'; +import { SystemConfig, defaults } from 'src/config'; import { supportedDayTokens, supportedHourTokens, @@ -15,6 +15,7 @@ import { import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit, OnServerEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; +import { LogLevel } from 'src/enum'; import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index de270777b06c5..a479a09fbb48e 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -140,6 +140,23 @@ describe(TagService.name, () => { parent: expect.objectContaining({ id: 'tag-parent' }), }); }); + + it('should upsert a tag and ignore leading and trailing slashes', async () => { + tagMock.getByValue.mockResolvedValueOnce(null); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); + await expect(sut.upsert(authStub.admin, { tags: ['/Parent/Child/'] })).resolves.toBeDefined(); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { + value: 'Parent', + userId: 'admin_id', + parent: undefined, + }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + value: 'Parent/Child', + userId: 'admin_id', + parent: expect.objectContaining({ id: 'tag-parent' }), + }); + }); }); describe('remove', () => { diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index 97b0ef1be6843..cc6d64f749d20 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -14,6 +14,7 @@ import { TagEntity } from 'src/entities/tag.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; +import { JobStatus } from 'src/interfaces/job.interface'; import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @@ -138,6 +139,11 @@ export class TagService { return results; } + async handleTagCleanup() { + await this.repository.deleteEmptyTags(); + return JobStatus.SUCCESS; + } + private async findOrFail(id: string) { const tag = await this.repository.get(id); if (!tag) { diff --git a/server/src/services/trash.service.spec.ts b/server/src/services/trash.service.spec.ts index 73a4f3d57b95f..d0c719ae48e73 100644 --- a/server/src/services/trash.service.spec.ts +++ b/server/src/services/trash.service.spec.ts @@ -1,22 +1,24 @@ import { BadRequestException } from '@nestjs/common'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; -import { IJobRepository, JobName } from 'src/interfaces/job.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ITrashRepository } from 'src/interfaces/trash.interface'; import { TrashService } from 'src/services/trash.service'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newJobRepositoryMock } from 'test/repositories/job.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; +import { newTrashRepositoryMock } from 'test/repositories/trash.repository.mock'; import { Mocked } from 'vitest'; describe(TrashService.name, () => { let sut: TrashService; let accessMock: IAccessRepositoryMock; - let assetMock: Mocked; - let jobMock: Mocked; let eventMock: Mocked; + let jobMock: Mocked; + let trashMock: Mocked; + let loggerMock: Mocked; it('should work', () => { expect(sut).toBeDefined(); @@ -24,11 +26,12 @@ describe(TrashService.name, () => { beforeEach(() => { accessMock = newAccessRepositoryMock(); - assetMock = newAssetRepositoryMock(); eventMock = newEventRepositoryMock(); jobMock = newJobRepositoryMock(); + trashMock = newTrashRepositoryMock(); + loggerMock = newLoggerRepositoryMock(); - sut = new TrashService(accessMock, assetMock, jobMock, eventMock); + sut = new TrashService(accessMock, eventMock, jobMock, trashMock, loggerMock); }); describe('restoreAssets', () => { @@ -40,46 +43,70 @@ describe(TrashService.name, () => { ).rejects.toBeInstanceOf(BadRequestException); }); + it('should handle an empty list', async () => { + await expect(sut.restoreAssets(authStub.user1, { ids: [] })).resolves.toEqual({ count: 0 }); + expect(accessMock.asset.checkOwnerAccess).not.toHaveBeenCalled(); + }); + it('should restore a batch of assets', async () => { accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset1', 'asset2'])); await sut.restoreAssets(authStub.user1, { ids: ['asset1', 'asset2'] }); - expect(assetMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']); + expect(trashMock.restoreAll).toHaveBeenCalledWith(['asset1', 'asset2']); expect(jobMock.queue.mock.calls).toEqual([]); }); }); describe('restore', () => { it('should handle an empty trash', async () => { - assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); - await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); - expect(assetMock.restoreAll).not.toHaveBeenCalled(); - expect(eventMock.clientSend).not.toHaveBeenCalled(); + trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false }); + trashMock.restore.mockResolvedValue(0); + await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 0 }); + expect(trashMock.restore).toHaveBeenCalledWith('user-id'); }); - it('should restore and notify', async () => { - assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); - await expect(sut.restore(authStub.user1)).resolves.toBeUndefined(); - expect(assetMock.restoreAll).toHaveBeenCalledWith([assetStub.image.id]); - expect(eventMock.clientSend).toHaveBeenCalledWith(ClientEvent.ASSET_RESTORE, authStub.user1.user.id, [ - assetStub.image.id, - ]); + it('should restore', async () => { + trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); + trashMock.restore.mockResolvedValue(1); + await expect(sut.restore(authStub.user1)).resolves.toEqual({ count: 1 }); + expect(trashMock.restore).toHaveBeenCalledWith('user-id'); }); }); describe('empty', () => { it('should handle an empty trash', async () => { - assetMock.getByUserId.mockResolvedValue({ items: [], hasNextPage: false }); - await expect(sut.empty(authStub.user1)).resolves.toBeUndefined(); - expect(jobMock.queueAll).toHaveBeenCalledWith([]); + trashMock.getDeletedIds.mockResolvedValue({ items: [], hasNextPage: false }); + trashMock.empty.mockResolvedValue(0); + await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 0 }); + expect(jobMock.queue).not.toHaveBeenCalled(); }); it('should empty the trash', async () => { - assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image], hasNextPage: false }); - await expect(sut.empty(authStub.user1)).resolves.toBeUndefined(); + trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); + trashMock.empty.mockResolvedValue(1); + await expect(sut.empty(authStub.user1)).resolves.toEqual({ count: 1 }); + expect(trashMock.empty).toHaveBeenCalledWith('user-id'); + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + }); + }); + + describe('onAssetsDelete', () => { + it('should queue the empty trash job', async () => { + await expect(sut.onAssetsDelete()).resolves.toBeUndefined(); + expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + }); + }); + + describe('handleQueueEmptyTrash', () => { + it('should queue asset delete jobs', async () => { + trashMock.getDeletedIds.mockResolvedValue({ items: ['asset-1'], hasNextPage: false }); + await expect(sut.handleQueueEmptyTrash()).resolves.toEqual(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ - { name: JobName.ASSET_DELETION, data: { id: assetStub.image.id, deleteOnDisk: true } }, + { + name: JobName.ASSET_DELETION, + data: { id: 'asset-1', deleteOnDisk: true }, + }, ]); }); }); diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index ac141521ddc18..88340f7d7c2c7 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,69 +1,86 @@ import { Inject } from '@nestjs/common'; -import { DateTime } from 'luxon'; +import { OnEmit } from 'src/decorators'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { TrashResponseDto } from 'src/dtos/trash.dto'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; -import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ITrashRepository } from 'src/interfaces/trash.interface'; import { requireAccess } from 'src/utils/access'; import { usePagination } from 'src/utils/pagination'; export class TrashService { constructor( @Inject(IAccessRepository) private access: IAccessRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, - @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, - ) {} + @Inject(IJobRepository) private jobRepository: IJobRepository, + @Inject(ITrashRepository) private trashRepository: ITrashRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, + ) { + this.logger.setContext(TrashService.name); + } - async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { + async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { const { ids } = dto; - await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); - await this.restoreAndSend(auth, ids); - } - - async restore(auth: AuthDto): Promise { - const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { - trashedBefore: DateTime.now().toJSDate(), - }), - ); - - for await (const assets of assetPagination) { - const ids = assets.map((a) => a.id); - await this.restoreAndSend(auth, ids); + if (ids.length === 0) { + return { count: 0 }; } + + await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); + await this.trashRepository.restoreAll(ids); + await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id }); + + this.logger.log(`Restored ${ids.length} assets from trash`); + + return { count: ids.length }; } - async empty(auth: AuthDto): Promise { + async restore(auth: AuthDto): Promise { + const count = await this.trashRepository.restore(auth.user.id); + if (count > 0) { + this.logger.log(`Restored ${count} assets from trash`); + } + return { count }; + } + + async empty(auth: AuthDto): Promise { + const count = await this.trashRepository.empty(auth.user.id); + if (count > 0) { + await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + } + return { count }; + } + + @OnEmit({ event: 'assets.delete' }) + async onAssetsDelete() { + await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} }); + } + + async handleQueueEmptyTrash() { + let count = 0; const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => - this.assetRepository.getByUserId(pagination, auth.user.id, { - trashedBefore: DateTime.now().toJSDate(), - withArchived: true, - }), + this.trashRepository.getDeletedIds(pagination), ); - for await (const assets of assetPagination) { + for await (const assetIds of assetPagination) { + this.logger.debug(`Queueing ${assetIds.length} assets for deletion from the trash`); + count += assetIds.length; await this.jobRepository.queueAll( - assets.map((asset) => ({ + assetIds.map((assetId) => ({ name: JobName.ASSET_DELETION, data: { - id: asset.id, + id: assetId, deleteOnDisk: true, }, })), ); } - } - private async restoreAndSend(auth: AuthDto, ids: string[]) { - if (ids.length === 0) { - return; - } + this.logger.log(`Queued ${count} assets for deletion from the trash`); - await this.assetRepository.restoreAll(ids); - this.eventRepository.clientSend(ClientEvent.ASSET_RESTORE, auth.user.id, ids); + return JobStatus.SUCCESS; } } diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 0ac0ea6dbc7cf..f5b564e86f18b 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { UserEntity } from 'src/entities/user.entity'; -import { UserMetadataKey } from 'src/enum'; +import { CacheControl, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; @@ -9,7 +9,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { UserService } from 'src/services/user.service'; -import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichFileResponse } from 'src/utils/file'; import { authStub } from 'test/fixtures/auth.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 92404a6958f3c..dca893aa826b4 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -2,16 +2,16 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nes import { DateTime } from 'luxon'; import { getClientLicensePublicKey, getServerLicensePublicKey } from 'src/config'; import { SALT_ROUNDS } from 'src/constants'; -import { StorageCore, StorageFolder } from 'src/cores/storage.core'; +import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; -import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; +import { CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { UserMetadataKey } from 'src/enum'; +import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -19,7 +19,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface'; -import { CacheControl, ImmichFileResponse } from 'src/utils/file'; +import { ImmichFileResponse } from 'src/utils/file'; import { getPreferences, getPreferencesPartial, mergePreferences } from 'src/utils/preferences'; @Injectable() @@ -93,13 +93,23 @@ export class UserService { return mapUser(user); } - async createProfileImage(auth: AuthDto, fileInfo: Express.Multer.File): Promise { + async createProfileImage(auth: AuthDto, file: Express.Multer.File): Promise { const { profileImagePath: oldpath } = await this.findOrFail(auth.user.id, { withDeleted: false }); - const updatedUser = await this.userRepository.update(auth.user.id, { profileImagePath: fileInfo.path }); + + const user = await this.userRepository.update(auth.user.id, { + profileImagePath: file.path, + profileChangedAt: new Date(), + }); + if (oldpath !== '') { await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldpath] } }); } - return mapCreateProfileImageResponse(updatedUser.id, updatedUser.profileImagePath); + + return { + userId: user.id, + profileImagePath: user.profileImagePath, + profileChangedAt: user.profileChangedAt, + }; } async deleteProfileImage(auth: AuthDto): Promise { @@ -107,7 +117,7 @@ export class UserService { if (user.profileImagePath === '') { throw new BadRequestException("Can't delete a missing profile Image"); } - await this.userRepository.update(auth.user.id, { profileImagePath: '' }); + await this.userRepository.update(auth.user.id, { profileImagePath: '', profileChangedAt: new Date() }); await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [user.profileImagePath] } }); } diff --git a/server/src/services/view.service.spec.ts b/server/src/services/view.service.spec.ts index 3f9aa9f2f50a3..8d17e4d897401 100644 --- a/server/src/services/view.service.spec.ts +++ b/server/src/services/view.service.spec.ts @@ -1,21 +1,20 @@ import { mapAsset } from 'src/dtos/asset-response.dto'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; - +import { IViewRepository } from 'src/interfaces/view.interface'; import { ViewService } from 'src/services/view.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; -import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newViewRepositoryMock } from 'test/repositories/view.repository.mock'; import { Mocked } from 'vitest'; describe(ViewService.name, () => { let sut: ViewService; - let assetMock: Mocked; + let viewMock: Mocked; beforeEach(() => { - assetMock = newAssetRepositoryMock(); + viewMock = newViewRepositoryMock(); - sut = new ViewService(assetMock); + sut = new ViewService(viewMock); }); it('should work', () => { @@ -25,12 +24,12 @@ describe(ViewService.name, () => { describe('getUniqueOriginalPaths', () => { it('should return unique original paths', async () => { const mockPaths = ['path1', 'path2', 'path3']; - assetMock.getUniqueOriginalPaths.mockResolvedValue(mockPaths); + viewMock.getUniqueOriginalPaths.mockResolvedValue(mockPaths); const result = await sut.getUniqueOriginalPaths(authStub.admin); expect(result).toEqual(mockPaths); - expect(assetMock.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id); + expect(viewMock.getUniqueOriginalPaths).toHaveBeenCalledWith(authStub.admin.user.id); }); }); @@ -45,11 +44,11 @@ describe(ViewService.name, () => { const mockAssetReponseDto = mockAssets.map((a) => mapAsset(a, { auth: authStub.admin })); - assetMock.getAssetsByOriginalPath.mockResolvedValue(mockAssets); + viewMock.getAssetsByOriginalPath.mockResolvedValue(mockAssets); const result = await sut.getAssetsByOriginalPath(authStub.admin, path); expect(result).toEqual(mockAssetReponseDto); - await expect(assetMock.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets); + await expect(viewMock.getAssetsByOriginalPath(authStub.admin.user.id, path)).resolves.toEqual(mockAssets); }); }); }); diff --git a/server/src/services/view.service.ts b/server/src/services/view.service.ts index 1bf9a3408c63a..d870f9fd2e1a6 100644 --- a/server/src/services/view.service.ts +++ b/server/src/services/view.service.ts @@ -1,18 +1,17 @@ import { Inject } from '@nestjs/common'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { IViewRepository } from 'src/interfaces/view.interface'; export class ViewService { - constructor(@Inject(IAssetRepository) private assetRepository: IAssetRepository) {} + constructor(@Inject(IViewRepository) private viewRepository: IViewRepository) {} getUniqueOriginalPaths(auth: AuthDto): Promise { - return this.assetRepository.getUniqueOriginalPaths(auth.user.id); + return this.viewRepository.getUniqueOriginalPaths(auth.user.id); } async getAssetsByOriginalPath(auth: AuthDto, path: string): Promise { - const assets = await this.assetRepository.getAssetsByOriginalPath(auth.user.id, path); - + const assets = await this.viewRepository.getAssetsByOriginalPath(auth.user.id, path); return assets.map((asset) => mapAsset(asset, { auth })); } } diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index f2a03a9dcb159..44c291e139766 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,4 +1,5 @@ import { BadRequestException } from '@nestjs/common'; +import { StorageCore } from 'src/cores/storage.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; @@ -134,8 +135,10 @@ export const getMyPartnerIds = async ({ userId, repository, timelineEnabled }: P return [...partnerIds]; }; +export type AssetHookRepositories = { asset: IAssetRepository; event: IEventRepository }; + export const onBeforeLink = async ( - { asset: assetRepository, event: eventRepository }: { asset: IAssetRepository; event: IEventRepository }, + { asset: assetRepository, event: eventRepository }: AssetHookRepositories, { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string }, ) => { const motionAsset = await assetRepository.getById(livePhotoVideoId); @@ -154,3 +157,27 @@ export const onBeforeLink = async ( await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId }); } }; + +export const onBeforeUnlink = async ( + { asset: assetRepository }: AssetHookRepositories, + { livePhotoVideoId }: { livePhotoVideoId: string }, +) => { + const motion = await assetRepository.getById(livePhotoVideoId); + if (!motion) { + return null; + } + + if (StorageCore.isAndroidMotionPath(motion.originalPath)) { + throw new BadRequestException('Cannot unlink Android motion photos'); + } + + return motion; +}; + +export const onAfterUnlink = async ( + { asset: assetRepository, event: eventRepository }: AssetHookRepositories, + { userId, livePhotoVideoId }: { userId: string; livePhotoVideoId: string }, +) => { + await assetRepository.update({ id: livePhotoVideoId, isVisible: true }); + await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId }); +}; diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index f3232eb78bb2e..498dd3456b932 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -80,7 +80,7 @@ export function searchAssetBuilder( }); } - const status = _.pick(options, ['isFavorite', 'isOffline', 'isVisible', 'type']); + const status = _.pick(options, ['isFavorite', 'isVisible', 'type']); const { isArchived, isEncoded, @@ -120,7 +120,7 @@ export function searchAssetBuilder( } if (withPeople) { - builder.leftJoinAndSelect(`${builder.alias}.person`, 'person'); + builder.leftJoinAndSelect('faces.person', 'person'); } if (withSmartInfo) { diff --git a/server/src/utils/events.ts b/server/src/utils/events.ts index 064c9f75071ef..f5b079dea4ef3 100644 --- a/server/src/utils/events.ts +++ b/server/src/utils/events.ts @@ -1,8 +1,8 @@ import { ModuleRef, Reflector } from '@nestjs/core'; import _ from 'lodash'; import { EmitConfig } from 'src/decorators'; +import { MetadataKey } from 'src/enum'; import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface'; -import { Metadata } from 'src/middleware/auth.guard'; import { services } from 'src/services'; type Item = { @@ -35,7 +35,7 @@ export const setupEventHandlers = (moduleRef: ModuleRef) => { continue; } - const options = reflector.get(Metadata.ON_EMIT_CONFIG, handler); + const options = reflector.get(MetadataKey.ON_EMIT_CONFIG, handler); if (!options) { continue; } diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts index 53a4d571dcfa3..3b26c3e1ba1e8 100644 --- a/server/src/utils/file.ts +++ b/server/src/utils/file.ts @@ -3,6 +3,7 @@ import { NextFunction, Response } from 'express'; import { access, constants } from 'node:fs/promises'; import { basename, extname, isAbsolute } from 'node:path'; import { promisify } from 'node:util'; +import { CacheControl } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ImmichReadStream } from 'src/interfaces/storage.interface'; import { isConnectionAborted } from 'src/utils/misc'; @@ -19,12 +20,6 @@ export function getLivePhotoMotionFilename(stillName: string, motionName: string return getFileNameWithoutExtension(stillName) + extname(motionName); } -export enum CacheControl { - PRIVATE_WITH_CACHE = 'private_with_cache', - PRIVATE_WITHOUT_CACHE = 'private_without_cache', - NONE = 'none', -} - export class ImmichFileResponse { public readonly path!: string; public readonly contentType!: string; diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 8068f4a5e6587..6f0ab4ef81d90 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -1,5 +1,5 @@ -import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/config'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; +import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/enum'; import { AudioStreamInfo, BitrateDistribution, @@ -80,6 +80,7 @@ export class BaseConfig implements VideoCodecSWConfig { inputOptions: this.getBaseInputOptions(videoStream), outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'], twoPass: this.eligibleForTwoPass(), + progress: { frameCount: videoStream.frameCount, percentInterval: 5 }, } as TranscodeCommand; if ([TranscodeTarget.ALL, TranscodeTarget.VIDEO].includes(target)) { const filters = this.getFilterOptions(videoStream); diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 47f3f552c47e7..2cef33d4f57c9 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -13,8 +13,8 @@ import path from 'node:path'; import { SystemConfig } from 'src/config'; import { CLIP_MODEL_INFO, isDev, serverVersion } from 'src/constants'; import { ImmichCookie, ImmichHeader } from 'src/dtos/auth.dto'; +import { MetadataKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { Metadata } from 'src/middleware/auth.guard'; /** * @returns a list of strings representing the keys of the object in dot notation @@ -210,7 +210,7 @@ export const useSwagger = (app: INestApplication, force = false) => { in: 'header', name: ImmichHeader.API_KEY, }, - Metadata.API_KEY_SECURITY, + MetadataKey.API_KEY_SECURITY, ) .addServer('/api') .build(); diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts index dec1a9de0c313..4009f219c1c96 100644 --- a/server/src/utils/pagination.ts +++ b/server/src/utils/pagination.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { PaginationMode } from 'src/enum'; import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm'; export interface PaginationOptions { @@ -6,11 +7,6 @@ export interface PaginationOptions { skip?: number; } -export enum PaginationMode { - LIMIT_OFFSET = 'limit-offset', - SKIP_TAKE = 'skip-take', -} - export interface PaginatedBuilderOptions { take: number; skip?: number; diff --git a/server/src/utils/tag.ts b/server/src/utils/tag.ts index 6d6c70f1d73ac..027afcf040195 100644 --- a/server/src/utils/tag.ts +++ b/server/src/utils/tag.ts @@ -8,7 +8,7 @@ export const upsertTags = async (repository: ITagRepository, { userId, tags }: U const results: TagEntity[] = []; for (const tag of tags) { - const parts = tag.split('/'); + const parts = tag.split('/').filter(Boolean); let parent: TagEntity | undefined; for (const part of parts) { diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 5ee42224ba862..119c0b6e5ab76 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -2,7 +2,7 @@ import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { StackEntity } from 'src/entities/stack.entity'; -import { AssetFileType, AssetType } from 'src/enum'; +import { AssetFileType, AssetStatus, AssetType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { libraryStub } from 'test/fixtures/library.stub'; @@ -42,6 +42,7 @@ export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity = export const assetStub = { noResizePath: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, originalFileName: 'IMG_123.jpg', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -69,13 +70,14 @@ export const assetStub = { faces: [], sidecarPath: null, deletedAt: null, - isOffline: false, isExternal: false, duplicateId: null, + isOffline: false, }), noWebpPath: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -83,7 +85,6 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - files: [previewFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, @@ -103,17 +104,18 @@ export const assetStub = { originalFileName: 'IMG_456.jpg', faces: [], sidecarPath: null, - isOffline: false, isExternal: false, exifInfo: { fileSizeInByte: 123_000, } as ExifEntity, deletedAt: null, duplicateId: null, + isOffline: false, }), noThumbhash: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -131,7 +133,6 @@ export const assetStub = { localDateTime: new Date('2023-02-23T05:06:29.716Z'), isFavorite: true, isArchived: false, - isOffline: false, duration: null, isVisible: true, isExternal: false, @@ -144,10 +145,12 @@ export const assetStub = { sidecarPath: null, deletedAt: null, duplicateId: null, + isOffline: false, }), primaryImage: Object.freeze({ id: 'primary-asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -170,7 +173,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -188,10 +190,12 @@ export const assetStub = { { id: 'stack-child-asset-2' } as AssetEntity, ]), duplicateId: null, + isOffline: false, }), image: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -214,7 +218,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -227,6 +230,7 @@ export const assetStub = { exifImageWidth: 2160, } as ExifEntity, duplicateId: null, + isOffline: false, }), trashed: Object.freeze({ @@ -254,7 +258,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -266,10 +269,52 @@ export const assetStub = { exifImageWidth: 2160, } as ExifEntity, duplicateId: null, + isOffline: false, + status: AssetStatus.TRASHED, }), + trashedOffline: Object.freeze({ + id: 'asset-id', + status: AssetStatus.ACTIVE, + deviceAssetId: 'device-asset-id', + fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), + fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), + owner: userStub.user1, + ownerId: 'user-id', + deviceId: 'device-id', + originalPath: '/original/path.jpg', + checksum: Buffer.from('file hash', 'utf8'), + type: AssetType.IMAGE, + files, + thumbhash: Buffer.from('blablabla', 'base64'), + encodedVideoPath: null, + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + deletedAt: new Date('2023-02-24T05:06:29.716Z'), + localDateTime: new Date('2023-02-23T05:06:29.716Z'), + isFavorite: false, + isArchived: false, + duration: null, + isVisible: true, + isExternal: false, + livePhotoVideo: null, + livePhotoVideoId: null, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.jpg', + faces: [], + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + exifImageHeight: 3840, + exifImageWidth: 2160, + } as ExifEntity, + duplicateId: null, + isOffline: true, + }), archived: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -292,7 +337,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -305,10 +349,12 @@ export const assetStub = { exifImageWidth: 2160, } as ExifEntity, duplicateId: null, + isOffline: false, }), external: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -331,99 +377,24 @@ export const assetStub = { isVisible: true, livePhotoVideo: null, livePhotoVideoId: null, + libraryId: 'library-id', + library: libraryStub.externalLibrary1, + tags: [], + sharedLinks: [], + originalFileName: 'asset-id.jpg', + faces: [], + deletedAt: null, + sidecarPath: null, + exifInfo: { + fileSizeInByte: 5000, + } as ExifEntity, + duplicateId: null, isOffline: false, - libraryId: 'library-id', - library: libraryStub.externalLibrary1, - tags: [], - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - deletedAt: null, - sidecarPath: null, - exifInfo: { - fileSizeInByte: 5000, - } as ExifEntity, - duplicateId: null, - }), - - offline: Object.freeze({ - id: 'asset-id', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.jpg', - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isArchived: false, - isExternal: false, - duration: null, - isVisible: true, - livePhotoVideo: null, - livePhotoVideoId: null, - isOffline: true, - tags: [], - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - sidecarPath: null, - exifInfo: { - fileSizeInByte: 5000, - } as ExifEntity, - deletedAt: null, - duplicateId: null, - }), - - externalOffline: Object.freeze({ - id: 'asset-id', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/data/user1/photo.jpg', - checksum: Buffer.from('path hash', 'utf8'), - type: AssetType.IMAGE, - files, - thumbhash: Buffer.from('blablabla', 'base64'), - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isArchived: false, - isExternal: true, - duration: null, - isVisible: true, - livePhotoVideo: null, - livePhotoVideoId: null, - isOffline: true, - libraryId: 'library-id', - library: libraryStub.externalLibrary1, - tags: [], - sharedLinks: [], - originalFileName: 'asset-id.jpg', - faces: [], - sidecarPath: null, - exifInfo: { - fileSizeInByte: 5000, - } as ExifEntity, - deletedAt: null, - duplicateId: null, }), image1: Object.freeze({ id: 'asset-id-1', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -447,7 +418,6 @@ export const assetStub = { livePhotoVideo: null, livePhotoVideoId: null, isExternal: false, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.ext', @@ -457,10 +427,12 @@ export const assetStub = { fileSizeInByte: 5000, } as ExifEntity, duplicateId: null, + isOffline: false, }), imageFrom2015: Object.freeze({ id: 'asset-id-1', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2015-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2015-02-23T05:06:29.716Z'), @@ -479,7 +451,6 @@ export const assetStub = { isFavorite: true, isArchived: false, isExternal: false, - isOffline: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -494,10 +465,12 @@ export const assetStub = { } as ExifEntity, deletedAt: null, duplicateId: null, + isOffline: false, }), video: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, originalFileName: 'asset-id.ext', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -517,7 +490,6 @@ export const assetStub = { isFavorite: true, isArchived: false, isExternal: false, - isOffline: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -533,9 +505,11 @@ export const assetStub = { } as ExifEntity, deletedAt: null, duplicateId: null, + isOffline: false, }), livePhotoMotionAsset: Object.freeze({ + status: AssetStatus.ACTIVE, id: fileStub.livePhotoMotion.uuid, originalPath: fileStub.livePhotoMotion.originalPath, ownerId: authStub.user1.user.id, @@ -551,6 +525,7 @@ export const assetStub = { liveMotionWithThumb: Object.freeze({ id: fileStub.livePhotoMotion.uuid, + status: AssetStatus.ACTIVE, originalPath: fileStub.livePhotoMotion.originalPath, ownerId: authStub.user1.user.id, type: AssetType.VIDEO, @@ -581,6 +556,7 @@ export const assetStub = { livePhotoStillAsset: Object.freeze({ id: 'live-photo-still-asset', + status: AssetStatus.ACTIVE, originalPath: fileStub.livePhotoStill.originalPath, ownerId: authStub.user1.user.id, type: AssetType.IMAGE, @@ -596,6 +572,7 @@ export const assetStub = { livePhotoStillAssetWithTheSameLivePhotoMotionAsset: Object.freeze({ id: 'live-photo-still-asset-1', + status: AssetStatus.ACTIVE, originalPath: fileStub.livePhotoStill.originalPath, ownerId: authStub.user1.user.id, type: AssetType.IMAGE, @@ -611,6 +588,7 @@ export const assetStub = { livePhotoWithOriginalFileName: Object.freeze({ id: 'live-photo-still-asset', + status: AssetStatus.ACTIVE, originalPath: fileStub.livePhotoStill.originalPath, originalFileName: fileStub.livePhotoStill.originalName, ownerId: authStub.user1.user.id, @@ -627,6 +605,7 @@ export const assetStub = { withLocation: Object.freeze({ id: 'asset-with-favorite-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-22T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-22T05:06:29.716Z'), @@ -646,7 +625,6 @@ export const assetStub = { isFavorite: false, isArchived: false, isExternal: false, - isOffline: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -665,9 +643,11 @@ export const assetStub = { } as ExifEntity, deletedAt: null, duplicateId: null, + isOffline: false, }), sidecar: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -686,7 +666,6 @@ export const assetStub = { isFavorite: true, isArchived: false, isExternal: false, - isOffline: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -698,9 +677,11 @@ export const assetStub = { sidecarPath: '/original/path.ext.xmp', deletedAt: null, duplicateId: null, + isOffline: false, }), sidecarWithoutExt: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -719,7 +700,6 @@ export const assetStub = { isFavorite: true, isArchived: false, isExternal: false, - isOffline: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -731,44 +711,12 @@ export const assetStub = { sidecarPath: '/original/path.xmp', deletedAt: null, duplicateId: null, - }), - - readOnly: Object.freeze({ - id: 'read-only-asset', - deviceAssetId: 'device-asset-id', - fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), - fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), - owner: userStub.user1, - ownerId: 'user-id', - deviceId: 'device-id', - originalPath: '/original/path.ext', - thumbhash: null, - checksum: Buffer.from('file hash', 'utf8'), - type: AssetType.IMAGE, - files: [previewFile], - encodedVideoPath: null, - createdAt: new Date('2023-02-23T05:06:29.716Z'), - updatedAt: new Date('2023-02-23T05:06:29.716Z'), - localDateTime: new Date('2023-02-23T05:06:29.716Z'), - isFavorite: true, - isArchived: false, - isExternal: false, isOffline: false, - duration: null, - isVisible: true, - livePhotoVideo: null, - livePhotoVideoId: null, - tags: [], - sharedLinks: [], - originalFileName: 'asset-id.ext', - faces: [], - sidecarPath: '/original/path.ext.xmp', - deletedAt: null, - duplicateId: null, }), hasEncodedVideo: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, originalFileName: 'asset-id.ext', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -788,7 +736,6 @@ export const assetStub = { isFavorite: true, isArchived: false, isExternal: false, - isOffline: false, duration: null, isVisible: true, livePhotoVideo: null, @@ -802,9 +749,11 @@ export const assetStub = { } as ExifEntity, deletedAt: null, duplicateId: null, + isOffline: false, }), missingFileExtension: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -827,7 +776,6 @@ export const assetStub = { isVisible: true, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, libraryId: 'library-id', library: libraryStub.externalLibrary1, tags: [], @@ -840,9 +788,11 @@ export const assetStub = { fileSizeInByte: 5000, } as ExifEntity, duplicateId: null, + isOffline: false, }), hasFileExtension: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -865,7 +815,6 @@ export const assetStub = { isVisible: true, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, libraryId: 'library-id', library: libraryStub.externalLibrary1, tags: [], @@ -878,9 +827,11 @@ export const assetStub = { fileSizeInByte: 5000, } as ExifEntity, duplicateId: null, + isOffline: false, }), imageDng: Object.freeze({ id: 'asset-id', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -903,7 +854,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -916,9 +866,11 @@ export const assetStub = { bitsPerSample: 14, } as ExifEntity, duplicateId: null, + isOffline: false, }), hasEmbedding: Object.freeze({ id: 'asset-id-embedding', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -941,7 +893,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -956,9 +907,11 @@ export const assetStub = { assetId: 'asset-id', embedding: Array.from({ length: 512 }, Math.random), }, + isOffline: false, }), hasDupe: Object.freeze({ id: 'asset-id-dupe', + status: AssetStatus.ACTIVE, deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -981,7 +934,6 @@ export const assetStub = { isExternal: false, livePhotoVideo: null, livePhotoVideoId: null, - isOffline: false, tags: [], sharedLinks: [], originalFileName: 'asset-id.jpg', @@ -996,5 +948,6 @@ export const assetStub = { assetId: 'asset-id', embedding: Array.from({ length: 512 }, Math.random), }, + isOffline: false, }), }; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 54898d8693e75..f237e1dea942c 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -5,7 +5,7 @@ import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; import { mapUser } from 'src/dtos/user.dto'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { AssetOrder, AssetType, SharedLinkType } from 'src/enum'; +import { AssetOrder, AssetStatus, AssetType, SharedLinkType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; @@ -188,6 +188,7 @@ export const sharedLinkStub = { assets: [ { id: 'id_1', + status: AssetStatus.ACTIVE, owner: undefined as unknown as UserEntity, ownerId: 'user_id_1', deviceAssetId: 'device_asset_id_1', diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index af267dd49cd7d..dd5c3af6a8d9a 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -4,17 +4,14 @@ import { Mocked, vitest } from 'vitest'; export const newAlbumRepositoryMock = (): Mocked => { return { getById: vitest.fn(), - getByIds: vitest.fn(), getByAssetId: vitest.fn(), getMetadataForIds: vitest.fn(), - getInvalidThumbnail: vitest.fn(), getOwned: vitest.fn(), getShared: vitest.fn(), getNotShared: vitest.fn(), restoreAll: vitest.fn(), softDeleteAll: vitest.fn(), deleteAll: vitest.fn(), - getAll: vitest.fn(), addAssetIds: vitest.fn(), removeAsset: vitest.fn(), removeAssetIds: vitest.fn(), diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 69f07bf1051c4..50fff31e55e4c 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -19,14 +19,12 @@ export const newAssetRepositoryMock = (): Mocked => { getUploadAssetIdByChecksum: vitest.fn(), getWith: vitest.fn(), getRandom: vitest.fn(), - getFirstAssetForAlbumId: vitest.fn(), getLastUpdatedAssetForAlbumId: vitest.fn(), getAll: vitest.fn().mockResolvedValue({ items: [], hasNextPage: false }), getAllByDeviceId: vitest.fn(), getLivePhotoCount: vitest.fn(), updateAll: vitest.fn(), updateDuplicates: vitest.fn(), - getExternalLibraryAssetPaths: vitest.fn(), getByLibraryIdAndOriginalPath: vitest.fn(), deleteAll: vitest.fn(), update: vitest.fn(), @@ -35,15 +33,12 @@ export const newAssetRepositoryMock = (): Mocked => { getStatistics: vitest.fn(), getTimeBucket: vitest.fn(), getTimeBuckets: vitest.fn(), - restoreAll: vitest.fn(), - softDeleteAll: vitest.fn(), getAssetIdByCity: vitest.fn(), getAssetIdByTag: vitest.fn(), getAllForUserFullSync: vitest.fn(), getChangedDeltaSync: vitest.fn(), getDuplicates: vitest.fn(), upsertFile: vitest.fn(), - getAssetsByOriginalPath: vitest.fn(), - getUniqueOriginalPaths: vitest.fn(), + upsertFiles: vitest.fn(), }; }; diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts new file mode 100644 index 0000000000000..40110186f43f3 --- /dev/null +++ b/server/test/repositories/config.repository.mock.ts @@ -0,0 +1,14 @@ +import { IConfigRepository } from 'src/interfaces/config.interface'; +import { DatabaseExtension } from 'src/interfaces/database.interface'; +import { Mocked, vitest } from 'vitest'; + +export const newConfigRepositoryMock = (): Mocked => { + return { + getEnv: vitest.fn().mockReturnValue({ + database: { + skipMigration: false, + vectorExtension: DatabaseExtension.VECTORS, + }, + }), + }; +}; diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts index 6bffe184fda57..871801830a81d 100644 --- a/server/test/repositories/job.repository.mock.ts +++ b/server/test/repositories/job.repository.mock.ts @@ -5,7 +5,6 @@ export const newJobRepositoryMock = (): Mocked => { return { addHandler: vitest.fn(), addCronJob: vitest.fn(), - deleteCronJob: vitest.fn(), updateCronJob: vitest.fn(), setConcurrency: vitest.fn(), empty: vitest.fn(), diff --git a/server/test/repositories/logger.repository.mock.ts b/server/test/repositories/logger.repository.mock.ts index 5f7262c7e5d92..6342e9e73cc85 100644 --- a/server/test/repositories/logger.repository.mock.ts +++ b/server/test/repositories/logger.repository.mock.ts @@ -6,7 +6,7 @@ export const newLoggerRepositoryMock = (): Mocked => { setLogLevel: vitest.fn(), setContext: vitest.fn(), setAppName: vitest.fn(), - + isLevelEnabled: vitest.fn(), verbose: vitest.fn(), debug: vitest.fn(), log: vitest.fn(), diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts index 4c344a986679f..a809b08162347 100644 --- a/server/test/repositories/media.repository.mock.ts +++ b/server/test/repositories/media.repository.mock.ts @@ -3,8 +3,9 @@ import { Mocked, vitest } from 'vitest'; export const newMediaRepositoryMock = (): Mocked => { return { - generateThumbnail: vitest.fn(), - generateThumbhash: vitest.fn(), + generateThumbnail: vitest.fn().mockImplementation(() => Promise.resolve()), + generateThumbhash: vitest.fn().mockImplementation(() => Promise.resolve()), + decodeImage: vitest.fn().mockResolvedValue({ data: Buffer.from(''), info: {} }), extract: vitest.fn().mockResolvedValue(false), probe: vitest.fn(), transcode: vitest.fn(), diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index 5dbfb3d453c32..60c5644b36673 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -7,10 +7,5 @@ export const newMetadataRepositoryMock = (): Mocked => { readTags: vitest.fn(), writeTags: vitest.fn(), extractBinaryTag: vitest.fn(), - getCameraMakes: vitest.fn(), - getCameraModels: vitest.fn(), - getCities: vitest.fn(), - getCountries: vitest.fn(), - getStates: vitest.fn(), }; }; diff --git a/server/test/repositories/notification.repository.mock.ts b/server/test/repositories/notification.repository.mock.ts index 71975b429c98e..16862dc3d762b 100644 --- a/server/test/repositories/notification.repository.mock.ts +++ b/server/test/repositories/notification.repository.mock.ts @@ -4,7 +4,7 @@ import { Mocked } from 'vitest'; export const newNotificationRepositoryMock = (): Mocked => { return { renderEmail: vitest.fn(), - sendEmail: vitest.fn(), + sendEmail: vitest.fn().mockResolvedValue({ messageId: 'message-1' }), verifySmtp: vitest.fn(), }; }; diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts index 77e8ccf010671..6ffe7bf97be1c 100644 --- a/server/test/repositories/person.repository.mock.ts +++ b/server/test/repositories/person.repository.mock.ts @@ -18,7 +18,7 @@ export const newPersonRepositoryMock = (): Mocked => { updateAll: vitest.fn(), delete: vitest.fn(), deleteAll: vitest.fn(), - deleteAllFaces: vitest.fn(), + deleteFaces: vitest.fn(), getStatistics: vitest.fn(), getAllFaces: vitest.fn(), @@ -26,6 +26,7 @@ export const newPersonRepositoryMock = (): Mocked => { getRandomFace: vitest.fn(), reassignFaces: vitest.fn(), + unassignFaces: vitest.fn(), createFaces: vitest.fn(), replaceFaces: vitest.fn(), getFaces: vitest.fn(), diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts index fd244c6f5cccd..be0e753e30577 100644 --- a/server/test/repositories/search.repository.mock.ts +++ b/server/test/repositories/search.repository.mock.ts @@ -7,11 +7,17 @@ export const newSearchRepositoryMock = (): Mocked => { searchSmart: vitest.fn(), searchDuplicates: vitest.fn(), searchFaces: vitest.fn(), + searchRandom: vitest.fn(), upsert: vitest.fn(), searchPlaces: vitest.fn(), getAssetsByCity: vitest.fn(), deleteAllSearchEmbeddings: vitest.fn(), getDimensionSize: vitest.fn(), setDimensionSize: vitest.fn(), + getCameraMakes: vitest.fn(), + getCameraModels: vitest.fn(), + getCities: vitest.fn(), + getCountries: vitest.fn(), + getStates: vitest.fn(), }; }; diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 5c2951e097b24..5226e0bb1e985 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -48,7 +48,9 @@ export const newStorageRepositoryMock = (reset = true): Mocked => { addAssetIds: vitest.fn(), removeAssetIds: vitest.fn(), upsertAssetIds: vitest.fn(), + deleteEmptyTags: vitest.fn(), }; }; diff --git a/server/test/repositories/trash.repository.mock.ts b/server/test/repositories/trash.repository.mock.ts new file mode 100644 index 0000000000000..472b315b01410 --- /dev/null +++ b/server/test/repositories/trash.repository.mock.ts @@ -0,0 +1,11 @@ +import { ITrashRepository } from 'src/interfaces/trash.interface'; +import { Mocked, vitest } from 'vitest'; + +export const newTrashRepositoryMock = (): Mocked => { + return { + empty: vitest.fn(), + restore: vitest.fn(), + restoreAll: vitest.fn(), + getDeletedIds: vitest.fn(), + }; +}; diff --git a/server/test/repositories/view.repository.mock.ts b/server/test/repositories/view.repository.mock.ts new file mode 100644 index 0000000000000..a002362ae78f9 --- /dev/null +++ b/server/test/repositories/view.repository.mock.ts @@ -0,0 +1,9 @@ +import { IViewRepository } from 'src/interfaces/view.interface'; +import { Mocked, vitest } from 'vitest'; + +export const newViewRepositoryMock = (): Mocked => { + return { + getAssetsByOriginalPath: vitest.fn(), + getUniqueOriginalPaths: vitest.fn(), + }; +}; diff --git a/server/vitest.config.mjs b/server/vitest.config.mjs index 3c0ea00c84c7a..1013b4606df3f 100644 --- a/server/vitest.config.mjs +++ b/server/vitest.config.mjs @@ -13,7 +13,7 @@ export default defineConfig({ lines: 80, statements: 80, branches: 85, - functions: 85, + functions: 80, }, }, server: { diff --git a/web/Dockerfile b/web/Dockerfile index 4bc711e15ece5..19d8d890ab55e 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 +FROM node:20.17.0-alpine3.20@sha256:2d07db07a2df6830718ae2a47db6fedce6745f5bcd174c398f2acdda90a11c03 RUN apk add --no-cache tini USER node diff --git a/web/README.md b/web/README.md index e9693ceb01410..603c7ad64e720 100644 --- a/web/README.md +++ b/web/README.md @@ -2,4 +2,4 @@ This project uses the [SvelteKit](https://kit.svelte.dev/) web framework. Please refer to [the SvelteKit docs](https://kit.svelte.dev/docs) for information on getting started as a contributor to this project. In particular, it will help you navigate the project's code if you understand the basics of [SvelteKit routing](https://kit.svelte.dev/docs/routing). -When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [../server](the server project). +When developing locally, you will run a SvelteKit Node.js server. When this project is deployed to production, it is built as a SPA and deployed as part of [the server project](../server). diff --git a/web/package-lock.json b/web/package-lock.json index 0bf82f26b7585..a32e96e67f78b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,17 +1,17 @@ { "name": "immich-web", - "version": "1.114.0", + "version": "1.116.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.114.0", + "version": "1.116.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@mapbox/mapbox-gl-rtl-text": "^0.2.3", + "@mapbox/mapbox-gl-rtl-text": "^0.3.0", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", @@ -74,13 +74,13 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.114.0", + "version": "1.116.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.16.2", + "@types/node": "^20.16.5", "typescript": "^5.3.3" } }, @@ -726,9 +726,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": { @@ -745,10 +745,23 @@ "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/@faker-js/faker": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.0.tgz", - "integrity": "sha512-dTDHJSmz6c1OJ6HO7jiUiIb4sB20Dlkb3pxYsKm0qTXm2Bmj97rlXIhlvaFsW2rvCi+OLlwKLVSS6ZxFUVZvjQ==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.0.1.tgz", + "integrity": "sha512-4mDeYIgM3By7X6t5E6eYwLAa+2h4DeZDF7thhzIg6XB76jeEvMwadYAMCFJL/R4AnEBcAUO9+gL0vhy3s+qvZA==", "dev": true, "funding": [ { @@ -1422,9 +1435,10 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -1447,13 +1461,6 @@ "geojson-rewind": "geojson-rewind" } }, - "node_modules/@mapbox/geojson-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", - "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", - "license": "ISC", - "peer": true - }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -1463,23 +1470,10 @@ } }, "node_modules/@mapbox/mapbox-gl-rtl-text": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.3.tgz", - "integrity": "sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw==", - "license": "BSD-2-Clause", - "peerDependencies": { - "mapbox-gl": ">=0.32.1 <2.0.0" - } - }, - "node_modules/@mapbox/mapbox-gl-supported": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", - "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", - "license": "BSD-3-Clause", - "peer": true, - "peerDependencies": { - "mapbox-gl": ">=0.32.1 <2.0.0" - } + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.3.0.tgz", + "integrity": "sha512-OwQplFqAAEYRobrTKm2wiVP+wcpUVlgXXiUMNQ8tcm5gPN5SQRXFADmITdQOaec4LhDhuuFchS7TS8ua8dUl4w==", + "license": "BSD-2-Clause" }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", @@ -1539,6 +1533,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/@namnode/store/-/store-0.1.0.tgz", "integrity": "sha512-4NGTldxKcmY0UuZ7OEkvCjs8ZEoeYB6M2UwMu74pdLiFMKxXbj9HdNk1Qn213bxX1O7bY5h+PLh5DZsTURZkYA==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/willnguyen1312" @@ -1580,30 +1575,30 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.9.0.tgz", - "integrity": "sha512-Th8S2SbKpKEE5l150Mh0Na+3RirceJL9ioRl+33kE59s0Dx675snGWI7gy/xFKEWsdYOhj9f6xNWZ8MSqs8RhQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.10.0.tgz", + "integrity": "sha512-VRXPTq6bFxkf5YqmijXLTKV+K05ZZHDoccXBsoxWsS9wKA5gCfKLlebRxQBBazmnwr1KmsLbAIsd1ZGbLdhI4g==", "license": "MIT", "dependencies": { "three": "^0.167.0" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.9.0.tgz", - "integrity": "sha512-mQPnuKQPQvtNKMtjY8M3b6ANupA7soSDDLL/R8igtlP9vGMPgbVzPmGbrkyq6Ed2bQr+u8j2LkT38ztZ70Ingg==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.10.0.tgz", + "integrity": "sha512-qnxPDsT88x+mdiC//+/VnFe9z4ANz/xHjQbtm9zMNwVOryZdo1hhHtp3k80+ZnkBCF3tA8TkcEpiLifhWrHWPw==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.9.0" + "@photo-sphere-viewer/core": "5.10.0" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.9.0", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.9.0.tgz", - "integrity": "sha512-u1li4KEO7iRMhlLWZsn55Jprb8LdSyFbisvHvk75wcSLGZIZj24vabogPrDtdiXuELaC1DTD6En9IpVD/H+mGQ==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.10.0.tgz", + "integrity": "sha512-Zym35YVdNwx6Kng/P9vOJ/UmLkarJNmjYUsP2hllQVl6Xy/iznfzE9NLCr5gJ3fmLT7ICkuz5dLfdDUxOl6Btw==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.9.0" + "@photo-sphere-viewer/core": "5.10.0" } }, "node_modules/@pkgjs/parseargs": { @@ -1880,9 +1875,9 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.4.tgz", - "integrity": "sha512-Qm4GAHCnRXwfWG9/AtnQ7mqjyjTs7i0Opyb8H2KH9rMR7fLxqiPx/tXeoE6HHo66+72CjyOb4nFH3lrejY4vzA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.5.tgz", + "integrity": "sha512-kFJR7RxeB6FBvrKZWAEzIALatgy11ISaaZbcPup8JdWUdrmmfUHHTJ738YHJTEfnCiiXi6aX8Q6ePY7tnSMD6Q==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1890,9 +1885,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.4.tgz", - "integrity": "sha512-eX+ob5uWr0bTLMKeG9nhhM84aR88hqiLiyEfWZPX7ijhk/wlmYSUX9nOiaVHh2ct1U+Ju9Hhb90Copw+ZNOB8w==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.8.tgz", + "integrity": "sha512-n66u46ZeqHltiTm0BEjWptYmCrCY0EltEEvakmC7d5o5ZejDbOvOWm914mebbRKaP2Bezv65TNCod/wqvw/0KA==", "dev": true, "license": "MIT", "dependencies": { @@ -1906,9 +1901,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.25", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.25.tgz", - "integrity": "sha512-5hBSEN8XEjDZ5+2bHkFh8Z0QyOk0C187cyb12aANe1c8aeKbfu5ZD5XaC2vEH4h0alJFDXPdUkXQBmeeXeMr1A==", + "version": "2.5.28", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.28.tgz", + "integrity": "sha512-/O7pvFGBsQPcFa9UrW8eUC5uHTOXLsUp3SN0dY6YmRAL9nfPSrJsSJk//j5vMpinSshzUjteAFcfQTU+04Ka1w==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2323,17 +2318,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", @@ -2357,16 +2352,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": { @@ -2386,14 +2381,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" @@ -2404,14 +2399,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" }, @@ -2429,9 +2424,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": { @@ -2443,14 +2438,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", @@ -2497,30 +2492,17 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "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" @@ -2534,13 +2516,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": { @@ -2552,19 +2534,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", @@ -2574,17 +2557,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" }, @@ -2592,11 +2582,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" }, @@ -2605,12 +2624,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": { @@ -2618,13 +2638,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": { @@ -2632,10 +2653,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" }, @@ -2644,13 +2666,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" }, @@ -2659,9 +2681,9 @@ } }, "node_modules/@zoom-image/core": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.36.2.tgz", - "integrity": "sha512-NtqIA82xJUtTS8RMey3VUGF/q1tjkFZZUAR6edGdtiy43xIV7a239uuxomuU94WBkBtFztXL/ieyxxL8iPiyFg==", + "version": "0.38.0", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.38.0.tgz", + "integrity": "sha512-rA6/qTGfsRtWRs+WfMF0dIs+Ft9GBFusxXzEqqFsQa/0iYtN0MmOiuKzXGYPcIFKTbmQW/qqk0afIBtWd9163g==", "license": "MIT", "dependencies": { "@namnode/store": "^0.1.0" @@ -2672,12 +2694,12 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.2.19", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.19.tgz", - "integrity": "sha512-mnQ8eEmUkGi/UolaReQJmEsQu7DmX+8Y+5cdcS6nHmIM/LZImClB53/AySjJym+y5ZbDLUOOc7phgijTkPYz9g==", + "version": "0.2.22", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.22.tgz", + "integrity": "sha512-lExo4M511/HtkmCsBzV5f8ABs8bEMZGtIrwl1pJro77iJ+5j9Yt7KUlPs6o+Yp028T6fqGJUsOCxCNWNZn9BIg==", "license": "MIT", "dependencies": { - "@zoom-image/core": "0.36.2" + "@zoom-image/core": "0.38.0" }, "funding": { "type": "github", @@ -2811,6 +2833,7 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -2987,6 +3010,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" } @@ -3035,6 +3059,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", @@ -3065,6 +3090,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" } @@ -3329,13 +3355,6 @@ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, - "node_modules/csscolorparser": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", - "license": "MIT", - "peer": true - }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -3440,6 +3459,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" } @@ -3730,9 +3750,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": { @@ -3740,7 +3760,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", @@ -3763,7 +3784,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", @@ -3805,39 +3825,6 @@ "eslint": ">=6.0.0" } }, - "node_modules/eslint-compat-utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/eslint-compat-utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/eslint-config-prettier": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", @@ -3851,9 +3838,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.43.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz", - "integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==", + "version": "2.44.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.44.0.tgz", + "integrity": "sha512-wav4MOs02vBb1WjvTCYItwJCxMkuk2Z4p+K/eyjL0N/z7ahXLP+0LtQQjiKc2ezuif7GnZLbD1F3o1VHzSvdVg==", "dev": true, "license": "MIT", "dependencies": { @@ -3867,7 +3854,7 @@ "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", - "svelte-eslint-parser": "^0.41.0" + "svelte-eslint-parser": "^0.41.1" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -3885,19 +3872,6 @@ } } }, - "node_modules/eslint-plugin-svelte/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-plugin-unicorn": { "version": "55.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", @@ -3945,19 +3919,6 @@ "node": ">=6" } }, - "node_modules/eslint-plugin-unicorn/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -4219,41 +4180,6 @@ "es5-ext": "~0.10.14" } }, - "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/execa/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/ext": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", @@ -4499,6 +4425,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": "*" } @@ -4592,13 +4519,6 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, - "node_modules/grid-index": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", - "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", - "license": "ISC", - "peer": true - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -4696,15 +4616,6 @@ "node": ">= 14" } }, - "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.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -5001,18 +4912,6 @@ "@types/estree": "*" } }, - "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/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -5347,6 +5246,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" } @@ -5378,12 +5278,12 @@ } }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magicast": { @@ -5412,90 +5312,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", - "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mapbox-gl": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", - "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", - "license": "SEE LICENSE IN LICENSE.txt", - "peer": true, - "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/geojson-types": "^1.0.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^1.5.0", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^1.1.1", - "@mapbox/unitbezier": "^0.0.0", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "csscolorparser": "~1.0.3", - "earcut": "^2.2.2", - "geojson-vt": "^3.2.1", - "gl-matrix": "^3.2.1", - "grid-index": "^1.1.0", - "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", - "potpack": "^1.0.1", - "quickselect": "^2.0.0", - "rw": "^1.3.3", - "supercluster": "^7.1.0", - "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.1" - }, - "engines": { - "node": ">=6.4.0" - } - }, - "node_modules/mapbox-gl/node_modules/@mapbox/tiny-sdf": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", - "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/mapbox-gl/node_modules/@mapbox/unitbezier": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", - "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", - "license": "BSD-2-Clause", - "peer": true - }, - "node_modules/mapbox-gl/node_modules/kdbush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", - "license": "ISC", - "peer": true - }, - "node_modules/mapbox-gl/node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC", - "peer": true - }, - "node_modules/mapbox-gl/node_modules/supercluster": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", - "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", - "license": "ISC", - "peer": true, - "dependencies": { - "kdbush": "^3.0.0" - } - }, "node_modules/maplibre-gl": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.0.1.tgz", @@ -5559,12 +5375,6 @@ "node": ">=0.12" } }, - "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", @@ -5612,18 +5422,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/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5780,33 +5578,6 @@ "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", - "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/nwsapi": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz", @@ -5842,21 +5613,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/open": { "version": "8.4.2", "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", @@ -6039,13 +5795,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" } @@ -6073,9 +5831,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "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" }, @@ -6128,9 +5886,9 @@ } }, "node_modules/postcss": { - "version": "8.4.41", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", - "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -6149,8 +5907,8 @@ "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" @@ -6828,6 +6586,19 @@ "node": ">=v12.22.7" } }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/set-cookie-parser": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.6.0.tgz", @@ -6900,39 +6671,6 @@ "@img/sharp-win32-x64": "0.33.3" } }, - "node_modules/sharp/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7068,9 +6806,10 @@ } }, "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==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7217,18 +6956,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", @@ -7353,9 +7080,9 @@ } }, "node_modules/svelte-check": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.0.tgz", - "integrity": "sha512-QgKO6OQbee9B2dyWZgrGruS3WHKrUZ718Ug53nK45vamsx93Al3on6tOrxyCMVX+OMOLLlrenn7b2VAomePwxQ==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.0.2.tgz", + "integrity": "sha512-w2yqcG9ELJe2RJCnAvB7v0OgkHhL3czzz/tVoxGFfO6y4mOrF6QHCDhXijeXzsU7LVKEwWS3Qd9tza4JBuDxqA==", "dev": true, "license": "MIT", "dependencies": { @@ -7407,9 +7134,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz", - "integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.1.tgz", + "integrity": "sha512-08ndI6zTghzI8SuJAFpvMbA/haPSGn3xz19pjre19yYMw8Nw/wQJ2PrZBI/L8ijGTgtkWCQQiLLy+Z1tfaCwNA==", "dev": true, "license": "MIT", "dependencies": { @@ -7925,9 +7652,9 @@ "peer": true }, "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==", "dev": true, "license": "MIT", "dependencies": { @@ -8012,9 +7739,9 @@ } }, "node_modules/tailwindcss/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": { @@ -8139,10 +7866,18 @@ } }, "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", @@ -8168,10 +7903,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" } @@ -8279,9 +8015,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": { @@ -8411,14 +8147,14 @@ } }, "node_modules/vite": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", - "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.41", + "postcss": "^8.4.43", "rollup": "^4.20.0" }, "bin": { @@ -8484,15 +8220,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": { @@ -8520,29 +8256,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": { @@ -8557,8 +8294,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/web/package.json b/web/package.json index b59835b80c7da..20553759fad43 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.114.0", + "version": "1.116.2", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", @@ -67,7 +67,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "@immich/sdk": "file:../open-api/typescript-sdk", - "@mapbox/mapbox-gl-rtl-text": "^0.2.3", + "@mapbox/mapbox-gl-rtl-text": "^0.3.0", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", diff --git a/web/src/app.html b/web/src/app.html index 778375c1e142b..ff6a8bf580a89 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -74,7 +74,7 @@ if (!theme) { theme = { value: 'light', system: true }; } else if (theme === 'dark' || theme === 'light') { - theme = { value: item, system: false }; + theme = { value: theme, system: false }; localStorage.setItem(colorThemeKeyName, JSON.stringify(theme)); } else { theme = JSON.parse(theme); diff --git a/web/src/lib/actions/click-outside.ts b/web/src/lib/actions/click-outside.ts index bbcb0c405b718..1a421f1f5625e 100644 --- a/web/src/lib/actions/click-outside.ts +++ b/web/src/lib/actions/click-outside.ts @@ -6,6 +6,12 @@ interface Options { onEscape?: () => void; } +/** + * Calls a function when a click occurs outside of the element, or when the escape key is pressed. + * @param node + * @param options Object containing onOutclick and onEscape functions + * @returns + */ export function clickOutside(node: HTMLElement, options: Options = {}): ActionReturn { const { onOutclick, onEscape } = options; diff --git a/web/src/lib/actions/focus-outside.ts b/web/src/lib/actions/focus-outside.ts index 2266ea8f0ff83..c302e33d4ca2c 100644 --- a/web/src/lib/actions/focus-outside.ts +++ b/web/src/lib/actions/focus-outside.ts @@ -2,6 +2,11 @@ interface Options { onFocusOut?: (event: FocusEvent) => void; } +/** + * Calls a function when focus leaves the element. + * @param node + * @param options Object containing onFocusOut function + */ export function focusOutside(node: HTMLElement, options: Options = {}) { const { onFocusOut } = options; diff --git a/web/src/lib/actions/focus.ts b/web/src/lib/actions/focus.ts index 81185625f7332..3b6049f24732f 100644 --- a/web/src/lib/actions/focus.ts +++ b/web/src/lib/actions/focus.ts @@ -1,3 +1,4 @@ +/** Focus the given element when it is mounted. */ export const initInput = (element: HTMLInputElement) => { element.focus(); }; diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts index 700ae0c3733b4..edbc07e5c1f09 100644 --- a/web/src/lib/actions/intersection-observer.ts +++ b/web/src/lib/actions/intersection-observer.ts @@ -13,7 +13,9 @@ type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElem type OnSeparateCallback = (element: HTMLElement) => unknown; type IntersectionObserverActionProperties = { key?: string; + /** Function to execute when the element leaves the viewport */ onSeparate?: OnSeparateCallback; + /** Function to execute when the element enters the viewport */ onIntersect?: OnIntersectCallback; root?: Element | Document | null; @@ -112,6 +114,12 @@ function _intersectionObserver( }; } +/** + * Monitors an element's visibility in the viewport and calls functions when it enters or leaves (based on a threshold). + * @param element + * @param properties One or multiple configurations for the IntersectionObserver(s) + * @returns + */ export function intersectionObserver( element: HTMLElement, properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[], diff --git a/web/src/lib/actions/list-navigation.ts b/web/src/lib/actions/list-navigation.ts index b981f675214fe..8f8ed62ed009e 100644 --- a/web/src/lib/actions/list-navigation.ts +++ b/web/src/lib/actions/list-navigation.ts @@ -1,6 +1,11 @@ import { shortcuts } from '$lib/actions/shortcut'; import type { Action } from 'svelte/action'; +/** + * Enables keyboard navigation (up and down arrows) for a list of elements. + * @param node Element which listens for keyboard events + * @param container Element containing the list of elements + */ export const listNavigation: Action = (node, container: HTMLElement) => { const moveFocus = (direction: 'up' | 'down') => { const children = Array.from(container?.children); diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts index df155ea821ad0..6348257c40496 100644 --- a/web/src/lib/actions/shortcut.ts +++ b/web/src/lib/actions/shortcut.ts @@ -10,11 +10,16 @@ export type Shortcut = { export type ShortcutOptions = { shortcut: Shortcut; + /** If true, the event handler will not execute if the event comes from an input field */ ignoreInputFields?: boolean; onShortcut: (event: KeyboardEvent & { currentTarget: T }) => unknown; preventDefault?: boolean; }; +/** Determines whether an event should be ignored. The event will be ignored if: + * - The element dispatching the event is not the same as the element which the event listener is attached to + * - The element dispatching the event is an input field + */ export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => { if (event.target === event.currentTarget) { return false; @@ -33,6 +38,7 @@ export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => { ); }; +/** Bind a single keyboard shortcut to node. */ export const shortcut = ( node: T, option: ShortcutOptions, @@ -47,6 +53,7 @@ export const shortcut = ( }; }; +/** Binds multiple keyboard shortcuts to node */ export const shortcuts = ( node: T, options: ShortcutOptions[], diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts index ab9d28ffc9b4d..e49f04dbee546 100644 --- a/web/src/lib/actions/thumbhash.ts +++ b/web/src/lib/actions/thumbhash.ts @@ -1,6 +1,11 @@ import { decodeBase64 } from '$lib/utils'; import { thumbHashToRGBA } from 'thumbhash'; +/** + * Renders a thumbnail onto a canvas from a base64 encoded hash. + * @param canvas + * @param param1 object containing the base64 encoded hash (base64Thumbhash: yourString) + */ export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) { const ctx = canvas.getContext('2d'); if (ctx) { diff --git a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte index e8490339a6eb7..a2fbbe787a1e9 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialogue.svelte @@ -1,25 +1,21 @@
dispatch('command', { command: JobCommand.ClearFailed, force: false })} + on:click={() => onCommand({ command: JobCommand.ClearFailed, force: false })} />
@@ -117,54 +116,42 @@ dispatch('command', { command: JobCommand.Start, force: false })} + on:click={() => onCommand({ command: JobCommand.Start, force: false })} > {$t('disabled').toUpperCase()} {:else if !isIdle} {#if waitingCount > 0} - dispatch('command', { command: JobCommand.Empty, force: false })}> + onCommand({ command: JobCommand.Empty, force: false })}> {$t('clear').toUpperCase()} {/if} {#if queueStatus.isPaused} {@const size = waitingCount > 0 ? '24' : '48'} - dispatch('command', { command: JobCommand.Resume, force: false })} - > + onCommand({ command: JobCommand.Resume, force: false })}> {$t('resume').toUpperCase()} {:else} - dispatch('command', { command: JobCommand.Pause, force: false })} - > + onCommand({ command: JobCommand.Pause, force: false })}> {$t('pause').toUpperCase()} {/if} {:else if allowForceCommand} - dispatch('command', { command: JobCommand.Start, force: true })}> + onCommand({ command: JobCommand.Start, force: true })}> {allText} - dispatch('command', { command: JobCommand.Start, force: false })} - > + onCommand({ command: JobCommand.Start, force: false })}> {missingText} {:else} - dispatch('command', { command: JobCommand.Start, force: false })} - > + onCommand({ command: JobCommand.Start, force: false })}> {$t('start').toUpperCase()} diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte index cd9855eea21bb..6de3112ad5a8a 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -163,7 +163,7 @@ {allowForceCommand} {jobCounts} {queueStatus} - on:command={({ detail }) => (handleCommandOverride || handleCommand)(jobName, detail)} + onCommand={(command) => (handleCommandOverride || handleCommand)(jobName, command)} /> {/each} diff --git a/web/src/lib/components/admin-page/restore-dialogue.svelte b/web/src/lib/components/admin-page/restore-dialogue.svelte index 9b274d2c2f314..25afbc6d4b9b6 100644 --- a/web/src/lib/components/admin-page/restore-dialogue.svelte +++ b/web/src/lib/components/admin-page/restore-dialogue.svelte @@ -3,28 +3,24 @@ import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; import { handleError } from '$lib/utils/handle-error'; import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk'; - import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n'; export let user: UserResponseDto; - - const dispatch = createEventDispatcher<{ - success: void; - fail: void; - cancel: void; - }>(); + export let onSuccess: () => void; + export let onFail: () => void; + export let onCancel: () => void; const handleRestoreUser = async () => { try { const { deletedAt } = await restoreUserAdmin({ id: user.id }); if (deletedAt == undefined) { - dispatch('success'); + onSuccess(); } else { - dispatch('fail'); + onFail(); } } catch (error) { handleError(error, $t('errors.unable_to_restore_user')); - dispatch('fail'); + onFail(); } }; @@ -34,7 +30,7 @@ confirmText={$t('continue')} confirmColor="green" onConfirm={handleRestoreUser} - onCancel={() => dispatch('cancel')} + {onCancel} >

diff --git a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte index 37f875c604f16..9b0e4b32706b5 100644 --- a/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/auth/auth-settings.svelte @@ -71,7 +71,7 @@

-
+
(config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])} + onSelect={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])} /> + onSelect={() => config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec) ? null : config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)} diff --git a/web/src/lib/components/admin-page/settings/image/image-settings.svelte b/web/src/lib/components/admin-page/settings/image/image-settings.svelte index a7b47920fd98b..b5e381d5f87a0 100644 --- a/web/src/lib/components/admin-page/settings/image/image-settings.svelte +++ b/web/src/lib/components/admin-page/settings/image/image-settings.svelte @@ -11,6 +11,7 @@ SettingInputFieldType, } from '$lib/components/shared-components/settings/setting-input-field.svelte'; import { t } from 'svelte-i18n'; + import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; export let savedConfig: SystemConfigDto; export let defaultConfig: SystemConfigDto; @@ -24,79 +25,102 @@
- + + - + - + + - + + - + + + + (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)} + onToggle={(isChecked) => (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)} isEdited={config.image.colorspace !== savedConfig.image.colorspace} {disabled} /> @@ -105,7 +129,7 @@ title={$t('admin.image_prefer_embedded_preview')} subtitle={$t('admin.image_prefer_embedded_preview_setting_description')} checked={config.image.extractEmbedded} - on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)} + onToggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)} isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} {disabled} /> diff --git a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte index f6ed132c8c0a7..68867a2a494ac 100644 --- a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte +++ b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte @@ -29,95 +29,84 @@
- - -
- -
- -
- onReset({ ...options, configKeys: ['library'] })} - onSave={() => onSave({ library: config.library })} - showResetToDefault={!isEqual(savedConfig.library, defaultConfig.library)} - {disabled} - /> -
- -
- - -
-
- - -
- - + +
+ +
+
+
- - -

- - - {message} - - -

-
-
-
+ +
+ -
- onReset({ ...options, configKeys: ['library'] })} - onSave={() => onSave({ library: config.library })} - showResetToDefault={!isEqual(savedConfig.library, defaultConfig.library)} - {disabled} - /> -
- - +
+ + +
+ + + +

+ + + {message} + + +

+
+
+
+
+ + onReset({ ...options, configKeys: ['library'] })} + onSave={() => onSave({ library: config.library })} + showResetToDefault={!isEqual(savedConfig.library, defaultConfig.library)} + {disabled} + /> +
+
diff --git a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte index 05a5224bd022b..aac8cd52123be 100644 --- a/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte +++ b/web/src/lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte @@ -145,7 +145,7 @@ desc={$t('admin.machine_learning_min_detection_score_description')} bind:value={config.machineLearning.facialRecognition.minScore} step="0.1" - min={0} + min={0.1} max={1} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.minScore !== @@ -158,7 +158,7 @@ desc={$t('admin.machine_learning_max_recognition_distance_description')} bind:value={config.machineLearning.facialRecognition.maxDistance} step="0.1" - min={0} + min={0.1} max={2} disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} isEdited={config.machineLearning.facialRecognition.maxDistance !== diff --git a/web/src/lib/components/album-page/album-options.svelte b/web/src/lib/components/album-page/album-options.svelte index 84a2873788d4b..ebcf835649070 100644 --- a/web/src/lib/components/album-page/album-options.svelte +++ b/web/src/lib/components/album-page/album-options.svelte @@ -2,7 +2,6 @@ import Icon from '$lib/components/elements/icon.svelte'; import { updateAlbumInfo, type AlbumResponseDto, type UserResponseDto, AssetOrder } from '@immich/sdk'; import { mdiArrowDownThin, mdiArrowUpThin, mdiPlus } from '@mdi/js'; - import { createEventDispatcher } from 'svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; @@ -16,6 +15,9 @@ export let order: AssetOrder | undefined; export let user: UserResponseDto; export let onChangeOrder: (order: AssetOrder) => void; + export let onClose: () => void; + export let onToggleEnabledActivity: () => void; + export let onShowSelectSharedUser: () => void; const options: Record = { [AssetOrder.Asc]: { icon: mdiArrowUpThin, title: $t('oldest_first') }, @@ -24,12 +26,6 @@ $: selectedOption = order ? options[order] : options[AssetOrder.Desc]; - const dispatch = createEventDispatcher<{ - close: void; - toggleEnableActivity: void; - showSelectSharedUser: void; - }>(); - const handleToggle = async (returnedOption: RenderedOption) => { if (selectedOption === returnedOption) { return; @@ -51,7 +47,7 @@ }; - dispatch('close')}> +

{$t('settings').toUpperCase()}

@@ -68,14 +64,14 @@ title={$t('comments_and_likes')} subtitle={$t('let_others_respond')} checked={album.isActivityEnabled} - on:toggle={() => dispatch('toggleEnableActivity')} + onToggle={onToggleEnabledActivity} />
{$t('people').toUpperCase()}
-
{/key} @@ -152,10 +151,8 @@ rounded="full" disabled={Object.keys(selectedUsers).length === 0} on:click={() => - dispatch( - 'select', - Object.values(selectedUsers).map(({ user, ...rest }) => ({ userId: user.id, ...rest })), - )}>{$t('add')} ({ userId: user.id, ...rest })))} + >{$t('add')}
{/if} @@ -166,7 +163,7 @@ -
{/if} @@ -625,10 +622,10 @@ assetId={asset.id} {isLiked} bind:reactions - on:addComment={handleAddComment} - on:deleteComment={handleRemoveComment} - on:deleteLike={() => (isLiked = null)} - on:close={() => (isShowActivity = false)} + onAddComment={handleAddComment} + onDeleteComment={handleRemoveComment} + onDeleteLike={() => (isLiked = null)} + onClose={() => (isShowActivity = false)} />
{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel-location.svelte b/web/src/lib/components/asset-viewer/detail-panel-location.svelte index a93c90d0d4ac7..7d5d86b4435dc 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-location.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-location.svelte @@ -81,10 +81,6 @@ {#if isShowChangeLocation} - handleConfirmChangeLocation(gps)} - on:cancel={() => (isShowChangeLocation = false)} - /> + (isShowChangeLocation = false)} /> {/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte index 434682f73ec27..449f61183fb33 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -1,6 +1,7 @@
diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 30a2018febc80..da8febc3d94c3 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,10 +1,11 @@ @@ -38,7 +37,7 @@ title={$t('search')} size="16" padding="2" - on:click={() => dispatch('search', { force: true })} + on:click={() => onSearch({ force: true })} /> dispatch('search', { force: false })} + on:input={() => onSearch({ force: false })} /> {#if showLoadingSpinner}
diff --git a/web/src/lib/components/elements/slider.svelte b/web/src/lib/components/elements/slider.svelte index 68b085fb91826..efe67fda9ccfd 100644 --- a/web/src/lib/components/elements/slider.svelte +++ b/web/src/lib/components/elements/slider.svelte @@ -1,6 +1,4 @@
- {#if !device.current} + {#if !device.current && onDelete}
dispatcher('delete')} + on:click={onDelete} />
{/if} diff --git a/web/src/lib/components/user-settings-page/device-list.svelte b/web/src/lib/components/user-settings-page/device-list.svelte index 57299bb46fb22..26e03c35d8acd 100644 --- a/web/src/lib/components/user-settings-page/device-list.svelte +++ b/web/src/lib/components/user-settings-page/device-list.svelte @@ -68,7 +68,7 @@ {$t('other_devices').toUpperCase()} {#each otherDevices as device, index} - handleDelete(device)} /> + handleDelete(device)} /> {#if index !== otherDevices.length - 1}
{/if} diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte index dc11dab15e8d0..d04bbc3e7d3c9 100644 --- a/web/src/lib/components/user-settings-page/feature-settings.svelte +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -55,7 +55,7 @@
-
+
diff --git a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte index 3cff1cd1de2bc..8ab747aa276da 100644 --- a/web/src/lib/components/user-settings-page/partner-selection-modal.svelte +++ b/web/src/lib/components/user-settings-page/partner-selection-modal.svelte @@ -1,19 +1,18 @@ - + - + - + - + - + - + {#if $featureFlags.loaded && $featureFlags.oauth} {/if} - + - + 1} - on:next={() => { + onNext={() => { const index = getAssetIndex($viewingAsset.id) + 1; setAsset(assets[index % assets.length]); }} - on:previous={() => { + onPrevious={() => { const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; setAsset(assets[index % assets.length]); }} - on:close={() => { + onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 05011680dcc41..3abea669e6013 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -71,9 +71,8 @@ export const dateFormats = { export enum QueryParameter { ACTION = 'action', - ASSET_INDEX = 'assetIndex', + ID = 'id', IS_OPEN = 'isOpen', - MEMORY_INDEX = 'memoryIndex', ONBOARDING_STEP = 'step', OPEN_SETTING = 'openSetting', PREVIOUS_ROUTE = 'previousRoute', @@ -94,7 +93,7 @@ export enum ActionQueryParameterValue { MERGE = 'merge', } -export const maximumLengthSearchPeople: number = 20; +export const maximumLengthSearchPeople = 1000; // time to load the map before displaying the loading spinner export const timeToLoadTheMap: number = 100; diff --git a/web/src/lib/i18n/ar.json b/web/src/lib/i18n/ar.json index 1d90f9e625dde..9291ba627177b 100644 --- a/web/src/lib/i18n/ar.json +++ b/web/src/lib/i18n/ar.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "ملاحظة: لا يمكن تغيير هذا لاحقًا!", "note_unlimited_quota": "ملاحظة: أدخل 0 للحصول على حصة غير محدودة", "notification_email_from_address": "عنوان المرسل", - "notification_email_from_address_description": "عنوان البريد الإلكتروني للمرسل، على سبيل المثال: \"Immich Photo Server noreply@immich.app\"", + "notification_email_from_address_description": "عنوان البريد الإلكتروني للمرسل، على سبيل المثال: \"Immich Photo Server noreply@example.com\"", "notification_email_host_description": "مضيف خادم البريد الإلكتروني (مثلًا: smtp.immich.app)", "notification_email_ignore_certificate_errors": "تجاهل أخطاء الشهادة", "notification_email_ignore_certificate_errors_description": "تجاهل أخطاء التحقق من صحة شهادة TLS (غير مستحسن)", @@ -198,7 +198,7 @@ "refreshing_all_libraries": "تحديث كافة المكتبات", "registration": "تسجيل المدير", "registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.", - "removing_offline_files": "إزالة الملفات غير المتصلة", + "removing_deleted_files": "إزالة الملفات غير المتصلة", "repair_all": "إصلاح الكل", "repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}", "repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}", @@ -671,8 +671,8 @@ "unable_to_remove_api_key": "تعذر إزالة مفتاح API", "unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "غير قادر على إزالة الملفات غير المتصلة", "unable_to_remove_library": "غير قادر على إزالة المكتبة", - "unable_to_remove_offline_files": "غير قادر على إزالة الملفات غير المتصلة", "unable_to_remove_partner": "غير قادر على إزالة الشريك", "unable_to_remove_reaction": "غير قادر على إزالة رد الفعل", "unable_to_remove_user": "", @@ -1072,10 +1072,10 @@ "remove_assets_shared_link_confirmation": "هل أنت متأكد أنك تريد إزالة {count, plural, one {# المحتوى} other {# المحتويات}} من رابط المشاركة هذا؟", "remove_assets_title": "هل تريد إزالة المحتويات؟", "remove_custom_date_range": "إزالة النطاق الزمني المخصص", + "remove_deleted_assets": "إزالة الملفات الغير متصلة", "remove_from_album": "إزالة من الألبوم", "remove_from_favorites": "إزالة من المفضلة", "remove_from_shared_link": "إزالة من الرابط المشترك", - "remove_offline_files": "إزالة الملفات الغير متصلة", "remove_user": "إزالة المستخدم", "removed_api_key": "تم إزالة مفتاح API: {name}", "removed_from_archive": "تمت إزالتها من الأرشيف", diff --git a/web/src/lib/i18n/bg.json b/web/src/lib/i18n/bg.json index 3780c48482c28..9e4807f602a26 100644 --- a/web/src/lib/i18n/bg.json +++ b/web/src/lib/i18n/bg.json @@ -12,19 +12,19 @@ "add_a_description": "Добави описание", "add_a_location": "Добави местоположение", "add_a_name": "Добави име", - "add_a_title": "Добави заглавие", + "add_a_title": "Добавете заглавие", "add_exclusion_pattern": "Добави модел за изключване", "add_import_path": "Добави път за импортиране", - "add_location": "Добави местоположение", - "add_more_users": "Добави още потребители", - "add_partner": "Добави партньор", + "add_location": "Добавете местоположение", + "add_more_users": "Добавете още потребители", + "add_partner": "Добавете партньор", "add_path": "Добави път", - "add_photos": "Добави снимки", + "add_photos": "Добавете снимки", "add_to": "Добави към...", "add_to_album": "Добави към албум", "add_to_shared_album": "Добави към споделен албум", - "added_to_archive": "Добавено в архива", - "added_to_favorites": "Добавено към любими", + "added_to_archive": "Добавено към архива", + "added_to_favorites": "Добавени към любимите ви", "added_to_favorites_count": "Добавени {count, number} към любими", "admin": { "add_exclusion_pattern_description": "Добави модели за изключване. Поддържа се \"globbing\" с помощта на *, ** и ?. За да игнорирате всички файлове в директория с име \"Raw\", използвайте \"**/Raw/**\". За да игнорирате всички файлове, завършващи на \".tif\", използвайте \"**/*.tif\". За да игнорирате абсолютен път, използвайте \"/path/to/ignore/**\".", @@ -137,7 +137,11 @@ "map_settings_description": "Управление на настройките на картата", "map_style_description": "URL адрес към файл \"style.json\" за задаване на стил на картата", "metadata_extraction_job": "Извличане на метаданни", - "metadata_extraction_job_description": "Извличане на метаданни от всеки ресурс, като GPS и резолюция", + "metadata_extraction_job_description": "Извличане на метаданни от всеки от ресурсите, като GPS локация, лица и резолюция на файловете", + "metadata_faces_import_setting": "Включи импорт на лице", + "metadata_faces_import_setting_description": "Импортирай лица от EXIF данни и помощни файлове", + "metadata_settings": "Опции за метаданни", + "metadata_settings_description": "Управление на настройките за метаданни", "migration_job": "Миграция", "migration_job_description": "Мигриране на миниатюрите за ресурси и лица към най-новата структура на папките", "no_paths_added": "Няма добавени пътища", @@ -146,7 +150,7 @@ "note_cannot_be_changed_later": "ВНИМАНИЕ: Това не може да бъде променено по-късно!", "note_unlimited_quota": "Бележка: Въведете 0 за да нямате лимит на квотата", "notification_email_from_address": "От адрес", - "notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server \"", + "notification_email_from_address_description": "Електронна поща на изпращача, например: \"Immich Photo Server \"", "notification_email_host_description": "Хост на сървъра за електронна поща (например: smtp.immich.app)", "notification_email_ignore_certificate_errors": "Игорнорирайте сертификационни грешки", "notification_email_ignore_certificate_errors_description": "Игнорирай грешки свързани с валидация на TLS сертификат (не се препоръчва)", @@ -172,7 +176,7 @@ "oauth_issuer_url": "URL на издателя", "oauth_mobile_redirect_uri": "URI за мобилно пренасочване", "oauth_mobile_redirect_uri_override": "URI пренасочване за мобилни устройства", - "oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.", + "oauth_mobile_redirect_uri_override_description": "Разреши когато доставчика за OAuth удостоверяване не позволява за мобилни URI идентификатори, като '{callback}'", "oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили", "oauth_profile_signing_algorithm_description": "Алгоритъм излпозлван за вписване на потребителски профил.", "oauth_scope": "Област/обхват на приложение", @@ -196,7 +200,7 @@ "refreshing_all_libraries": "Опресняване на всички библиотеки", "registration": "Администраторска регистрация", "registration_description": "Тъй като сте първият потребител в системата, ще бъдете назначен като администратор и ще отговаряте за административните задачи, а допълнителните потребители ще бъдат създадени от вас.", - "removing_offline_files": "Премахване на офлайн файлове", + "removing_deleted_files": "Премахване на офлайн файлове", "repair_all": "Поправяне на всичко", "repair_matched_items": "{count, plural, one {Съвпадащ елемент (#)} other {Съвпадащи елементи (#)}}", "repaired_items": "{count, plural, one {Поправен елемент (#)} other {Поправени елементи (#)}}", @@ -240,7 +244,7 @@ "thumbnail_generation_job": "Генериране на миниатюри", "thumbnail_generation_job_description": "Генерирайте големи, малки и замъглени миниатюри за всеки актив, както и миниатюри за всеки човек", "transcoding_acceleration_api": "API за ускоряване", - "transcoding_acceleration_api_description": "API, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „best effort“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може или не може да работи в зависимост от вашия хардуер.", + "transcoding_acceleration_api_description": "API интерфейсът, който ще взаимодейства с вашето устройство, за да ускори транскодирането. Тази настройка е „възможно най-доброто“: тя ще се върне към софтуерно транскодиране при повреда. VP9 може и да не работи в зависимост от вашия хардуер.", "transcoding_acceleration_nvenc": "NVENC (необходим NVIDIA GPU)", "transcoding_acceleration_qsv": "Quick Sync (необходим 7th поколение Intel CPU или по-ново)", "transcoding_acceleration_rkmpp": "RKMPP (само на Rockchip SOCs)", @@ -248,9 +252,9 @@ "transcoding_accepted_audio_codecs": "Допустими аудио кодеци", "transcoding_accepted_audio_codecs_description": "Изберете кои аудио кодеци не са нужни за разкодиране. Използва се само за определени правила за разкодиране.", "transcoding_accepted_containers": "Приети контейнери", - "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не трябва да се пренасочват към MP4. Използва се само за определени правила за разкодиране.", + "transcoding_accepted_containers_description": "Изберете кои формати на контейнери не е нужно да бъдат преобразувани в MP4 формат. Използва се само за определени правила за разкодиране.", "transcoding_accepted_video_codecs": "Приети видео кодеци", - "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябва да се разкодиране. Използва се само за определени правила за разкодиране.", + "transcoding_accepted_video_codecs_description": "Изберете кои видео кодеци не трябват за разкодиране. Използва се само за определени правила за разкодиране.", "transcoding_advanced_options_description": "Опции, които повечето потребители не трябва да променят", "transcoding_audio_codec": "Аудио кодек", "transcoding_audio_codec_description": "Opus е опцията с най-високо качество, но има по-ниска съвместимост със стари устройства или софтуер.", @@ -275,7 +279,7 @@ "transcoding_optimal_description": "Видеоклипове с по-висока от целевата разделителна способност или не в приетия формат", "transcoding_preferred_hardware_device": "Предпочитано хардуерно устройство", "transcoding_preferred_hardware_device_description": "Прилага се само за VAAPI и QSV. Задава dri възела, използван за хардуерно транскодиране.", - "transcoding_preset_preset": "", + "transcoding_preset_preset": "Предварително зададени(-preset)", "transcoding_preset_preset_description": "Скорост на компресия. По-бавните предварително зададени настройки създават по-малки файлове и повишават качеството при насочване към определен битрейт. VP9 игнорира скорости над „по-бързо“.", "transcoding_reference_frames": "Референтни фреймове", "transcoding_reference_frames_description": "Броят кадри за препратка при компресиране на даден кадър. По-високите стойности подобряват ефективността на компресията, но забавят кодирането. 0 задава тази стойност автоматично.", @@ -292,10 +296,10 @@ "transcoding_tone_mapping_description": "Опитва се да запази външния вид на HDR видеоклипове, когато се преобразува в SDR. Всеки алгоритъм прави различни компромиси за цвят, детайлност и яркост. Hable запазва детайлите, Mobius запазва цвета, а Reinhard запазва яркостта.", "transcoding_tone_mapping_npl": "", "transcoding_tone_mapping_npl_description": "Цветовете ще бъдат коригирани, за да изглеждат нормално за дисплей с тази яркост. Противоинтуитивно, по-ниските стойности увеличават яркостта на видеото и обратно, тъй като компенсират яркостта на дисплея. 0 задава тази стойност автоматично.", - "transcoding_transcode_policy": "", + "transcoding_transcode_policy": "Правила за транскодиране", "transcoding_transcode_policy_description": "Правила за това кога видеоклипът трябва да бъде транскодиран. HDR видеоклиповете винаги ще бъдат транскодирани (освен ако транскодирането е деактивирано).", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", + "transcoding_two_pass_encoding": "Кодиране с двойно минаване", + "transcoding_two_pass_encoding_setting_description": "Транскодирането с две минавания създава по-добре кодиране видеа. Когато максималния битрейт е включен (задължително е да се работи с H.264 и HEVC), тази опция използва диапазон на битрейта базиран на максималния битрейт и игнорира CRF. За VP9, CRF може да се използва ако максималният битрейт е изключен.", "transcoding_video_codec": "Видеокодек", "transcoding_video_codec_description": "VP9 има висока ефективност и уеб съвместимост, но отнема повече време за транскодиране. HEVC работи по подобен начин, но има по-ниска уеб съвместимост. H.264 е широко съвместим и бърз за разкодиране, но създава много по-големи файлове. AV1 е най-ефективният кодек, но му липсва поддръжка на по-стари устройства.", "trash_enabled_description": "Активирайте функциите за кошче", @@ -442,7 +446,7 @@ "copy_to_clipboard": "Копиране в клипборда", "country": "Държава", "cover": "", - "covers": "", + "covers": "Обложка", "create": "Създай", "create_album": "Създай албум", "create_library": "Създай библиотека", @@ -601,8 +605,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "", @@ -891,10 +895,10 @@ "refreshed": "Опреснено", "refreshes_every_file": "", "remove": "Премахни", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "rename": "Преименувай", "repair": "Поправи", @@ -934,19 +938,22 @@ "search_city": "", "search_country": "", "search_for_existing_person": "", - "search_people": "", - "search_places": "", + "search_people": "Търсете на хора", + "search_places": "Търсене на места", "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", + "search_tags": "Търсене на етикети...", + "search_timezone": "Търсене на часова зона...", + "search_type": "Тип на търсене", + "search_your_photos": "Търсете вашите снимки", "searching_locales": "", "second": "Секунда", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", + "see_all_people": "Вижте всички хора", + "select_album_cover": "Изберете обложка на албум", + "select_all": "Изберете всички", + "select_avatar_color": "Изберете цвят на аватара", + "select_face": "Изберете лице", "select_featured_photo": "", + "select_from_computer": "Изберете от компютъра", "select_keep_all": "", "select_library_owner": "Изберете собственик на библиотека", "select_new_face": "Изберете ново лице", @@ -994,28 +1001,40 @@ "show_metadata": "Покажи метаданни", "show_or_hide_info": "Покажи или скрий информацията", "show_password": "Покажи паролата", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", + "show_person_options": "Показване на опции за лица", + "show_progress_bar": "Показване на прогрес бара", + "show_search_options": "Показване на опциите за търсене", + "show_supporter_badge": "Значка поддръжник", + "show_supporter_badge_description": "Покажи значка поддръжник", "shuffle": "Разбъркване", - "sign_out": "", - "sign_up": "", + "sidebar": "Странична лента", + "sidebar_display_description": "Показване на връзка към изгледа в страничната лента", + "sign_out": "Отписване", + "sign_up": "Запиши се", "size": "Размер", - "skip_to_content": "", + "skip_to_content": "Премини към съдържанието", + "skip_to_folders": "Премини към папките", + "skip_to_tags": "Премини към етикетите", "slideshow": "Слайдшоу", - "slideshow_settings": "", - "sort_albums_by": "", + "slideshow_settings": "Настройки за слайдшоу", + "sort_albums_by": "Сортиране на албуми по...", + "sort_created": "Дата на създаване", + "sort_items": "Брой елементи", + "sort_modified": "Дата на промяна", + "sort_oldest": "Най-старата снимка", + "sort_recent": "Най-новата снимка", "sort_title": "Заглавие", "source": "Източник", "stack": "", - "stack_selected_photos": "", + "stack_duplicates": "Подреждане на дубликати", + "stack_selected_photos": "Подреждане на избрани снимки", "stacktrace": "", "start": "Старт", - "start_date": "", + "start_date": "Начална дата", "state": "", "status": "Статус", "stop_motion_photo": "", - "stop_photo_sharing": "Да спрете ли споделянето на вашите снимки?", + "stop_photo_sharing": "Да спра ли споделянето на вашите снимки?", "stop_photo_sharing_description": "{partner} вече няма достъп до вашите снимки.", "stop_sharing_photos_with_user": "Прекратете споделянето на снимки с този потребител", "storage": "Пространство на хранилището", @@ -1026,6 +1045,12 @@ "sunrise_on_the_beach": "Изгрев на плажа", "swap_merge_direction": "Размяна посоката на сливане", "sync": "Синхронизиране", + "tag": "Таг", + "tag_created": "Създаден етикет: {tag}", + "tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове", + "tag_not_found_question": "Не можете да намерите етикет? Създайте такъв тук", + "tag_updated": "Актуализиран етикет: {tag}", + "tags": "Етикет", "template": "Шаблон", "theme": "Тема", "theme_selection": "Избор на тема", @@ -1044,7 +1069,7 @@ "total_usage": "Общо използвано", "trash": "кошче", "trash_all": "Изхвърли всички", - "trash_count": "", + "trash_count": "Кошче {count, number}", "trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.", "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# day} other {# days}}.", "type": "Тип", @@ -1058,6 +1083,7 @@ "unlink_oauth": "", "unlinked_oauth_account": "", "unnamed_album": "Албум без име", + "unnamed_album_delete_confirmation": "Сигурни ли сте, че искате да изтриете този албум?", "unnamed_share": "Споделяне без име", "unsaved_change": "Незапазена промяна", "unselect_all": "Деселектирайте всички", @@ -1081,7 +1107,7 @@ "user_purchase_settings": "Покупка", "user_purchase_settings_description": "Управлявай покупката си", "user_role_set": "Задай {user} като {role}", - "user_usage_detail": "", + "user_usage_detail": "Подробности за използването на потребителя", "username": "Потребителско име", "users": "Потребители", "utilities": "Инструменти", @@ -1099,9 +1125,11 @@ "view_album": "Разгледай албума", "view_all": "Преглед на всички", "view_all_users": "Преглед на всички потребители", + "view_in_timeline": "Покажи във времева линия", "view_links": "Преглед на връзките", "view_next_asset": "Преглед на следващия файл", "view_previous_asset": "Преглед на предишния файл", + "view_stack": "Покажи в стек", "viewer": "", "waiting": "в изчакване", "warning": "Внимание", diff --git a/web/src/lib/i18n/bi.json b/web/src/lib/i18n/bi.json index 7d70cb8434661..aa5e3401c0ab2 100644 --- a/web/src/lib/i18n/bi.json +++ b/web/src/lib/i18n/bi.json @@ -172,7 +172,7 @@ "paths_validated_successfully": "", "quota_size_gib": "", "refreshing_all_libraries": "", - "removing_offline_files": "", + "removing_deleted_files": "", "repair_all": "", "repair_matched_items": "", "repaired_items": "", @@ -485,8 +485,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "", @@ -718,10 +718,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "rename": "", "repair": "", diff --git a/web/src/lib/i18n/ca.json b/web/src/lib/i18n/ca.json index 52880c322d034..4b58175c872c4 100644 --- a/web/src/lib/i18n/ca.json +++ b/web/src/lib/i18n/ca.json @@ -8,9 +8,9 @@ "active": "Actiu", "activity": "Activitat", "activity_changed": "L'activitat està {enabled, select, true {activada} other {desactivada}}", - "add": "Agregar", - "add_a_description": "Afegir una descripció", - "add_a_location": "Afegir una ubicació", + "add": "Afegir", + "add_a_description": "Afegiu una descripció", + "add_a_location": "Afegiu una ubicació", "add_a_name": "Afegir un nom", "add_a_title": "Afegir un títol", "add_exclusion_pattern": "Afegir un patró d'exclusió", @@ -41,6 +41,7 @@ "confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota", "confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.", "confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?", + "create_job": "Crear tasca", "crontab_guru": "Crontab Guru", "disable_login": "Deshabiliteu l'inici de sessió", "disabled": "Deshabilitat", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Resolució de la miniatura", "image_thumbnail_resolution_description": "S'empra per a veure grups de fotos (cronologia, vista d'àlbum, etc.). L'alta resolució pot preservar més detalls però triguen més en codificar-se, tenen fitxers més pesats i poden reduir la reactivitat de l'aplicació.", "job_concurrency": "{job} concurrència", + "job_created": "Tasca creada", "job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.", "job_settings": "Configuració de les tasques", "job_settings_description": "Gestiona la concurrència de tasques", @@ -129,16 +131,21 @@ "map_enable_description": "Habilita característiques del mapa", "map_gps_settings": "Configuració de mapa i GPS", "map_gps_settings_description": "Gestiona la configuració de mapa i GPS (Geocodificació inversa)", + "map_implications": "La funció mapa depèn del servei extern de tesel·les (tiles.immich.cloud)", "map_light_style": "Tema clar", "map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de geocodificació inversa", "map_reverse_geocoding": "Geocodificació inversa", "map_reverse_geocoding_enable_description": "Habilita la geocodificació inversa", "map_reverse_geocoding_settings": "Configuració de Geocodificació Inversa", - "map_settings": "Configuració del mapa i GPS", + "map_settings": "Mapa", "map_settings_description": "Gestiona la configuració del mapa", "map_style_description": "URL a un tema del mapa style.json", "metadata_extraction_job": "Extreure metadades", - "metadata_extraction_job_description": "Extreu l'informació de metadades de cada element, com per exemple el GPS i la resolució", + "metadata_extraction_job_description": "Extreu la informació de metadades de cada element, com per exemple el GPS i la resolució", + "metadata_faces_import_setting": "Activar la importació de cares", + "metadata_faces_import_setting_description": "Importar cares des de les metadades EXIF de les imatges i arxius auxiliars", + "metadata_settings": "Configuració de les metadades", + "metadata_settings_description": "Administrar la configuració de les metadades", "migration_job": "Migració", "migration_job_description": "Migra les miniatures d'elements i cares cap a la nova estructura de carpetes", "no_paths_added": "Cap camí afegit", @@ -147,7 +154,7 @@ "note_cannot_be_changed_later": "NOTA: Això és irreversible!", "note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada", "notification_email_from_address": "Des de l'adreça", - "notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server \"", + "notification_email_from_address_description": "Adreça de correu electrònic del remitent, per exemple: \"Immich Photo Server \"", "notification_email_host_description": "Amfitrió del servidor de correu electrònic (p.ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignora els errors de certificat", "notification_email_ignore_certificate_errors_description": "Ignora els errors de validació de certificat TLS (no recomanat)", @@ -173,7 +180,7 @@ "oauth_issuer_url": "URL de l'emissor", "oauth_mobile_redirect_uri": "URI de redirecció mòbil", "oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil", - "oauth_mobile_redirect_uri_override_description": "Habilita quan 'app.immich:/' és una URI de redirecció invàlida.", + "oauth_mobile_redirect_uri_override_description": "Habilita quan el proveïdor d'OAuth no permet una URI mòbil, com ara '{callback}'", "oauth_profile_signing_algorithm": "Algoritme de signatura del perfil", "oauth_profile_signing_algorithm_description": "Algoritme utilitzat per signar el perfil d’usuari.", "oauth_scope": "Abast", @@ -193,11 +200,12 @@ "password_settings": "Inici de sessió amb contrasenya", "password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya", "paths_validated_successfully": "Tots els camins han estat validats amb èxit", + "person_cleanup_job": "Neteja de persona", "quota_size_gib": "Tamany de la quota (GiB)", "refreshing_all_libraries": "Actualitzant totes les biblioteques", "registration": "Registre d'administrador", "registration_description": "Com que ets el primer usuari del sistema, seràs designat com a administrador i seràs responsable de les tasques administratives. També seràs l'encarregat de crear usuaris addicionals.", - "removing_offline_files": "Eliminant fitxers fora de línia", + "removing_deleted_files": "Eliminant fitxers fora de línia", "repair_all": "Reparar tot", "repair_matched_items": "Coincidència {count, plural, one {# element} other {# elements}}", "repaired_items": "Corregit {count, plural, one {# element} other {# elements}}", @@ -206,6 +214,7 @@ "reset_settings_to_recent_saved": "Restablir la configuració guardada més recent", "scanning_library_for_changed_files": "Escanejant llibreria per trobar fitxers modificats", "scanning_library_for_new_files": "Escanejant llibreria per trobar fitxers nous", + "search_jobs": "Tasques de cerca...", "send_welcome_email": "Enviar correu electrònic de benvinguda", "server_external_domain_settings": "Domini extern", "server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://", @@ -233,6 +242,7 @@ "storage_template_settings_description": "Gestiona l'estructura de les carpetes i el nom del fitxers dels elements pujats", "storage_template_user_label": "{label} és l'etiqueta d'emmagatzematge de l'usuari", "system_settings": "Configuració del sistema", + "tag_cleanup_job": "Neteja d'etiqueta", "theme_custom_css_settings": "CSS personalitzat", "theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.", "theme_settings": "Configuració del tema", @@ -278,7 +288,7 @@ "transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit", "transcoding_preferred_hardware_device_description": "S'aplica només a VAAPI i QSV. Estableix el node dri utilitzat per a la transcodificació de maquinari.", "transcoding_preset_preset": "Preestablert (-preset)", - "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a \"més ràpides\".", + "transcoding_preset_preset_description": "Velocitat de compressió. Els valors predefinits més lents produeixen fitxers més petits i augmenten la qualitat quan s'orienta a una taxa de bits determinada. VP9 ignora les velocitats superiors a 'més ràpides'.", "transcoding_reference_frames": "Fotogrames de referència", "transcoding_reference_frames_description": "El nombre de fotogrames a fer referència en comprimir un fotograma determinat. Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. 0 estableix aquest valor automàticament.", "transcoding_required_description": "Només vídeos que no tenen un format acceptat", @@ -307,6 +317,7 @@ "trash_settings_description": "Gestiona la configuració de la paperera", "untracked_files": "Fitxers sense seguiment", "untracked_files_description": "L'aplicació no fa un seguiment d'aquests fitxers. Poden ser el resultat de moviments fallits, càrregues interrompudes o deixades enrere a causa d'un error", + "user_cleanup_job": "Neteja d'usuari", "user_delete_delay": "El compte i els recursos de {user} es programaran per a la supressió permanent en {delay, plural, one {# dia} other {# dies}}.", "user_delete_delay_settings": "Retard de la supressió", "user_delete_delay_settings_description": "Nombre de dies després de la supressió per eliminar permanentment el compte i els elements d'un usuari. El treball de supressió d'usuaris s'executa a mitjanit per comprovar si hi ha usuaris preparats per eliminar. Els canvis en aquesta configuració s'avaluaran en la propera execució.", @@ -320,7 +331,8 @@ "user_settings": "Configuració d'usuaris", "user_settings_description": "Gestiona la configuració dels usuaris", "user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.", - "version_check_enabled_description": "Activa sol·licituds periòdiques a GitHub per comprovar si hi ha versions noves", + "version_check_enabled_description": "Activa la comprovació de la versió", + "version_check_implications": "La funció de comprovació de versions depèn de comunicacions periòdiques amb github.com", "version_check_settings": "Comprovació de versió", "version_check_settings_description": "Activa/desactiva la notificació de nova versió", "video_conversion_job": "Transcodificació de vídeos", @@ -336,7 +348,8 @@ "album_added": "Àlbum afegit", "album_added_notification_setting_description": "Rep una notificació per correu quan siguis afegit a un àlbum compartit", "album_cover_updated": "Portada de l'àlbum actualitzada", - "album_delete_confirmation": "N'esteu segur que voleu suprimir l'àlbum {album}?\nSi aquest àlbum és compartit, altres usuaris no hi podran accedir més.", + "album_delete_confirmation": "Esteu segur que voleu suprimir l'àlbum {album}?", + "album_delete_confirmation_description": "Si aquest àlbum es comparteix, els altres usuaris ja no podran accedir-hi.", "album_info_updated": "Informació de l'àlbum actualitzada", "album_leave": "Sortir de l'àlbum?", "album_leave_confirmation": "N'esteu segur que voleu sortir de {album}?", @@ -360,6 +373,7 @@ "allow_edits": "Permet editar", "allow_public_user_to_download": "Permet que l'usuari públic pugui descarregar", "allow_public_user_to_upload": "Permet que l'usuari públic pugui carregar", + "anti_clockwise": "En sentit antihorari", "api_key": "Clau API", "api_key_description": "Aquest valor només es mostrarà una vegada. Assegureu-vos de copiar-lo abans de tancar la finestra.", "api_key_empty": "El nom de la clau de l'API no pot estar buit", @@ -383,6 +397,7 @@ "asset_offline": "Element fora de línia", "asset_offline_description": "Aquest element està fora de línia. L'Immich no pot accedir a la seva ubicació. Si us plau, assegureu-vos que l'actiu està disponible i després torneu la llibreria.", "asset_skipped": "Saltat", + "asset_skipped_in_trash": "A la paperera", "asset_uploaded": "Carregat", "asset_uploading": "S'està carregant...", "assets": "Elements", @@ -440,9 +455,11 @@ "clear_all_recent_searches": "Esborra totes les cerques recents", "clear_message": "Neteja el missatge", "clear_value": "Neteja el valor", + "clockwise": "En sentit horari", "close": "Tanca", "collapse": "Tanca", "collapse_all": "Redueix-ho tot", + "color": "Color", "color_theme": "Tema de color", "comment_deleted": "Comentari esborrat", "comment_options": "Opcions de comentari", @@ -476,6 +493,8 @@ "create_new_person": "Crea una nova persona", "create_new_person_hint": "Assigna els elements seleccionats a una persona nova", "create_new_user": "Crea un usuari nou", + "create_tag": "Crear etiqueta", + "create_tag_description": "Crear una nova etiqueta. Per les etiquetes aniuades, escriu la ruta comperta de l'etiqueta, incloses les barres diagonals.", "create_user": "Crea un usuari", "created": "Creat", "current_device": "Dispositiu actual", @@ -499,6 +518,8 @@ "delete_library": "Suprimeix la llibreria", "delete_link": "Esborra l'enllaç", "delete_shared_link": "Odstranit sdílený odkaz", + "delete_tag": "Eliminar etiqueta", + "delete_tag_confirmation_prompt": "Estàs segur que vols eliminar l'etiqueta {tagName}?", "delete_user": "Suprimeix l'usuari", "deleted_shared_link": "Suprimeix l'enllaç compartit", "description": "Descripció", @@ -516,6 +537,8 @@ "do_not_show_again": "No tornis a mostrar aquest missatge", "done": "Fet", "download": "Descarregar", + "download_include_embedded_motion_videos": "Vídeos incrustats", + "download_include_embedded_motion_videos_description": "Incloure vídeos incrustats en fotografies en moviment com un arxiu separat", "download_settings": "Descarregar", "download_settings_description": "Gestioneu la configuració relacionada amb la descàrrega de recursos", "downloading": "Baixant", @@ -545,10 +568,15 @@ "edit_location": "Edita ubicació", "edit_name": "Edita el nom", "edit_people": "Edita la gent", + "edit_tag": "Editar etiqueta", "edit_title": "Edita títol", "edit_user": "Edita l'usuari", "edited": "Editat", "editor": "Editor", + "editor_close_without_save_prompt": "No es desaran els canvis", + "editor_close_without_save_title": "Tancar l'editor?", + "editor_crop_tool_h2_aspect_ratios": "Relació d'aspecte", + "editor_crop_tool_h2_rotation": "Rotació", "email": "Correu electrònic", "empty": "", "empty_album": "", @@ -638,6 +666,7 @@ "unable_to_get_comments_number": "No es pot obtenir el nombre de comentaris", "unable_to_get_shared_link": "No s'ha pogut obtenir l'enllaç compartit", "unable_to_hide_person": "No es pot amagar la persona", + "unable_to_link_motion_video": "No es pot enllaçar el vídeo en moviment", "unable_to_link_oauth_account": "No es pot enllaçar el compte OAuth", "unable_to_load_album": "No es pot carregar l'àlbum", "unable_to_load_asset_activity": "No es pot carregar l'activitat dels recursos", @@ -654,8 +683,8 @@ "unable_to_remove_api_key": "No es pot eliminar la clau de l'API", "unable_to_remove_assets_from_shared_link": "No es poden eliminar recursos de l'enllaç compartit", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "No es poden eliminar els fitxers fora de línia", "unable_to_remove_library": "No es pot eliminar la biblioteca", - "unable_to_remove_offline_files": "No es poden eliminar els fitxers fora de línia", "unable_to_remove_partner": "No es pot eliminar company/a", "unable_to_remove_reaction": "No es pot eliminar la reacció", "unable_to_remove_user": "", @@ -678,6 +707,7 @@ "unable_to_submit_job": "No es pot enviar la tasca", "unable_to_trash_asset": "No es pot eliminar el recurs a la paperera", "unable_to_unlink_account": "No es pot desenllaçar el compte", + "unable_to_unlink_motion_video": "No es pot desvincular el vídeo en moviment", "unable_to_update_album_cover": "No es pot actualitzar la portada de l'àlbum", "unable_to_update_album_info": "No es pot actualitzar la informació de l'àlbum", "unable_to_update_library": "No es pot actualitzar la biblioteca", @@ -698,6 +728,7 @@ "expired": "Caducat", "expires_date": "Caduca el {date}", "explore": "Explorar", + "explorer": "Explorador", "export": "Exporta", "export_as_json": "Exportar com a JSON", "extension": "Extensió", @@ -711,6 +742,8 @@ "feature": "", "feature_photo_updated": "Foto destacada actualitzada", "featurecollection": "", + "features": "Característiques", + "features_setting_description": "Administrar les funcions de l'aplicació", "file_name": "Nom de l'arxiu", "file_name_or_extension": "Nom de l'arxiu o extensió", "filename": "Nom del fitxer", @@ -719,6 +752,8 @@ "filter_people": "Filtra persones", "find_them_fast": "Trobeu-los ràpidament pel nom amb la cerca", "fix_incorrect_match": "Corregiu la coincidència incorrecta", + "folders": "Carpetes", + "folders_feature_description": "Explorar la vista de carpetes per les fotos i vídeos del sistema d'arxius", "force_re-scan_library_files": "Força a tornar a escanejar tots els fitxers de la biblioteca", "forward": "Endavant", "general": "General", @@ -803,6 +838,7 @@ "license_trial_info_3": "{accountAge, plural, one {# dia} other {# dies}}", "light": "Llum", "like_deleted": "M'agrada suprimit", + "link_motion_video": "Enllaçar vídeo en moviment", "link_options": "Opcions d'enllaç", "link_to_oauth": "Enllaç a OAuth", "linked_oauth_account": "Compte OAuth enllaçat", @@ -895,13 +931,15 @@ "offline_paths_description": "Aquests resultats poden ser deguts a la supressió manual de fitxers que no formen part d'una biblioteca externa.", "ok": "D'acord", "oldest_first": "El més vell primer", - "onboarding": "Onboarding", + "onboarding": "Incorporació", + "onboarding_privacy_description": "Les següents funcions (opcionals) depenen de serveis externs i poden desactivarse en qualsevol moment de dels ajustos.", "onboarding_theme_description": "Trieu un tema de color per a la vostra instància. Podeu canviar-ho més endavant a la vostra configuració.", "onboarding_welcome_description": "Configurem la vostra instància amb alguns paràmetres habituals.", "onboarding_welcome_user": "Benvingut, {user}", "online": "En línia", "only_favorites": "Només preferits", "only_refreshes_modified_files": "Només actualitza els fitxers modificats", + "open_in_map_view": "Obrir a la vista del mapa", "open_in_openstreetmap": "Obre a OpenStreetMap", "open_the_search_filters": "Obriu els filtres de cerca", "options": "Opcions", @@ -936,6 +974,7 @@ "pending": "Pendent", "people": "Persones", "people_edits_count": "{count, plural, one {# persona editada} other {# persones editades}}", + "people_feature_description": "Explorar fotos i vídeos agrupades per persona", "people_sidebar_description": "Mostrar un enllaç a Persones a la barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Avís d'eliminació permanent", @@ -967,6 +1006,7 @@ "previous_memory": "Memòria anterior", "previous_or_next_photo": "Foto anterior o següent", "primary": "Primària", + "privacy": "Privacitat", "profile_image_of_user": "Imatge de perfil de {user}", "profile_picture_set": "Imatge de perfil configurada.", "public_album": "Àlbum públic", @@ -1004,6 +1044,10 @@ "purchase_server_title": "Servidor", "purchase_settings_server_activated": "La clau de producte del servidor la gestiona l'administrador", "range": "", + "rating": "Valoració", + "rating_clear": "Esborrar valoració", + "rating_count": "{count, plural, one {# estrella} other {# estrelles}}", + "rating_description": "Mostrar la valoració EXIF al panell d'informació", "raw": "", "reaction_options": "Opcions de reacció", "read_changelog": "Llegeix el registre de canvis", @@ -1027,15 +1071,16 @@ "remove_assets_shared_link_confirmation": "Esteu segur que voleu eliminar {count, plural, one {# recurs} other {# recursos}} d'aquest enllaç compartit?", "remove_assets_title": "Eliminar els elements?", "remove_custom_date_range": "Elimina l'interval de dates personalitzat", + "remove_deleted_assets": "Suprimeix fitxers fora de línia", "remove_from_album": "Treu de l'àlbum", "remove_from_favorites": "Eliminar dels preferits", "remove_from_shared_link": "Eliminar de l'enllaç compartit", - "remove_offline_files": "Suprimeix fitxers fora de línia", "remove_user": "Eliminar l'usuari", "removed_api_key": "Eliminada la clau d'API: {name}", "removed_from_archive": "Eliminat de l'arxiu", "removed_from_favorites": "Eliminat dels preferits", "removed_from_favorites_count": "{count, plural, other {# eliminats}} dels preferits", + "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# actiu} other {# actius}}", "rename": "Canviar nom", "repair": "Reparació", "repair_no_results_message": "Els fitxers sense seguiment i que falten es mostraran aquí", @@ -1074,7 +1119,7 @@ "search_albums": "Buscar àlbums", "search_by_context": "Buscar per context", "search_by_filename": "Cerca per nom de fitxer o extensió", - "search_by_filename_example": "i.e. IMG_1234.JPG or PNG", + "search_by_filename_example": "per exemple IMG_1234.JPG o PNG", "search_camera_make": "Buscar per fabricant de càmara...", "search_camera_model": "Buscar per model de càmera...", "search_city": "Buscar per ciutat...", @@ -1082,9 +1127,12 @@ "search_for_existing_person": "Busca una persona existent", "search_no_people": "Cap persona", "search_no_people_named": "Cap persona anomenada \"{name}\"", + "search_options": "Opcions de cerca", "search_people": "Buscar persones", "search_places": "Buscar llocs", + "search_settings": "Configuració de cerca", "search_state": "Buscar per regió...", + "search_tags": "Cercant etiquetes...", "search_timezone": "Buscar per fus horari...", "search_type": "Buscar per tipus", "search_your_photos": "Cerca les teves fotos", @@ -1126,6 +1174,7 @@ "shared_by_user": "Compartit per {user}", "shared_by_you": "Compartit per tu", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opcions d'enllaços compartits", "shared_links": "Enllaços compartits", "shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}", "shared_with_partner": "Compartit amb {partner}", @@ -1134,6 +1183,7 @@ "sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral", "shift_to_permanent_delete": "premeu ⇧ per suprimir el recurs permanentment", "show_album_options": "Mostra les opcions d'àlbum", + "show_albums": "Mostrar àlbums", "show_all_people": "Veure totes les persones", "show_and_hide_people": "Mostra i amaga persones", "show_file_location": "Mostra l'ubicació del fitxer", @@ -1151,10 +1201,14 @@ "show_supporter_badge": "Insígnia de contribuent", "show_supporter_badge_description": "Mostra una insígnia de contributor", "shuffle": "Mescla", + "sidebar": "Barra lateral", + "sidebar_display_description": "Mostra un enllaç a la vista a la barra lateral", "sign_out": "Tanca sessió", "sign_up": "Registrar-se", "size": "Mida", "skip_to_content": "Salta al contingut", + "skip_to_folders": "Anar a carpetes", + "skip_to_tags": "Anar a etiquetes", "slideshow": "Diapositives", "slideshow_settings": "Configuració de diapositives", "sort_albums_by": "Ordena àlbums per...", @@ -1166,6 +1220,8 @@ "sort_title": "Títol", "source": "Font", "stack": "Apila", + "stack_duplicates": "Aplicar duplicats", + "stack_select_one_photo": "Selecciona una imatge principal per la pila", "stack_selected_photos": "Apila les fotos seleccionades", "stacked_assets_count": "Apilats {count, plural, one {# element} other {# elements}}", "stacktrace": "Traça de pila", @@ -1185,6 +1241,14 @@ "sunrise_on_the_beach": "Albada a la platja", "swap_merge_direction": "Canvia la direcció d'unió", "sync": "Sincronitza", + "tag": "Etiqueta", + "tag_assets": "Etiquetar actius", + "tag_created": "Etiqueta creada: {tag}", + "tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques", + "tag_not_found_question": "No trobeu una etiqueta? Creeu-ne una aquí", + "tag_updated": "Etiqueta actualizada: {tag}", + "tagged_assets": "{count, plural, one {#Etiquetat} other {#Etiquetats}} {count, plural, one {# actiu} other {# actius}}", + "tags": "Etiquetes", "template": "Plantilla", "theme": "Tema", "theme_selection": "Selecció de tema", @@ -1196,9 +1260,10 @@ "to_change_password": "Canviar la contrasenya", "to_favorite": "Prefereix", "to_login": "Iniciar sessió", + "to_parent": "Anar als pares", "to_trash": "Paperera", "toggle_settings": "Canvia configuració", - "toggle_theme": "Canvia tema", + "toggle_theme": "Alternar tema", "toggle_visibility": "Canvia visibilitat", "total_usage": "Ús total", "trash": "Paperera", @@ -1217,9 +1282,11 @@ "unknown_album": "Àlbum desconegut", "unknown_year": "Any desconegut", "unlimited": "Il·limitat", + "unlink_motion_video": "Desvincular vídeo en moviment", "unlink_oauth": "Desvincula OAuth", "unlinked_oauth_account": "Compte Oauth desvinculat", "unnamed_album": "Àlbum sense nom", + "unnamed_album_delete_confirmation": "Segur que voleu esborrar aquest àlbum?", "unnamed_share": "Compartit sense nom", "unsaved_change": "Canvi no desat", "unselect_all": "Deselecciona-ho tot", @@ -1267,6 +1334,7 @@ "view_album": "Veure l'àlbum", "view_all": "Veure tot", "view_all_users": "Mostra tot els usuaris", + "view_in_timeline": "Mostrar a la línia de temps", "view_links": "Mostra enllaços", "view_next_asset": "Mostra el següent element", "view_previous_asset": "Mostra l'element anterior", diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index ceb692a736685..cc119dbd6dc02 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -41,6 +41,7 @@ "confirm_email_below": "Pro potvrzení zadejte níže \"{email}\"", "confirm_reprocess_all_faces": "Opravdu chcete znovu zpracovat všechny obličeje? Tím se vymažou i pojmenované osoby.", "confirm_user_password_reset": "Opravdu chcete obnovit heslo uživatele {user}?", + "create_job": "Vytvořit úlohu", "crontab_guru": "Crontab Guru", "disable_login": "Zakázat přihlášení", "disabled": "Zakázáno", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Rozlišení miniatur", "image_thumbnail_resolution_description": "Používá se při prohlížení skupin fotografií (hlavní časová osa, zobrazení alba atd.). Vyšší rozlišení může zachovat více detailů, ale trvá déle, než se zakóduje, má větší velikost souboru a může snížit odezvu aplikace.", "job_concurrency": "Souběžnost {job}", + "job_created": "Úloha vytvořena", "job_not_concurrency_safe": "Tato úloha není bezpečená pro souběh.", "job_settings": "Úlohy", "job_settings_description": "Správa souběžnosti úloh", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "UPOZORNĚNÍ: Toto nelze později změnit!", "note_unlimited_quota": "Upozornění: Pro neomezenou kvótu zadejte 0", "notification_email_from_address": "Adresa Od", - "notification_email_from_address_description": "E-mailová adresa odesílatele, např.: \"Immich Photo Server \"", + "notification_email_from_address_description": "E-mailová adresa odesílatele, např.: \"Immich Photo Server \"", "notification_email_host_description": "Adresa e-mailového serveru (např. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorovat chyby certifikátů", "notification_email_ignore_certificate_errors_description": "Ignorovat chyby ověření certifikátu TLS (nedoporučuje se)", @@ -198,11 +200,12 @@ "password_settings": "Přihlášení heslem", "password_settings_description": "Správa nastavení přihlašování pomocí hesla", "paths_validated_successfully": "Všechny cesty byly úspěšně ověřeny", + "person_cleanup_job": "Promazání osob", "quota_size_gib": "Velikost kvóty (GiB)", "refreshing_all_libraries": "Obnovení všech knihoven", "registration": "Registrace správce", "registration_description": "Vzhledem k tomu, že jste prvním uživatelem v systému, budete přiřazen jako správce a budete zodpovědný za úkoly správy a další uživatelé budou vytvořeni vámi.", - "removing_offline_files": "Odstranění offline souborů", + "removing_deleted_files": "Odstranění offline souborů", "repair_all": "Opravit vše", "repair_matched_items": "Shoda {count, plural, one {# položky} other {# položek}}", "repaired_items": "{count, plural, one {Opravena # položka} few {Opraveny # položky} other {Opraveno # položek}}", @@ -211,6 +214,7 @@ "reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení", "scanning_library_for_changed_files": "Hledání změněných souborů v knihovně", "scanning_library_for_new_files": "Hledání nových souborů v knihovně", + "search_jobs": "Hledat úlohy...", "send_welcome_email": "Odeslat uvítací e-mail", "server_external_domain_settings": "Externí doména", "server_external_domain_settings_description": "Doména pro veřejně sdílené odkazy, včetně http(s)://", @@ -238,6 +242,7 @@ "storage_template_settings_description": "Správa struktury složek a názvů nahraných souborů", "storage_template_user_label": "{label} je štítek úložiště uživatele", "system_settings": "Systémová nastavení", + "tag_cleanup_job": "Promazání značek", "theme_custom_css_settings": "Vlastní CSS", "theme_custom_css_settings_description": "Kaskádové styly umožňují přizpůsobit design aplikace Immich.", "theme_settings": "Motivy", @@ -312,6 +317,7 @@ "trash_settings_description": "Správa nastavení koše", "untracked_files": "Neznámé soubory", "untracked_files_description": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě", + "user_cleanup_job": "Promazání uživatelů", "user_delete_delay": "Účet a položky uživatele {user} budou trvale smazány za {delay, plural, one {# den} few {# dny} other {# dní}}.", "user_delete_delay_settings": "Odložení odstranění", "user_delete_delay_settings_description": "Počet dní po odstranění, po kterých bude odstraněn účet a položky uživatele. Úloha odstraňování uživatelů se spouští o půlnoci a kontroluje uživatele, kteří jsou připraveni k odstranění. Změny tohoto nastavení se vyhodnotí při dalším spuštění.", @@ -391,6 +397,7 @@ "asset_offline": "Offline položka", "asset_offline_description": "Tato položka je offline. Immich nemá přístup k jejímu umístění. Zkontrolujte, zda je položka dostupná, a poté knihovnu znovu prohledejte.", "asset_skipped": "Přeskočeno", + "asset_skipped_in_trash": "V koši", "asset_uploaded": "Nahráno", "asset_uploading": "Nahrávání...", "assets": "Položky", @@ -660,6 +667,7 @@ "unable_to_get_comments_number": "Nelze načíst počet komentářů", "unable_to_get_shared_link": "Nepodařilo se získat sdílený odkaz", "unable_to_hide_person": "Nelze skrýt osobu", + "unable_to_link_motion_video": "Nelze připojit pohyblivé video", "unable_to_link_oauth_account": "Nelze propojit OAuth účet", "unable_to_load_album": "Nelze načíst album", "unable_to_load_asset_activity": "Nelze načíst aktivitu položky", @@ -676,8 +684,8 @@ "unable_to_remove_api_key": "Nelze odstranit API klíč", "unable_to_remove_assets_from_shared_link": "Nelze odstranit položky ze sdíleného odkazu", "unable_to_remove_comment": "Nelze odstranit komentář", + "unable_to_remove_deleted_assets": "Nelze odstranit offline soubory", "unable_to_remove_library": "Nelze odstranit knihovnu", - "unable_to_remove_offline_files": "Nelze odstranit offline soubory", "unable_to_remove_partner": "Nelze odebrat partnera", "unable_to_remove_reaction": "Nelze odstranit reakci", "unable_to_remove_user": "Nelze odebrat uživatele", @@ -700,6 +708,7 @@ "unable_to_submit_job": "Nelze odeslat úlohu", "unable_to_trash_asset": "Nelze vyhodit položku do koše", "unable_to_unlink_account": "Nelze zrušit propojení účtu", + "unable_to_unlink_motion_video": "Nelze odpojit pohyblivé video", "unable_to_update_album_cover": "Nelze aktualizovat obal alba", "unable_to_update_album_info": "Nelze aktualizovat informace o albu", "unable_to_update_library": "Nelze aktualizovat knihovnu", @@ -845,6 +854,7 @@ "license_trial_info_4": "Zvažte prosím zakoupení licence na podporu dalšího rozvoje služby", "light": "Světlý", "like_deleted": "Lajk smazán", + "link_motion_video": "Připojit pohyblivé video", "link_options": "Možnosti odkazu", "link_to_oauth": "Propojit s OAuth", "linked_oauth_account": "Propojený OAuth účet", @@ -1079,10 +1089,10 @@ "remove_assets_shared_link_confirmation": "Opravdu chcete ze sdíleného odkazu odstranit {count, plural, one {# položku} few {# položky} other {# položek}}?", "remove_assets_title": "Odstranit položky?", "remove_custom_date_range": "Odstranit vlastní rozsah datumů", + "remove_deleted_assets": "Odstranit offline soubory", "remove_from_album": "Odstranit z alba", "remove_from_favorites": "Odstranit z oblíbených", "remove_from_shared_link": "Odstranit ze sdíleného odkazu", - "remove_offline_files": "Odstranit offline soubory", "remove_user": "Odebrat uživatele", "removed_api_key": "Odstraněn API klíč: {name}", "removed_from_archive": "Odstraněno z archivu", @@ -1135,8 +1145,10 @@ "search_for_existing_person": "Vyhledat existující osobu", "search_no_people": "Žádní lidé", "search_no_people_named": "Žádní lidé se jménem \"{name}\"", + "search_options": "Možnosti vyhledávání", "search_people": "Vyhledat lidi", "search_places": "Vyhledat místa", + "search_settings": "Hledat nastavení", "search_state": "Vyhledat stát...", "search_tags": "Vyhledávat značky...", "search_timezone": "Vyhledat časové pásmo...", @@ -1213,6 +1225,8 @@ "sign_up": "Zaregistrovat se", "size": "Velikost", "skip_to_content": "Přejít na obsah", + "skip_to_folders": "Přeskočit na složky", + "skip_to_tags": "Přeskočit na značky", "slideshow": "Prezentace", "slideshow_settings": "Nastavení prezentace", "sort_albums_by": "Seřadit alba podle...", @@ -1287,6 +1301,7 @@ "unknown_album": "Neznámé album", "unknown_year": "Neznámý rok", "unlimited": "Neomezeně", + "unlink_motion_video": "Odpojit pohyblivé video", "unlink_oauth": "Zrušit OAuth propojení", "unlinked_oauth_account": "OAuth účet odpojen", "unnamed_album": "Nepojmenované album", @@ -1311,7 +1326,7 @@ "upload_status_uploaded": "Nahráno", "upload_success": "Nahrání proběhlo úspěšně, obnovením stránky se zobrazí nově nahrané položky.", "url": "URL", - "usage": "Použití", + "usage": "Využití", "use_custom_date_range": "Použít vlastní rozsah dat", "user": "Uživatel", "user_id": "ID uživatele", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index eb9d99d074c10..480efde2e3571 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -140,6 +140,10 @@ "map_style_description": "URL til en style.json for et korttema", "metadata_extraction_job": "Udtræk metadata", "metadata_extraction_job_description": "Udtræk metadataoplysninger fra hvert Billede/Video, såsom GPS og opløsning", + "metadata_faces_import_setting": "Aktivér for at importere ansigter", + "metadata_faces_import_setting_description": "Importerer ansigter fra billed EXIF-data og forbandt filer", + "metadata_settings": "Metadatainstillinger", + "metadata_settings_description": "Håndtér metadataindstillinger", "migration_job": "Migrering", "migration_job_description": "Migrér miniaturebilleder for aktiver og ansigter til den seneste mappestruktur", "no_paths_added": "Ingen stier tilføjet", @@ -148,7 +152,7 @@ "note_cannot_be_changed_later": "BEMÆRK: Dette kan ikke ændres senere!", "note_unlimited_quota": "Bemærk: Indsæt 0 for uendelig kvote", "notification_email_from_address": "Fra adressse", - "notification_email_from_address_description": "Afsenderemailadresse, for eksempel: \"Immich Billedserver \"", + "notification_email_from_address_description": "Afsenderemailadresse, for eksempel: \"Immich Billedserver \"", "notification_email_host_description": "Host af emailserver (fx smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorér certifikatfejl", "notification_email_ignore_certificate_errors_description": "Ignorér TLS-certifikatgodkendelsesfejl (ikke anbefalet)", @@ -198,7 +202,7 @@ "refreshing_all_libraries": "Opdaterer alle biblioteker", "registration": "Administratorregistrering", "registration_description": "Da du er den første bruger i systemet, får du tildelt rollen som administrator og ansvar for administration og oprettelsen af nye brugere.", - "removing_offline_files": "Fjerner offline-filer", + "removing_deleted_files": "Fjerner offline-filer", "repair_all": "Reparér alle", "repair_matched_items": "Har parret {count, plural, one {# element} other {# elementer}}", "repaired_items": "Reparerede {count, plural, one {# element} other {# elementer}}", @@ -347,15 +351,25 @@ "album_options": "Albumindstillinger", "album_remove_user": "Fjern bruger?", "album_remove_user_confirmation": "Er du sikker på at du vil fjerne {user}?", + "album_share_no_users": "Det ser ud til at du har delt denne album med alle brugere, eller du har ikke nogen brugere til at dele med.", "album_updated": "Album opdateret", "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", + "album_user_left": "Forlod {album}", + "album_user_removed": "Fjernede {user}", "albums": "Albummer", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albummer}}", "all": "Alt", + "all_albums": "Alle albummer", "all_people": "Alle personer", + "all_videos": "Alle videoer", "allow_dark_mode": "Tillad mørk tilstand", "allow_edits": "Tillad redigeringer", + "allow_public_user_to_download": "Tillad offentlige brugere til at hente", + "allow_public_user_to_upload": "Tillad offentlige brugere til at uploade", + "anti_clockwise": "Mod uret", "api_key": "API-nøgle", + "api_key_description": "Denne værdi vises kun én gang. Venligst kopiér den før du lukker vinduet.", + "api_key_empty": "Din API-nøgle-navn burde ikke være tom", "api_keys": "API-nøgler", "app_settings": "Appindstillinger", "appears_in": "Optræder i", @@ -549,8 +563,8 @@ "unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album", "unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Kunne ikke fjerne offlinefiler", "unable_to_remove_library": "Ikke i stand til at fjerne bibliotek", - "unable_to_remove_offline_files": "Kunne ikke fjerne offlinefiler", "unable_to_remove_partner": "Ikke i stand til at fjerne partner", "unable_to_remove_reaction": "Ikke i stand til at reaktion", "unable_to_remove_user": "", @@ -797,10 +811,10 @@ "refreshed": "Opdateret", "refreshes_every_file": "Opdaterer alle filer", "remove": "Fjern", + "remove_deleted_assets": "Fjern fra offlinefiler", "remove_from_album": "Fjern fra album", "remove_from_favorites": "Fjern fra favoritter", "remove_from_shared_link": "Fjern fra delt link", - "remove_offline_files": "Fjern fra offlinefiler", "removed_api_key": "Fjernede API-nøgle: {name}", "rename": "Omdøb", "repair": "Reparér", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index ce51f11b66882..076ca030f2cfa 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -41,6 +41,7 @@ "confirm_email_below": "Bestätige, indem du \"{email}\" unten eingibst", "confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.", "confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?", + "create_job": "Job erstellen", "crontab_guru": "Crontab Guru", "disable_login": "Login deaktvieren", "disabled": "Deaktiviert", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Miniaturansichts-Auflösung", "image_thumbnail_resolution_description": "Dies wird bei der Anzeige von Bildergruppen („Zeitleiste“, „Albumansicht“ usw.) verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.", "job_concurrency": "{job} - (Anzahl gleichzeitiger Prozesse)", + "job_created": "Job erstellt", "job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.", "job_settings": "Job-Einstellungen", "job_settings_description": "Gleichzeitige Job-Prozessen verwalten", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "HINWEIS: Dies kann später nicht mehr geändert werden!", "note_unlimited_quota": "Hinweis: 0 eingeben für unlimitiertes Kontingent", "notification_email_from_address": "Von", - "notification_email_from_address_description": "E-Mail-Adresse des Senders, zum Beispiel: \"Immich Photo Server \"", + "notification_email_from_address_description": "E-Mail-Adresse des Senders, zum Beispiel: \"Immich Photo Server \"", "notification_email_host_description": "Host des E-Mail-Servers (z.B. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoriere Zertifikats-Fehler", "notification_email_ignore_certificate_errors_description": "TLS-Zertifikatsvalidierungsfehler ignorieren (nicht empfohlen)", @@ -198,11 +200,12 @@ "password_settings": "Passwort Login", "password_settings_description": "Passwort-Anmeldeeinstellungen verwalten", "paths_validated_successfully": "Alle Pfade wurden erfolgreich validiert", + "person_cleanup_job": "Personen aufräumen", "quota_size_gib": "Kontingent (GiB)", "refreshing_all_libraries": "Alle Bibliotheken aktualisieren", "registration": "Admin-Registrierung", "registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.", - "removing_offline_files": "Offline-Dateien entfernen", + "removing_deleted_files": "Offline-Dateien entfernen", "repair_all": "Alle reparieren", "repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden", "repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert", @@ -211,6 +214,7 @@ "reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurücksetzen", "scanning_library_for_changed_files": "Untersuche Bibliothek auf geänderte Dateien", "scanning_library_for_new_files": "Untersuche Bibliothek auf neue Dateien", + "search_jobs": "Jobs suchen...", "send_welcome_email": "Begrüssungsmail senden", "server_external_domain_settings": "Externe Domain", "server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://", @@ -238,6 +242,7 @@ "storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten", "storage_template_user_label": "{label} is das Speicher-Label des Benutzers", "system_settings": "Systemeinstellungen", + "tag_cleanup_job": "Tags aufräumen", "theme_custom_css_settings": "Benutzerdefiniertes CSS", "theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.", "theme_settings": "Theme-Einstellungen", @@ -312,6 +317,7 @@ "trash_settings_description": "Papierkorb-Einstellungen verwalten", "untracked_files": "Unverfolgte Dateien", "untracked_files_description": "Diese Dateien werden nicht von der Application getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein", + "user_cleanup_job": "Benutzer aufräumen", "user_delete_delay": "Das Konto und die Dateien von {user} werden in {delay, plural, one {einem Tag} other {# Tagen}} für eine permanente Löschung geplant.", "user_delete_delay_settings": "Verzögerung für das Löschen von Benutzern", "user_delete_delay_settings_description": "Gibt die Anzahl der Tage bis zur endgültigen Löschung eines Kontos und seiner Dateien an. Der Benutzerlöschauftrag wird täglich um Mitternacht ausgeführt, um zu überprüfen, ob Nutzer zur Löschung bereit sind. Änderungen an dieser Einstellung werden erst bei der nächsten Ausführung berücksichtigt.", @@ -391,6 +397,7 @@ "asset_offline": "Datei offline", "asset_offline_description": "Diese Datei ist nicht erreichbar. Immich kann nicht auf ihren Speicherort zugreifen. Bitte stelle sicher, dass die Datei verfügbar ist und scanne die Bibliothek erneut.", "asset_skipped": "Übersprungen", + "asset_skipped_in_trash": "Im Papierkorb", "asset_uploaded": "Hochgeladen", "asset_uploading": "Hochladen...", "assets": "Dateien", @@ -660,6 +667,7 @@ "unable_to_get_comments_number": "Anzahl der Kommentare konnte nicht abgerufen werden", "unable_to_get_shared_link": "Fehler beim Abrufen des Freigabelinks", "unable_to_hide_person": "Person kann nicht versteckt werden", + "unable_to_link_motion_video": "Bewegungsvideo kann nicht verlinkt werden", "unable_to_link_oauth_account": "OAuth-Konto kann nicht verknüpft werden", "unable_to_load_album": "Album kann nicht geladen werden", "unable_to_load_asset_activity": "Foto-Aktivität konnte nicht geladen werden", @@ -676,8 +684,8 @@ "unable_to_remove_api_key": "API-Schlüssel konnte nicht entfernt werden", "unable_to_remove_assets_from_shared_link": "Dateien konnten nicht von geteiltem Link entfernt werden", "unable_to_remove_comment": "Kommentar kann nicht entfernt werden", + "unable_to_remove_deleted_assets": "Offline-Dateien konnten nicht entfernt werden", "unable_to_remove_library": "Bibliothek kann nicht entfernt werden", - "unable_to_remove_offline_files": "Offline-Dateien konnten nicht entfernt werden", "unable_to_remove_partner": "Partner kann nicht entfernt werden", "unable_to_remove_reaction": "Reaktion kann nicht entfernt werden", "unable_to_remove_user": "Benutzer kann nicht entfernt werden", @@ -700,6 +708,7 @@ "unable_to_submit_job": "Auftrag konnte nicht übermittelt werden", "unable_to_trash_asset": "Objekte konnten nicht gelöscht werden", "unable_to_unlink_account": "Die Verknüpfung des Kontos kann nicht aufgehoben werden", + "unable_to_unlink_motion_video": "Verlinkung zum Bewegungsvideo kann nicht aufgehoben werden", "unable_to_update_album_cover": "Album-Cover konnte nicht aktualisiert werden", "unable_to_update_album_info": "Album-Info konnte nicht aktualisiert werden", "unable_to_update_library": "Die Bibliothek konnte nicht aktualisiert werden", @@ -845,6 +854,7 @@ "license_trial_info_4": "Bitte erwäge den Kauf einer Lizenz, um die kontinuierliche Weiterentwicklung des Dienstes zu unterstützen", "light": "Hell", "like_deleted": "Like gelöscht", + "link_motion_video": "Link Bewegungsvideo", "link_options": "Link-Optionen", "link_to_oauth": "Link zu OAuth", "linked_oauth_account": "Verknüpftes OAuth-Konto", @@ -1078,10 +1088,10 @@ "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?", "remove_assets_title": "Dateien entfernen?", "remove_custom_date_range": "Benutzerdefinierten Datumsbereich entfernen", + "remove_deleted_assets": "Offline-Dateien entfernen", "remove_from_album": "Aus Album entfernen", "remove_from_favorites": "Aus Favoriten entfernen", "remove_from_shared_link": "Aus geteilten Link entfernen", - "remove_offline_files": "Offline-Dateien entfernen", "remove_user": "Nutzer entfernen", "removed_api_key": "API-Schlüssel {name} wurde entfernt", "removed_from_archive": "Aus dem Archiv entfernt", @@ -1134,8 +1144,10 @@ "search_for_existing_person": "Suche nach vorhandener Person", "search_no_people": "Keine Personen", "search_no_people_named": "Keine Person mit dem Namen \"{name}\"", + "search_options": "Suchoptionen", "search_people": "Suche nach Personen", "search_places": "Suche nach Orten", + "search_settings": "Suche nach Einstellungen", "search_state": "Suche nach Bundesland / Provinz...", "search_tags": "Sache nach Tags...", "search_timezone": "Suche nach Zeitzone...", @@ -1166,7 +1178,7 @@ "server_stats": "Server-Statistiken", "server_version": "Server-Version", "set": "Speichern", - "set_as_album_cover": "Als Albumcover gesetzt", + "set_as_album_cover": "Als Albumcover festlegen", "set_as_profile_picture": "Als Profilbild festlegen", "set_date_of_birth": "Geburtsdatum festlegen", "set_profile_picture": "Profilbild einstellen", @@ -1212,6 +1224,8 @@ "sign_up": "Registrieren", "size": "Größe", "skip_to_content": "Zum Inhalt springen", + "skip_to_folders": "Springe zu Ordnern", + "skip_to_tags": "Springe zu Tags", "slideshow": "Diashow", "slideshow_settings": "Diashow Einstellungen", "sort_albums_by": "Alben sortieren nach...", @@ -1286,6 +1300,7 @@ "unknown_album": "Unbekanntes Album", "unknown_year": "Unbekanntes Jahr", "unlimited": "Unlimitiert", + "unlink_motion_video": "Verlinkung zum Bewegungsvideo aufheben", "unlink_oauth": "OAuth entfernen", "unlinked_oauth_account": "Nicht verknüpftes OAuth-Konto", "unnamed_album": "Unbenanntes Album", diff --git a/web/src/lib/i18n/el.json b/web/src/lib/i18n/el.json index 5ac37616ca01a..5f88772ea7d5f 100644 --- a/web/src/lib/i18n/el.json +++ b/web/src/lib/i18n/el.json @@ -12,6 +12,8 @@ "add_a_location": "Προσθήκη μιας τοποθεσίας", "add_a_name": "Προσθήκη Ονόματος", "add_a_title": "Προσθήκη τίτλου", + "add_exclusion_pattern": "Προσθήκη προτύπου αποκλεισμού", + "add_import_path": "Προσθήκη διαδρομής εισαγωγής", "add_location": "Προσθήκη τοποθεσίας", "add_more_users": "Προσθήκη επιπλέον χρηστών", "add_partner": "Προσθήκη συνεργάτη", @@ -24,16 +26,24 @@ "added_to_favorites": "Προστέθηκε στα αγαπημένα", "added_to_favorites_count": "Προστέθηκαν {count, number} στα αγαπημένα", "admin": { + "add_exclusion_pattern_description": "Προσθέστε πρότυπα αποκλεισμού. Υποστηρίζεται η επιλογή πολλών με *, **, και ?. Για να αγνοηθούν όλα τα αρχεία σε έναν φάκελο με το όνομα \"Raw\", χρησιμοποιήστε \"**/Raw/**\". Για να αγνοηθούν όλα τα αρχεία με κατάληξη \".tif\", χρησιμοποιήστε \"**/*.tif\". Για να αγνοηθεί μία απόλυτη διαδρομή, χρησιμοποιήστε \"/path/to/ignore/**\".", "authentication_settings": "Ρυθμίσεις ελέγχου ταυτότητας", "authentication_settings_description": "Διαχείριση κωδικού πρόσβασης, OAuth και άλλες ρυθμίσεις ελέγχου ταυτότητας", "authentication_settings_disable_all": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε όλες τις μεθόδους σύνδεσης; Η σύνδεση θα απενεργοποιηθεί πλήρως.", + "authentication_settings_reenable": "Για να επαναενεργοποιηθεί, χρησιμοποιήστε μία Server Command.", "background_task_job": "Εργασίες Παρασκηνίου", "check_all": "Έλεγχος Όλων", + "cleared_jobs": "Εκκαθάριση εργασιών για: {job}", + "config_set_by_file": "Η διαμόρφωση γίνεται προς το παρόν από ένα αρχείο config", "confirm_delete_library": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη βιβλιοθήκη {library};", + "confirm_delete_library_assets": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτή τη βιβλιοθήκη; Αυτό θα διαγράψει τα {count, plural, one {# contained asset} other {all # contained assets}} από το Immich και δεν μπορεί να αναιρεθεί. Τα αρχεία θα παραμείνουν στον δίσκο.", "confirm_email_below": "Για επιβεβαίωση, πληκτρολογήστε \"{email}\" παρακάτω", "confirm_reprocess_all_faces": "Είστε βέβαιοι ότι θέλετε να επεξεργαστείτε ξανά όλα τα πρόσωπα; Αυτό θα διαγράψει επίσης άτομα με όνομα.", "confirm_user_password_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό πρόσβασης του χρήστη {user};", + "disable_login": "Απενεργοποίηση σύνδεσης κατά την είσοδο", "duplicate_detection_job_description": "Εκτελέστε τη εκμάθηση μηχανής σε στοιχεία για να εντοπίσετε παρόμοιες εικόνες. Βασίζεται στην Έξυπνη Αναζήτηση", + "exclusion_pattern_description": "Τα πρότυπα αποκλεισμού σας επιτρέπουν να αγνοείται αρχεία κκαι φακέλους όσο σαρώνεται η βιβλιοθήκη. Αυτό είναι χρήσιμο εάν εχετε φακέλους που περιέχουν αρχεία που δεν θέλετε να εισαγάγετε, όπως αρχεία RAW.", + "external_library_created_at": "Εξωτερική βιβλιοθήκη (δημιουργήθηκε {date})", "external_library_management": "Διαχείριση Εξωτερικών Βιβλιοθηκών", "face_detection": "Αναγνώριση προσώπου", "face_detection_description": "Εντοπίστε τα πρόσωπα σε στοιχεία χρησιμοποιώντας μηχανική εκμάθηση. Για βίντεο, λαμβάνεται υπόψη μόνο η μικρογραφία. Η επιλογή \"Όλα\" επεξεργάζεται εκ νέου όλα τα στοιχεία. Η επιλογή \"Όσα Λείπουν\" προσθέτει στην ουρά στοιχεία που δεν έχουν υποστεί ακόμη επεξεργασία. Τα πρόσωπα που έχουν εντοπιστεί θα μπουν στην ουρά για την Αναγνώριση Προσώπου μετά την ολοκλήρωση της Ανίχνευσης Προσώπου, ομαδοποιώντας τα σε υπάρχοντα ή νέα άτομα.", @@ -43,7 +53,9 @@ "forcing_refresh_library_files": "Επιβολή ανανέωσης όλων των αρχείων της βιβλιοθήκης", "image_format_description": "Η μορφή WebP παράγει μικρότερα αρχεία από τη μορφή JPEG, αλλά είναι πιο αργή στην κωδικοποίηση.", "image_prefer_embedded_preview": "Προτίμηση ενσωματωμένης προεπισκόπησης", + "image_prefer_embedded_preview_setting_description": "Χρησιμοποιήστε ενσωματωμένες προεπισκοπίσεις για εικόνες RAW ως εισαγωγή στην επεξεργασία εικόνας όταν είναι διαθέσιμο. Αυτό μπορεί να δημιουργήσει πιο ακριβή χρωματα για κάποιες εικόνες, αλλά η ποιότητα των προεπισκοπίσεων εξαρτάται από την κάμερα και ενδέχεται να υπάρχουν περισσότερα μπιμπίκια λόγω συμπίεσης.", "image_prefer_wide_gamut": "Προτίμηση ευρείας γκάμας", + "image_prefer_wide_gamut_setting_description": "Χρησιμοποιήστε Display P3 για τις μικρογραφίες. Αυτό διατηρεί την ζωντάνια των χρωμάτων σε εικόνες μεγάλου χρωματικού εύρους, αλλά ενδέχεται να εμφανίζονται αλλιώς σε παλαιότερες συσκευές με παλαιότερες εκδόσεις περιηγητών. Οι εικόνες sRGB μένουν ως έχουν για να αποφευχθούν χρωματικές αλλαγές.", "image_preview_format": "Μορφή προεπισκόπησης", "image_preview_resolution": "Ανάλυση προεπισκόπησης", "image_preview_resolution_description": "Χρησιμοποιείται κατά την προβολή μιας φωτογραφίας και για μηχανική εκμάθηση. Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", @@ -54,10 +66,19 @@ "image_thumbnail_format": "Μορφή μικρογραφίας", "image_thumbnail_resolution": "Ανάλυση μικρογραφίας", "image_thumbnail_resolution_description": "Χρησιμοποιείται κατά την προβολή ομάδων φωτογραφιών (κύριο χρονολόγιο, προβολή άλμπουμ κλπ.). Υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", + "job_concurrency": "{job} συγχρονισμός", + "job_not_concurrency_safe": "Αυτή η εργασία δεν είναι ασφαλής για ταυτόχρονη εκτέλεση.", "job_settings": "Ρυθμίσεις Εργασιών", + "job_settings_description": "Διαχείρηση ταυτόχρονων εργασιών", "job_status": "Κατάσταση Εργασιών", + "jobs_delayed": "{jobCount, plural, other {# delayed}}", + "jobs_failed": "{jobCount, plural, other {# failed}}", "library_created": "Δημιουργήθηκε η βιβλιοθήκη: {library}", + "library_cron_expression": "Εκφράσεις Cron", + "library_cron_expression_description": "Ορισμός των διαστημάτων μεταξύ των σαρώσεων με χρήση cron μορφής. Για περισσότερες πληροφορίες παρακαλώ επισκεφθείτε το π.χ. Crontab Guru", + "library_cron_expression_presets": "Προκαθορισμένες εκφράσεις Cron", "library_deleted": "Η βιβλιοθήκη διαγράφηκε", + "library_import_path_description": "Καθορίστε έναν φάκελο για εισαγωγή. Αυτός ο φάκελος, συμπεριλαμβανομένων των υποφακέλων του, θα σαρωθεί για εικόνες και βίντεο.", "library_scanning": "Περιοδική Σάρωση", "library_scanning_description": "Διαμόρφωση περιοδικής σάρωσης βιβλιοθήκης", "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης", @@ -70,6 +91,7 @@ "logging_enable_description": "Ενεργοποίηση καταγραφής", "logging_level_description": "Όταν είναι ενεργοποιημένο, τι επίπεδο καταγραφής να εφαρμοστεί.", "logging_settings": "Καταγραφή", + "machine_learning_clip_model": "Μοντέλο CLIP", "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων", "machine_learning_duplicate_detection_enabled": "Ενεργοποίηση εντοπισμού διπλότυπων", "machine_learning_enabled": "Ενεργοποίηση μηχανικής εκμάθησης", diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index dbd6f32fde7c5..5ded90aad361e 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -28,6 +28,7 @@ "added_to_favorites_count": "Added {count, number} to favorites", "admin": { "add_exclusion_pattern_description": "Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named \"Raw\", use \"**/Raw/**\". To ignore all files ending in \".tif\", use \"**/*.tif\". To ignore an absolute path, use \"/path/to/ignore/**\".", + "asset_offline_description": "This external library asset is no longer found on disk and has been moved to trash. If the file was moved within the library, check your timeline for the new corresponding asset. To restore this asset, please ensure that the file path below can be accessed by Immich and scan the library.", "authentication_settings": "Authentication Settings", "authentication_settings_description": "Manage password, OAuth, and other authentication settings", "authentication_settings_disable_all": "Are you sure you want to disable all login methods? Login will be completely disabled.", @@ -41,6 +42,7 @@ "confirm_email_below": "To confirm, type \"{email}\" below", "confirm_reprocess_all_faces": "Are you sure you want to reprocess all faces? This will also clear named people.", "confirm_user_password_reset": "Are you sure you want to reset {user}'s password?", + "create_job": "Create job", "disable_login": "Disable login", "duplicate_detection_job_description": "Run machine learning on assets to detect similar images. Relies on Smart Search", "exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.", @@ -52,22 +54,25 @@ "failed_job_command": "Command {command} failed for job: {job}", "force_delete_user_warning": "WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be recovered.", "forcing_refresh_library_files": "Forcing refresh of all library files", + "image_format": "Format", "image_format_description": "WebP produces smaller files than JPEG, but is slower to encode.", "image_prefer_embedded_preview": "Prefer embedded preview", "image_prefer_embedded_preview_setting_description": "Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.", "image_prefer_wide_gamut": "Prefer wide gamut", "image_prefer_wide_gamut_setting_description": "Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.", - "image_preview_format": "Preview format", - "image_preview_resolution": "Preview resolution", - "image_preview_resolution_description": "Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.", + "image_preview_description": "Medium-size image with stripped metadata, used when viewing a single asset and for machine learning", + "image_preview_quality_description": "Preview quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness. Setting a low value may affect machine learning quality.", + "image_preview_title": "Preview Settings", "image_quality": "Quality", - "image_quality_description": "Image quality from 1-100. Higher is better for quality but produces larger files, this option affects the Preview and Thumbnail images.", + "image_resolution": "Resolution", + "image_resolution_description": "Higher resolutions can preserve more detail but take longer to encode, have larger file sizes and can reduce app responsiveness.", "image_settings": "Image Settings", "image_settings_description": "Manage the quality and resolution of generated images", - "image_thumbnail_format": "Thumbnail format", - "image_thumbnail_resolution": "Thumbnail resolution", - "image_thumbnail_resolution_description": "Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.", + "image_thumbnail_description": "Small thumbnail with stripped metadata, used when viewing groups of photos like the main timeline", + "image_thumbnail_quality_description": "Thumbnail quality from 1-100. Higher is better, but produces larger files and can reduce app responsiveness.", + "image_thumbnail_title": "Thumbnail Settings", "job_concurrency": "{job} concurrency", + "job_created": "Job created", "job_not_concurrency_safe": "This job is not concurrency-safe.", "job_settings": "Job Settings", "job_settings_description": "Manage job concurrency", @@ -150,7 +155,7 @@ "note_cannot_be_changed_later": "NOTE: This cannot be changed later!", "note_unlimited_quota": "Note: Enter 0 for unlimited quota", "notification_email_from_address": "From address", - "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \"", + "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \"", "notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignore certificate errors", "notification_email_ignore_certificate_errors_description": "Ignore TLS certificate validation errors (not recommended)", @@ -196,19 +201,19 @@ "password_settings": "Password Login", "password_settings_description": "Manage password login settings", "paths_validated_successfully": "All paths validated successfully", + "person_cleanup_job": "Person cleanup", "quota_size_gib": "Quota Size (GiB)", "refreshing_all_libraries": "Refreshing all libraries", "registration": "Admin Registration", "registration_description": "Since you are the first user on the system, you will be assigned as the Admin and are responsible for administrative tasks, and additional users will be created by you.", - "removing_offline_files": "Removing Offline Files", "repair_all": "Repair All", "repair_matched_items": "Matched {count, plural, one {# item} other {# items}}", "repaired_items": "Repaired {count, plural, one {# item} other {# items}}", "require_password_change_on_login": "Require user to change password on first login", "reset_settings_to_default": "Reset settings to default", "reset_settings_to_recent_saved": "Reset settings to the recent saved settings", - "scanning_library_for_changed_files": "Scanning library for changed files", - "scanning_library_for_new_files": "Scanning library for new files", + "scanning_library": "Scanning library", + "search_jobs": "Search jobs...", "send_welcome_email": "Send welcome email", "server_external_domain_settings": "External domain", "server_external_domain_settings_description": "Domain for public shared links, including http(s)://", @@ -236,6 +241,7 @@ "storage_template_settings_description": "Manage the folder structure and file name of the upload asset", "storage_template_user_label": "{label} is the user's Storage Label", "system_settings": "System Settings", + "tag_cleanup_job": "Tag cleanup", "theme_custom_css_settings": "Custom CSS", "theme_custom_css_settings_description": "Cascading Style Sheets allow the design of Immich to be customized.", "theme_settings": "Theme Settings", @@ -309,6 +315,7 @@ "trash_settings_description": "Manage trash settings", "untracked_files": "Untracked Files", "untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug", + "user_cleanup_job": "User cleanup", "user_delete_delay": "{user}'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Delete delay", "user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.", @@ -384,8 +391,8 @@ "asset_filename_is_offline": "Asset {filename} is offline", "asset_has_unassigned_faces": "Asset has unassigned faces", "asset_hashing": "Hashing...", - "asset_offline": "Asset offline", - "asset_offline_description": "This asset is offline. Immich can not access its file location. Please ensure the asset is available and then rescan the library.", + "asset_offline": "Asset Offline", + "asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.", "asset_skipped": "Skipped", "asset_skipped_in_trash": "In trash", "asset_uploaded": "Uploaded", @@ -398,7 +405,7 @@ "assets_moved_to_trash_count": "Moved {count, plural, one {# asset} other {# assets}} to trash", "assets_permanently_deleted_count": "Permanently deleted {count, plural, one {# asset} other {# assets}}", "assets_removed_count": "Removed {count, plural, one {# asset} other {# assets}}", - "assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action!", + "assets_restore_confirmation": "Are you sure you want to restore all your trashed assets? You cannot undo this action! Note that any offline assets cannot be restored this way.", "assets_restored_count": "Restored {count, plural, one {# asset} other {# assets}}", "assets_trashed_count": "Trashed {count, plural, one {# asset} other {# assets}}", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} already part of the album", @@ -501,13 +508,14 @@ "delete_api_key_prompt": "Are you sure you want to delete this API key?", "delete_duplicates_confirmation": "Are you sure you want to permanently delete these duplicates?", "delete_key": "Delete key", - "delete_library": "Delete library", + "delete_library": "Delete Library", "delete_link": "Delete link", "delete_shared_link": "Delete shared link", "delete_tag": "Delete tag", "delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?", "delete_user": "Delete user", "deleted_shared_link": "Deleted shared link", + "deletes_missing_assets": "Deletes assets missing from disk", "description": "Description", "details": "Details", "direction": "Direction", @@ -641,6 +649,7 @@ "unable_to_get_comments_number": "Unable to get number of comments", "unable_to_get_shared_link": "Failed to get shared link", "unable_to_hide_person": "Unable to hide person", + "unable_to_link_motion_video": "Unable to link motion video", "unable_to_link_oauth_account": "Unable to link OAuth account", "unable_to_load_album": "Unable to load album", "unable_to_load_asset_activity": "Unable to load asset activity", @@ -656,8 +665,8 @@ "unable_to_remove_album_users": "Unable to remove users from album", "unable_to_remove_api_key": "Unable to remove API Key", "unable_to_remove_assets_from_shared_link": "Unable to remove assets from shared link", + "unable_to_remove_deleted_assets": "Unable to remove offline files", "unable_to_remove_library": "Unable to remove library", - "unable_to_remove_offline_files": "Unable to remove offline files", "unable_to_remove_partner": "Unable to remove partner", "unable_to_remove_reaction": "Unable to remove reaction", "unable_to_repair_items": "Unable to repair items", @@ -679,6 +688,7 @@ "unable_to_submit_job": "Unable to submit job", "unable_to_trash_asset": "Unable to trash asset", "unable_to_unlink_account": "Unable to unlink account", + "unable_to_unlink_motion_video": "Unable to unlink motion video", "unable_to_update_album_cover": "Unable to update album cover", "unable_to_update_album_info": "Unable to update album info", "unable_to_update_library": "Unable to update library", @@ -717,7 +727,6 @@ "fix_incorrect_match": "Fix incorrect match", "folders": "Folders", "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", - "force_re-scan_library_files": "Force Re-scan All Library Files", "forward": "Forward", "general": "General", "get_help": "Get Help", @@ -885,7 +894,6 @@ "onboarding_welcome_user": "Welcome, {user}", "online": "Online", "only_favorites": "Only favorites", - "only_refreshes_modified_files": "Only refreshes modified files", "open_in_map_view": "Open in map view", "open_in_openstreetmap": "Open in OpenStreetMap", "open_the_search_filters": "Open the search filters", @@ -1005,7 +1013,7 @@ "refresh_metadata": "Refresh metadata", "refresh_thumbnails": "Refresh thumbnails", "refreshed": "Refreshed", - "refreshes_every_file": "Refreshes every file", + "refreshes_every_file": "Re-reads all existing and new files", "refreshing_encoded_video": "Refreshing encoded video", "refreshing_metadata": "Refreshing metadata", "regenerating_thumbnails": "Regenerating thumbnails", @@ -1014,10 +1022,10 @@ "remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?", "remove_assets_title": "Remove assets?", "remove_custom_date_range": "Remove custom date range", + "remove_deleted_assets": "Remove Deleted Assets", "remove_from_album": "Remove from album", "remove_from_favorites": "Remove from favorites", "remove_from_shared_link": "Remove from shared link", - "remove_offline_files": "Remove Offline Files", "remove_user": "Remove user", "removed_api_key": "Removed API Key: {name}", "removed_from_archive": "Removed from archive", @@ -1053,8 +1061,7 @@ "saved_settings": "Saved settings", "say_something": "Say something", "scan_all_libraries": "Scan All Libraries", - "scan_all_library_files": "Re-scan All Library Files", - "scan_new_library_files": "Scan New Library Files", + "scan_library": "Scan", "scan_settings": "Scan Settings", "scanning_for_album": "Scanning for album...", "search": "Search", @@ -1072,6 +1079,7 @@ "search_options": "Search options", "search_people": "Search people", "search_places": "Search places", + "search_settings": "Search settings", "search_state": "Search state...", "search_tags": "Search tags...", "search_timezone": "Search timezone...", @@ -1138,6 +1146,7 @@ "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", "show_search_options": "Show search options", + "show_slideshow_transition": "Show slideshow transition", "show_supporter_badge": "Supporter badge", "show_supporter_badge_description": "Show a supporter badge", "shuffle": "Shuffle", @@ -1185,7 +1194,7 @@ "tag_assets": "Tag assets", "tag_created": "Created tag: {tag}", "tag_feature_description": "Browsing photos and videos grouped by logical tag topics", - "tag_not_found_question": "Cannot find a tag? Create one here", + "tag_not_found_question": "Cannot find a tag? Create a new tag.", "tag_updated": "Updated tag: {tag}", "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", "tags": "Tags", @@ -1219,6 +1228,7 @@ "unknown": "Unknown", "unknown_year": "Unknown Year", "unlimited": "Unlimited", + "unlink_motion_video": "Unlink motion video", "unlink_oauth": "Unlink OAuth", "unlinked_oauth_account": "Unlinked OAuth account", "unnamed_album": "Unnamed Album", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index 509c6d515f25c..b6014703aa896 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -41,6 +41,7 @@ "confirm_email_below": "Para confirmar, escribe \"{email}\" debajo", "confirm_reprocess_all_faces": "¿Estás seguro de que quieres volver a procesar todas las caras? Esto también eliminará las personas a las que le hayas asignado nombre.", "confirm_user_password_reset": "¿Estás seguro de que quieres resetear la contraseña de {user}?", + "create_job": "Crear trabajo", "crontab_guru": "Crontab Guru", "disable_login": "Deshabilitar inicio de sesión", "disabled": "Deshabilitado", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Resolución de las miniaturas", "image_thumbnail_resolution_description": "Se utiliza para ver grupos de fotos (cronología, vista de álbum, etc.). Las resoluciones más altas pueden conservar más detalles, pero tardan más en codificarse, tienen archivos de mayor tamaño y pueden reducir la reactividad de la aplicación.", "job_concurrency": "{job}: Procesos simultáneos", + "job_created": "Trabajo creado", "job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.", "job_settings": "Configuración tareas", "job_settings_description": "Administrar tareas simultáneas", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "NOTA: No se puede cambiar posteriormente!", "note_unlimited_quota": "Nota: usa 0 para espacio sin límites", "notification_email_from_address": "Desde", - "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server \"", + "notification_email_from_address_description": "Dirección de correo electrónico del remitente, por ejemplo: \"Immich Photo Server \"", "notification_email_host_description": "Host del servidor de correo electrónico (por ejemplo: smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar errores de certificado", "notification_email_ignore_certificate_errors_description": "Ignorar los errores de validación del certificado TLS (no recomendado)", @@ -198,11 +200,12 @@ "password_settings": "Contraseña de Acceso", "password_settings_description": "Administrar la configuración de inicio de sesión con contraseña", "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente", + "person_cleanup_job": "Limpieza de personas", "quota_size_gib": "Tamaño de Quota (GiB)", "refreshing_all_libraries": "Actualizar todas las bibliotecas", "registration": "Registrar administrador", "registration_description": "Dado que eres el primer usuario del sistema, se te asignará como Admin y serás responsable de las tareas administrativas, y de crear a los usuarios adicionales.", - "removing_offline_files": "Eliminando archivos sin conexión", + "removing_deleted_files": "Eliminando archivos sin conexión", "repair_all": "Reparar todo", "repair_matched_items": "Coincidencia {count, plural, one {# elemento} other {# elementos}}", "repaired_items": "Reparado {count, plural, one {# elemento} other {# elementos}}", @@ -211,6 +214,7 @@ "reset_settings_to_recent_saved": "Restablecer la configuración a la configuración guardada recientemente", "scanning_library_for_changed_files": "Escanear archivos modificados en biblioteca", "scanning_library_for_new_files": "Escanear nuevos archivos en biblioteca", + "search_jobs": "Buscar trabajo...", "send_welcome_email": "Enviar correo de bienvenida", "server_external_domain_settings": "Dominio externo", "server_external_domain_settings_description": "Dominio para enlaces públicos compartidos, incluidos http(s)://", @@ -238,6 +242,7 @@ "storage_template_settings_description": "Administre la estructura de carpetas y el nombre de archivo del recurso cargado", "storage_template_user_label": "{label} es la etiqueta de almacenamiento del usuario", "system_settings": "Ajustes del Sistema", + "tag_cleanup_job": "Limpieza de etiquetas", "theme_custom_css_settings": "CSS Personalizado", "theme_custom_css_settings_description": "Las Hojas de Estilo (CSS) permiten personalizar el diseño de Immich.", "theme_settings": "Ajustes Tema", @@ -312,7 +317,8 @@ "trash_settings_description": "Administrar la configuración de la papelera", "untracked_files": "Archivos sin seguimiento", "untracked_files_description": "La aplicación no rastrea estos archivos. Puede ser el resultado de movimientos fallidos, cargas interrumpidas o sin procesar debido a un error", - "user_delete_delay": "La cuenta {user} y los archivos se programarán para su eliminación permanente en {delay, plural, one {# day} other {# days}}.", + "user_cleanup_job": "Limpieza de usuarios", + "user_delete_delay": "La cuenta {user} y los archivos se programarán para su eliminación permanente en {delay, plural, one {# día} other {# días}}.", "user_delete_delay_settings": "Eliminar retardo", "user_delete_delay_settings_description": "Número de días después de la eliminación para eliminar permanentemente la cuenta y los activos de un usuario. El trabajo de eliminación de usuarios se ejecuta a medianoche para comprobar si hay usuarios que estén listos para su eliminación. Los cambios a esta configuración se evaluarán en la próxima ejecución.", "user_delete_immediately": "La cuenta {user} y los archivos se pondrán en cola para su eliminación permanente inmediatamente.", @@ -336,8 +342,8 @@ "admin_password": "Contraseña del Administrador", "administration": "Administración", "advanced": "Avanzada", - "age_months": "Tiempo {months, plural, one {# month} other {# months}}", - "age_year_months": "1 año, {months, plural, one {# month} other {# months}}", + "age_months": "Tiempo {months, plural, one {# mes} other {# meses}}", + "age_year_months": "1 año, {months, plural, one {# mes} other {# meses}}", "age_years": "Edad {years, plural, one {# año} other {# años}}", "album_added": "Álbum añadido", "album_added_notification_setting_description": "Reciba una notificación por correo electrónico cuando lo agreguen a un álbum compartido", @@ -391,6 +397,7 @@ "asset_offline": "Archivos fuera de linea", "asset_offline_description": "Este archivo está offline. Immich no puede acceder a la ubicación de su archivo. Asegúrese de que el archivo esté disponible y luego vuelva a escanear la biblioteca.", "asset_skipped": "Omitido", + "asset_skipped_in_trash": "En la papelera", "asset_uploaded": "Subido", "asset_uploading": "Subiendo...", "assets": "elementos", @@ -399,12 +406,12 @@ "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_moved_to_trash": "Se movió {count, plural, one {# activo} other {# activos}} a la papelera", - "assets_moved_to_trash_count": "Movido {count, plural, one {# asset} other {# assets}} a la papelera", - "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", - "assets_removed_count": "Eliminado {count, plural, one {# asset} other {# assets}}", + "assets_moved_to_trash_count": "Movido {count, plural, one {# elemento} other {# elementos}} a la papelera", + "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", + "assets_removed_count": "Eliminado {count, plural, one {# elemento} other {# elementos}}", "assets_restore_confirmation": "¿Está seguro de que desea restaurar todos sus archivos eliminados? ¡No puedes deshacer esta acción!", - "assets_restored_count": "Restaurado {count, plural, one {# asset} other {# assets}}", - "assets_trashed_count": "Borrado {count, plural, one {# asset} other {# assets}}", + "assets_restored_count": "Restaurado {count, plural, one {# elemento} other {# elementos}}", + "assets_trashed_count": "Borrado {count, plural, one {# elemento} other {# elementos}}", "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} ya forma parte del álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Atrás", @@ -415,7 +422,7 @@ "blurred_background": "Fondo borroso", "build": "Compilación", "build_image": "Construir Imagen", - "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# duplicate asset} other {# duplicate assets}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", + "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.", "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.", "buy": "Comprar Immich", @@ -588,7 +595,7 @@ "cant_apply_changes": "No se pueden aplicar los cambios", "cant_change_activity": "No se puede realizar la actividad {enabled, select, true {disable} other {enable}}", "cant_change_asset_favorite": "No se puede cambiar favorito para este archivo", - "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# asset} other {# assets}}", + "cant_change_metadata_assets_count": "No se pueden cambiar los metadatos de {count, plural, one {# elemento} other {# elementos}}", "cant_get_faces": "No se encuentran caras", "cant_get_number_of_comments": "No se puede obtener la cantidad de comentarios", "cant_search_people": "No se puede buscar a personas", @@ -615,7 +622,7 @@ "failed_to_unstack_assets": "Error al desagrupar los archivos", "import_path_already_exists": "Esta ruta de importación ya existe.", "incorrect_email_or_password": "Contraseña o email incorrecto", - "paths_validation_failed": "Falló la validación en {paths, plural, one {# carpetas} other {# carpetas}}", + "paths_validation_failed": "Falló la validación en {paths, plural, one {# carpeta} other {# carpetas}}", "profile_picture_transparent_pixels": "Las imágenes de perfil no pueden tener píxeles transparentes. Por favor amplíe y/o mueva la imagen.", "quota_higher_than_disk_size": "Se ha establecido una cuota superior al tamaño del disco", "repair_unable_to_check_items": "No se puede verificar {count, select, one {elemento} other {elementos}}", @@ -633,7 +640,7 @@ "unable_to_change_favorite": "Imposible cambiar el archivo favorito", "unable_to_change_location": "No se puede cambiar de ubicación", "unable_to_change_password": "No se puede cambiar la contraseña", - "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# person} other {# people}}", + "unable_to_change_visibility": "No se puede cambiar la visibilidad de {count, plural, one {# persona} other {# personas}}", "unable_to_check_item": "", "unable_to_check_items": "", "unable_to_complete_oauth_login": "No se puede completar el inicio de sesión de OAuth", @@ -660,6 +667,7 @@ "unable_to_get_comments_number": "No se puede obtener el número de comentarios", "unable_to_get_shared_link": "Error al obtener el enlace compartido", "unable_to_hide_person": "No se puede ocultar a la persona", + "unable_to_link_motion_video": "No se puede enlazar el vídeo en movimiento", "unable_to_link_oauth_account": "No se puede vincular la cuenta OAuth", "unable_to_load_album": "No se puede cargar el álbum", "unable_to_load_asset_activity": "No se puede cargar la actividad de los archivos", @@ -676,8 +684,8 @@ "unable_to_remove_api_key": "No se puede eliminar la clave API", "unable_to_remove_assets_from_shared_link": "No se pueden eliminar archivos desde el enlace compartido", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "No se pueden eliminar archivos sin conexión", "unable_to_remove_library": "No se puede eliminar la biblioteca", - "unable_to_remove_offline_files": "No se pueden eliminar archivos sin conexión", "unable_to_remove_partner": "No se puede eliminar el invitado", "unable_to_remove_reaction": "No se puede eliminar la reacción", "unable_to_remove_user": "", @@ -700,6 +708,7 @@ "unable_to_submit_job": "No se puede enviar el trabajo", "unable_to_trash_asset": "No se puede eliminar el archivo", "unable_to_unlink_account": "No se puede desvincular la cuenta", + "unable_to_unlink_motion_video": "No se puede desvincular el vídeo en movimiento", "unable_to_update_album_cover": "No se puede actualizar la portada del álbum", "unable_to_update_album_info": "No se puede actualizar la información del álbum", "unable_to_update_library": "No se puede actualizar la biblioteca", @@ -845,6 +854,7 @@ "license_trial_info_4": "Por favor, considera la compra de una licencia para apoyar el desarrollo continuo del servicio", "light": "Claro", "like_deleted": "Me gusta eliminado", + "link_motion_video": "Enlazar vídeo en movimiento", "link_options": "Opciones de enlace", "link_to_oauth": "Enlace a OAuth", "linked_oauth_account": "Cuenta OAuth vinculada", @@ -887,7 +897,7 @@ "merge_people_limit": "Solo puedes fusionar hasta 5 caras a la vez", "merge_people_prompt": "¿Quieres fusionar a estas personas? Esta acción es irreversible.", "merge_people_successfully": "Personas fusionadas correctamente", - "merged_people_count": "Fusionar {count, plural, one {# person} other {# people}}", + "merged_people_count": "Fusionada {count, plural, one {# persona} other {# personas}}", "minimize": "Minimizar", "minute": "Minuto", "missing": "Perdido", @@ -968,9 +978,9 @@ "password_required": "Contraseña requerida", "password_reset_success": "Restablecimiento de contraseña exitoso", "past_durations": { - "days": "Pasados {days, plural, one {day} other {# days}}", - "hours": "Pasadas {hours, plural, one {hour} other {# hours}}", - "years": "Pasado(s) {years, plural, one {year} other {# years}}" + "days": "Pasados {days, plural, one {día} other {# días}}", + "hours": "Pasadas {hours, plural, one {hora} other {# horas}}", + "years": "Pasado(s) {years, plural, one {año} other {# años}}" }, "path": "Ruta", "pattern": "Patrón", @@ -979,18 +989,18 @@ "paused": "Detenido", "pending": "Pendiente", "people": "Personas", - "people_edits_count": "Editado {count, plural, one {# person} other {# people}}", + "people_edits_count": "Editada {count, plural, one {# persona} other {# personas}}", "people_feature_description": "Explorar fotos y vídeos agrupados por personas", "people_sidebar_description": "Mostrar un enlace a Personas en la barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Advertencia de eliminación permanente", "permanent_deletion_warning_setting_description": "Mostrar una advertencia al eliminar archivos permanentemente", "permanently_delete": "Borrar permanentemente", - "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {asset} other {assets}}", - "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {¿este activo?} other {¿estos # activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).", + "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {elemento} other {elementos}}", + "permanently_delete_assets_prompt": "¿Está seguro de que desea eliminar permanentemente {count, plural, one {este activo?} other {estos # activos?}} Esto también eliminará {count, plural, one {de tu} other {de tus}} álbum(es).", "permanently_deleted_asset": "Archivo eliminado permanentemente", "permanently_deleted_assets": "Eliminado permanentemente {count, plural, one {# activo} other {# activos}}", - "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", + "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# elemento} other {# elementos}}", "person": "Persona", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningún usuario con quien compartirlas.", @@ -1059,8 +1069,8 @@ "reaction_options": "Opciones de reacción", "read_changelog": "Leer registro de cambios", "reassign": "Reasignar", - "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# asset} other {# assets}} to {name, select, null {an existing person} other {{name}}}", - "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# asset} other {# assets}} a un nuevo usuario", + "reassigned_assets_to_existing_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a {name, select, null {una persona existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reasignado {count, plural, one {# elemento} other {# elementos}} a un nuevo usuario", "reassing_hint": "Asignar archivos seleccionados a una persona existente", "recent": "Reciente", "recent_searches": "Búsquedas recientes", @@ -1074,19 +1084,19 @@ "refreshing_metadata": "Recargando metadatos", "regenerating_thumbnails": "Recargando miniaturas", "remove": "Eliminar", - "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del álbum?", - "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del enlace compartido?", + "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del álbum?", + "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# elemento} other {# elementos}} del enlace compartido?", "remove_assets_title": "¿Eliminar activos?", "remove_custom_date_range": "Eliminar intervalo de fechas personalizado", + "remove_deleted_assets": "Eliminar archivos sin conexión", "remove_from_album": "Eliminar del álbum", "remove_from_favorites": "Quitar de favoritos", "remove_from_shared_link": "Eliminar desde enlace compartido", - "remove_offline_files": "Eliminar archivos sin conexión", "remove_user": "Eliminar usuario", "removed_api_key": "Clave API eliminada: {name}", "removed_from_archive": "Eliminado del archivo", "removed_from_favorites": "Eliminado de favoritos", - "removed_from_favorites_count": "{count, plural, other {Removed #}} de favoritos", + "removed_from_favorites_count": "{count, plural, other {Eliminados #}} de favoritos", "removed_tagged_assets": "Etiqueta eliminada de {count, plural, one {# activo} other {# activos}}", "rename": "Renombrar", "repair": "Reparar", @@ -1134,8 +1144,10 @@ "search_for_existing_person": "Buscar persona existente", "search_no_people": "Ninguna persona", "search_no_people_named": "Ninguna persona llamada \"{name}\"", + "search_options": "Opciones de búsqueda", "search_people": "Buscar personas", "search_places": "Buscar lugar", + "search_settings": "Ajustes de la búsqueda", "search_state": "Buscar región/estado...", "search_tags": "Buscando etiquetas...", "search_timezone": "Buscar zona horaria...", @@ -1212,6 +1224,8 @@ "sign_up": "Registrarse", "size": "Tamaño", "skip_to_content": "Saltar al contenido", + "skip_to_folders": "Ir a las carpetas", + "skip_to_tags": "Ir a las etiquetas", "slideshow": "Diapositivas", "slideshow_settings": "Ajustes de diapositivas", "sort_albums_by": "Ordenar álbumes por...", @@ -1226,7 +1240,7 @@ "stack_duplicates": "Apilar duplicados", "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", - "stacked_assets_count": "Apilados {count, plural, one {# asset} other {# assets}}", + "stacked_assets_count": "Apilado(s) {count, plural, one {# activo} other {# activos}}", "stacktrace": "Stacktrace", "start": "Inicio", "start_date": "Fecha de inicio", @@ -1286,6 +1300,7 @@ "unknown_album": "Álbum desconocido", "unknown_year": "Año desconocido", "unlimited": "Ilimitado", + "unlink_motion_video": "Desvincular vídeo en movimiento", "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Cuenta OAuth desconectada", "unnamed_album": "Album sin nombre", @@ -1295,14 +1310,14 @@ "unselect_all": "Limpiar selección", "unselect_all_duplicates": "Deseleccionar todos los duplicados", "unstack": "Desapilar", - "unstacked_assets_count": "Sin apilar {count, plural, one {# asset} other {# assets}}", + "unstacked_assets_count": "Desapilado(s) {count, plural, one {# elemento} other {# elementos}}", "untracked_files": "Archivos no monitorizados", "untracked_files_decription": "Estos archivos no están siendo monitorizados por la aplicación. Es posible que sean resultado de errores al moverlos, cargas interrumpidas o por un fallo de la aplicación", "up_next": "A continuación", "updated_password": "Contraseña actualizada", "upload": "Subir", "upload_concurrency": "Cargas simultáneas", - "upload_errors": "Carga completada con {count, plural, one {# error} other {# errors}}, actualice la página para ver los nuevos recursos de carga.", + "upload_errors": "Carga completada con {count, plural, one {# error} other {# errores}}, actualice la página para ver los nuevos recursos de carga.", "upload_progress": "Restante {remaining, number} - Procesado {processed, number}/{total, number}", "upload_skipped_duplicates": "Saltado {count, plural, one {# duplicate asset} other {# duplicate assets}}", "upload_status_duplicates": "Duplicados", @@ -1344,14 +1359,14 @@ "view_previous_asset": "Mostrar elemento anterior", "view_stack": "Ver Pila", "viewer": "Visualizador", - "visibility_changed": "Visibilidad cambiada para {count, plural, one {# person} other {# people}}", + "visibility_changed": "Visibilidad cambiada para {count, plural, one {# persona} other {# personas}}", "waiting": "Esperando", "warning": "Advertencia", "week": "Semana", "welcome": "Bienvenido", "welcome_to_immich": "Bienvenido a immich", "year": "Año", - "years_ago": "Hace {years, plural, one {# year} other {# years}}", + "years_ago": "Hace {years, plural, one {# año} other {# años}}", "yes": "Sí", "you_dont_have_any_shared_links": "No tienes ningún enlace compartido", "zoom_image": "Acercar Imagen" diff --git a/web/src/lib/i18n/et.json b/web/src/lib/i18n/et.json index a37370f84ad98..58a9ce024cbd5 100644 --- a/web/src/lib/i18n/et.json +++ b/web/src/lib/i18n/et.json @@ -41,20 +41,22 @@ "confirm_email_below": "Kinnitamiseks sisesta allpool \"{email}\"", "confirm_reprocess_all_faces": "Kas oled kindel, et soovid kõik näod uuesti töödelda? See eemaldab kõik nimega isikud.", "confirm_user_password_reset": "Kas oled kindel, et soovid kasutaja {user} parooli lähtestada?", + "create_job": "Lisa tööde", "disable_login": "Keela sisselogimine", - "duplicate_detection_job_description": "Rakenda üksustele masinõpet, et tuvastada sarnaseid pilte. Kasutab nutiotsingut", + "duplicate_detection_job_description": "Rakenda üksustele masinõpet, et leida sarnaseid pilte. Kasutab nutiotsingut", "exclusion_pattern_description": "Välistamismustrid võimaldavad ignoreerida faile ja kaustu kogu skaneerimisel. See on kasulik, kui sul on kaustu, mis sisaldavad faile, mida sa ei soovi importida, nagu RAW failid.", "external_library_created_at": "Väline kogu (lisatud {date})", "external_library_management": "Väliste kogude haldus", - "face_detection": "Näotuvastus", - "face_detection_description": "Otsi üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Kõik\" töötleb kõik üksused uuesti. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Leitud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.", - "facial_recognition_job_description": "Grupeeri leitud näod inimesteks. See samm käivitub siis, kui näotuvastus on lõppenud. \"Kõik\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.", + "face_detection": "Näoavastus", + "face_detection_description": "Avasta üksustest nägusid masinõppe abil. Videote puhul kasutatakse ainult pisipilti. \"Kõik\" töötleb kõik üksused uuesti. \"Puuduvad\" võtab ette üksused, mida pole veel töödeldud. Avastatud näod suunatakse näotuvastusse, et grupeerida nad olemasolevateks või uuteks isikuteks.", + "facial_recognition_job_description": "Grupeeri avastatud näod inimesteks. See samm käivitub siis, kui näoavastus on lõppenud. \"Kõik\" grupeerib kõik näod uuesti. \"Puuduvad\" võtab ette näod, mida pole isikuga seostatud.", "failed_job_command": "Käsk {command} ebaõnnestus töötes: {job}", "force_delete_user_warning": "HOIATUS: See kustutab koheselt kasutaja ja kõik üksused. Seda ei saa tagasi võtta ja faile ei saa taastada.", "forcing_refresh_library_files": "Kogu kõigi failide sundvärskendamine", "image_format_description": "WebP failid on väiksemad kui JPEG, aga kodeerimine on aeglasem.", "image_prefer_embedded_preview": "Eelista manustatud eelvaadet", "image_prefer_embedded_preview_setting_description": "Kasuta pilditöötluse sisendina võimalusel RAW fotodesse manustatud eelvaateid. See võib mõnede piltide puhul anda tulemuseks täpsemad värvid, aga eelvaate kvaliteet sõltub konkreetsest kaamerast ning pildis võib olla rohkem tihendusmüra.", + "image_prefer_wide_gamut": "Eelista laia värvigammat", "image_prefer_wide_gamut_setting_description": "Kasuta pisipiltide jaoks Display P3. See säilitab paremini laia värviruumiga piltide erksuse, aga vanematel seadmetel ja vanemate brauseritega võivad pildid teistsugused välja näha. sRGB pildid säilitatakse värvinihete vältimiseks.", "image_preview_format": "Eelvaate formaat", "image_preview_resolution": "Eelvaate resolutsioon", @@ -67,9 +69,13 @@ "image_thumbnail_resolution": "Pisipildi resolutsioon", "image_thumbnail_resolution_description": "Kasutusel fotode mitmekaupa vaatamisel (ajajoon, albumi vaade, jne). Kõrgem resolutsioon säilitab rohkem detaile, aga kodeerimine võtab rohkem aega, tekitab suurema faili ning võib mõjutada rakenduse töökiirust.", "job_concurrency": "{job} samaaegsus", + "job_created": "Tööde lisatud", + "job_not_concurrency_safe": "Seda töödet pole ohutu samaaegselt käivitada.", "job_settings": "Tööte seaded", "job_settings_description": "Halda töödete samaaegsust", "job_status": "Tööte seisund", + "jobs_delayed": "{jobCount, plural, other {# edasi lükatud}}", + "jobs_failed": "{jobCount, plural, other {# ebaõnnestus}}", "library_created": "Lisatud kogu: {library}", "library_cron_expression": "Cron avaldis", "library_cron_expression_description": "Sea skaneerimise intervall cron formaadis. Rohkema info jaoks vaata nt. Crontab Guru", @@ -81,6 +87,7 @@ "library_scanning_enable_description": "Luba kogu perioodiline skaneerimine", "library_settings": "Väline kogu", "library_settings_description": "Halda välise kogu seadeid", + "library_tasks_description": "Soorita kogu toiminguid", "library_watching_enable_description": "Jälgi välises kogus failide muudatusi", "library_watching_settings": "Kogu jälgimine (EKSPERIMENTAALNE)", "library_watching_settings_description": "Jälgi automaatselt muutunud faile", @@ -89,23 +96,26 @@ "logging_settings": "Logimine", "machine_learning_clip_model": "CLIP mudel", "machine_learning_clip_model_description": "CLIP mudeli nimi, mis on loetletud siin. Pane tähele, et mudeli muutmisel pead kõigi piltide peal nutiotsingu tööte uuesti käivitama.", - "machine_learning_duplicate_detection": "Duplikaatide tuvastus", - "machine_learning_duplicate_detection_enabled": "Luba duplikaatide tuvastus", + "machine_learning_duplicate_detection": "Duplikaatide leidmine", + "machine_learning_duplicate_detection_enabled": "Luba duplikaatide leidmine", "machine_learning_duplicate_detection_enabled_description": "Kui keelatud, dedubleeritakse siiski täpselt identsed üksused.", "machine_learning_duplicate_detection_setting_description": "Kasuta CLIP-manuseid, et leida tõenäoliseid duplikaate", "machine_learning_enabled": "Luba masinõpe", "machine_learning_enabled_description": "Kui keelatud, lülitatakse kõik masinõppe funktsioonid välja, sõltumata allolevatest seadetest.", "machine_learning_facial_recognition": "Näotuvastus", - "machine_learning_facial_recognition_description": "Otsi, tuvasta ja grupeeri piltidel näod", + "machine_learning_facial_recognition_description": "Avasta, tuvasta ja grupeeri piltidel näod", "machine_learning_facial_recognition_model": "Näotuvastuse mudel", - "machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näotuvastuse tööde kõigi piltide peal uuesti käivitada.", + "machine_learning_facial_recognition_model_description": "Mudelid on järjestatud suuruse järgi kahanevalt. Suuremad mudelid on aeglasemad ja kasutavad rohkem mälu, kuid annavad parema tulemuse. Mudeli muutmisel tuleb näoavastuse tööde kõigi piltide peal uuesti käivitada.", "machine_learning_facial_recognition_setting": "Luba näotuvastus", - "machine_learning_max_detection_distance": "Maksimaalne tuvastuskaugus", - "machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused tuvastavad rohkem duplikaate, aga võivad anda valepositiivseid.", + "machine_learning_facial_recognition_setting_description": "Kui keelatud, siis ei kodeerita pilte näotuvastuse jaoks ning isikute sektsioon Avasta lehel jääb tühjaks.", + "machine_learning_max_detection_distance": "Maksimaalne avastuskaugus", + "machine_learning_max_detection_distance_description": "Maksimaalne kaugus kahe pildi vahel, mille puhul loetakse nad duplikaatideks, vahemikus 0.001-0.1. Kõrgemad väärtused leiavad rohkem duplikaate, aga võib esineda valepositiivseid.", + "machine_learning_max_recognition_distance": "Maksimaalne tuvastuskaugus", "machine_learning_max_recognition_distance_description": "Maksimaalne kaugus kahe näo vahel, mida tuleks lugeda samaks isikuks, vahemikus 0-2. Selle vähendamine aitab vältida erinevate inimeste samaks isikuks märkimist ja tõstmine aitab vältida sama inimese kaheks erinevaks isikuks märkimist. Pane tähele, et kaht isikut ühendada on lihtsam kui üht isikut kaheks eraldada, seega võimalusel kasuta madalamat lävendit.", - "machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo tuvastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.", - "machine_learning_min_recognized_faces": "Minimaalne leitud nägude arv", - "machine_learning_min_recognized_faces_description": "Minimaalne leitud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.", + "machine_learning_min_detection_score": "Minimaalne avastusskoor", + "machine_learning_min_detection_score_description": "Minimaalne usaldusskoor näo avastamiseks, vahemikus 0-1. Madalamad väärtused leiavad rohkem nägusid, kuid võib esineda valepositiivseid.", + "machine_learning_min_recognized_faces": "Minimaalne tuvastatud nägude arv", + "machine_learning_min_recognized_faces_description": "Minimaalne tuvastatud nägude arv, mida saab isikuks grupeerida. Selle suurendamine teeb näotuvastuse täpsemaks, kuid suureneb tõenäosus, et nägu ei seostata ühegi isikuga.", "machine_learning_settings": "Masinõppe seaded", "machine_learning_settings_description": "Halda masinõppe funktsioone ja seadeid", "machine_learning_smart_search": "Nutiotsing", @@ -113,19 +123,32 @@ "machine_learning_smart_search_enabled": "Luba nutiotsing", "machine_learning_smart_search_enabled_description": "Kui keelatud, siis ei kodeerita pilte nutiotsingu jaoks.", "machine_learning_url_description": "Masinõppe serveri URL", + "manage_concurrency": "Halda samaaegsust", "manage_log_settings": "Halda logi seadeid", "map_dark_style": "Tume stiil", + "map_enable_description": "Luba kaardi funktsioonid", "map_gps_settings": "Kaardi ja GPS-i seaded", + "map_gps_settings_description": "Halda kaardi ja GPS-i (pöördgeokodeerimise) seadeid", + "map_implications": "Kaardifunktsioon kasutab välist kaarditeenust (tiles.immich.cloud)", "map_light_style": "Hele stiil", + "map_manage_reverse_geocoding_settings": "Halda pöördgeokodeerimise seadeid", + "map_reverse_geocoding": "Pöördgeokodeerimine", + "map_reverse_geocoding_enable_description": "Luba pöördgeokodeerimine", + "map_reverse_geocoding_settings": "Pöördgeokodeerimise seaded", "map_settings": "Kaart", "map_settings_description": "Halda kaardi seadeid", + "map_style_description": "Kaarditeema style.json URL", "metadata_extraction_job": "Metaandmete eraldamine", - "metadata_extraction_job_description": "Eralda igast üksusest metaandmed, nagu GPS-koordinaadid ja resolutsioon", + "metadata_extraction_job_description": "Eralda igast üksusest metaandmed, nagu GPS-koordinaadid, näod ja resolutsioon", + "metadata_faces_import_setting": "Luba nägude import", + "metadata_settings": "Metaandmete seaded", + "metadata_settings_description": "Halda metaandmete seadeid", "migration_job": "Migratsioon", "migration_job_description": "Migreeri üksuste ja nägude pisipildid uusimale kaustastruktuurile", "note_cannot_be_changed_later": "MÄRKUS: Seda ei saa hiljem muuta!", + "note_unlimited_quota": "Märkus: Piiramatu kvoodi jaoks sisesta 0", "notification_email_from_address": "Saatja aadress", - "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server \"", + "notification_email_from_address_description": "Saatja e-posti aadress, näiteks: \"Immich Photo Server \"", "notification_email_host_description": "E-posti serveri host (nt. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoreeri sertifikaadi vigu", "notification_email_ignore_certificate_errors_description": "Ignoreeri TLS sertifikaadi valideerimise vigu (mittesoovituslik)", @@ -140,17 +163,27 @@ "notification_enable_email_notifications": "Luba e-posti teel teavitused", "notification_settings": "Teavituse seaded", "notification_settings_description": "Halda teavituste seadeid, sh. e-posti teel", + "oauth_auto_launch": "Automaatne käivitamine", + "oauth_auto_launch_description": "Alusta OAuth autentimist automaatselt sisselogimise lehele jõudmisel", + "oauth_auto_register": "Automaatne registreerimine", + "oauth_auto_register_description": "Registreeri uued kasutajad automaatselt OAuth abil sisselogimisel", "oauth_button_text": "Nupu tekst", "oauth_client_id": "Kliendi ID", "oauth_client_secret": "Kliendi saladus", "oauth_enable_description": "Sisene OAuth abil", "oauth_issuer_url": "Väljastaja URL", + "oauth_mobile_redirect_uri": "Mobiilne ümbersuunamise URI", + "oauth_profile_signing_algorithm": "Profiili allkirjastamise algoritm", + "oauth_profile_signing_algorithm_description": "Algoritm, mida kasutatakse kasutajaprofiili allkirjastamiseks.", + "oauth_scope": "Skoop", "oauth_settings": "OAuth", "oauth_settings_description": "Halda OAuth sisselogimise seadeid", + "oauth_signing_algorithm": "Allkirjastamise algoritm", "password_enable_description": "Logi sisse e-posti aadressi ja parooliga", "password_settings": "Parooliga sisselogimine", "password_settings_description": "Halda parooliga sisselogimise seadeid", "paths_validated_successfully": "Kõik teed edukalt valideeritud", + "person_cleanup_job": "Isikute korrastamine", "quota_size_gib": "Kvoot (GiB)", "refreshing_all_libraries": "Kõikide kogude värskendamine", "registration_description": "Kuna sa oled süsteemis esimene kasutaja, määratakse sind administraatoriks, ning sa saad lisada täiendavaid kasutajaid.", @@ -171,6 +204,10 @@ "storage_template_migration_info": "Malli muudatused rakenduvad ainult uutele üksustele. Et rakendada malli tagasiulatuvalt olemasolevatele üksustele, käivita {job}.", "storage_template_settings_description": "Halda üleslaaditud üksuse kaustastruktuuri ja failinime", "system_settings": "Süsteemi seaded", + "tag_cleanup_job": "Siltide korrastamine", + "theme_custom_css_settings": "Kohandatud CSS", + "theme_custom_css_settings_description": "Cascading Style Sheets lubab Immich'i kujunduse kohandamist.", + "theme_settings": "Teema seaded", "theme_settings_description": "Halda Immich'i veebiliidese kohandamist", "thumbnail_generation_job": "Genereeri pisipildid", "thumbnail_generation_job_description": "Genereeri iga üksuse kohta suur, väike ja udustatud pisipilt ning iga isiku kohta pisipilt", @@ -229,13 +266,18 @@ "transcoding_video_codec_description": "VP9 on võimekas ja veebiga ühilduv, aga transkodeerimine võtab kauem aega. HEVC on sarnase jõudluse, aga mitte nii hea veebiga ühilduvusega. H.264 on laialt ühilduv ja transkodeerimine on kiire, aga tulemuseks on suuremad failid. AV1 on kõige võimekam koodek, aga pole vanematel seadmetel toetatud.", "trash_number_of_days": "Päevade arv", "trash_number_of_days_description": "Päevade arv, kui kaua hoida üksusi prügikastis enne nende lõplikku kustutamist", + "user_cleanup_job": "Kasutajate korrastamine", "user_delete_delay": "Kasutaja {user} konto ja üksuste lõplik kustutamine on planeeritud {delay, plural, one {# päeva} other {# päeva}} pärast.", "user_delete_delay_settings_description": "Päevade arv, pärast mida kustutatakse eemaldatud kasutaja konto ja üksused jäädavalt. Kasutajate kustutamise tööde käivitub keskööl, et otsida kustutamiseks valmis kasutajaid. Selle seadistuse muudatused rakenduvad järgmisel käivitumisel.", "user_delete_immediately": "Kasutaja {user} konto ja üksused suunatakse koheselt jäädavale kustutamisele.", "user_delete_immediately_checkbox": "Suuna kasutaja ja üksused jäädavale kustutamisele", + "user_management": "Kasutajate haldus", "user_password_has_been_reset": "Kasutaja parool on lähtestatud:", "user_password_reset_description": "Sisesta kasutajale ajutine parool ja teavita teda, et järgmisel sisselogimisel tuleb parool ära muuta.", "user_restore_description": "Kasutaja {user} konto taastatakse.", + "user_restore_scheduled_removal": "Taasta kasutaja - eemaldamine planeeritud {date, date, long}", + "user_settings": "Kasutajate seaded", + "user_settings_description": "Halda kasutajate seadeid", "user_successfully_removed": "Kasutaja {email} on eemaldatud.", "version_check_enabled_description": "Luba versioonikontroll", "version_check_implications": "Versioonikontroll vajab perioodilist ühendumist github.com-iga", @@ -270,11 +312,17 @@ "all_albums": "Kõik albumid", "all_people": "Kõik isikud", "all_videos": "Kõik videod", + "anti_clockwise": "Vastupäeva", + "api_key": "API võti", "api_key_description": "Seda väärtust kuvatakse ainult üks kord. Kopeeri see enne akna sulgemist.", + "api_key_empty": "Su API võtme nimi ei tohiks olla tühi", + "api_keys": "API võtmed", + "app_settings": "Rakenduse seaded", "archive": "Arhiiv", "archive_or_unarchive_photo": "Arhiveeri või taasta foto", "archive_size": "Arhiivi suurus", "archive_size_description": "Seadista arhiivi suurus allalaadimiseks (GiB)", + "are_these_the_same_person": "Kas need on sama isik?", "are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?", "asset_added_to_album": "Lisatud albumisse", "asset_adding_to_album": "Albumisse lisamine...", @@ -307,13 +355,19 @@ "bulk_delete_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid kustutatakse jäädavalt. Seda tegevust ei saa tagasi võtta!", "bulk_keep_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} alles jätta? Sellega märgitakse kõik duplikaadigrupid lahendatuks ilma midagi kustutamata.", "bulk_trash_duplicates_confirmation": "Kas oled kindel, et soovid {count, plural, one {# dubleeritud üksuse} other {# dubleeritud üksust}} masskustutada? Sellega jäetakse alles iga grupi suurim üksus ning duplikaadid liigutatakse prügikasti.", + "buy": "Osta Immich", "camera": "Kaamera", "camera_brand": "Kaamera mark", "camera_model": "Kaamera mudel", "cancel": "Katkesta", + "cancel_search": "Katkesta otsing", "cannot_merge_people": "Ei saa isikuid ühendada", "cannot_undo_this_action": "Sa ei saa seda tagasi võtta!", "cannot_update_the_description": "Kirjelduse muutmine ebaõnnestus", + "change_date": "Muuda kuupäeva", + "change_expiration_time": "Muuda aegumisaega", + "change_location": "Muuda asukohta", + "change_name": "Muuda nime", "change_password": "Parooli muutmine", "change_password_description": "See on su esimene kord süsteemi siseneda, või on tehtud taotlus parooli muutmiseks. Palun sisesta allpool uus parool.", "change_your_password": "Muuda oma parooli", @@ -322,6 +376,10 @@ "check_logs": "Vaata logisid", "choose_matching_people_to_merge": "Vali kattuvad isikud, mida ühendada", "city": "Linn", + "clear": "Tühjenda", + "clear_all": "Tühjenda kõik", + "clear_all_recent_searches": "Tühjenda hiljutised otsingud", + "clear_value": "Tühjenda väärtus", "clockwise": "Päripäeva", "close": "Sulge", "color": "Värv", @@ -335,6 +393,7 @@ "confirm_delete_shared_link": "Kas oled kindel, et soovid selle jagatud lingi kustutada?", "confirm_password": "Kinnita parool", "context": "Kontekst", + "continue": "Jätka", "copied_image_to_clipboard": "Pilt kopeeritud lõikelauale.", "copied_to_clipboard": "Kopeeritud lõikelauale!", "copy_error": "Kopeeri viga", @@ -383,7 +442,9 @@ "delete_user": "Kustuta kasutaja", "deleted_shared_link": "Jagatud link kustutatud", "description": "Kirjeldus", + "details": "Üksikasjad", "direction": "Suund", + "disallow_edits": "Keela muutmine", "discover": "Avasta", "display_options": "Kuva valikud", "display_original_photos_setting_description": "Eelista üksuse vaatamisel pisipildile algset fotot, kui see on veebiga ühilduv. See võib mõjutada fotode kuvamise kiirust.", @@ -454,6 +515,7 @@ "import_path_already_exists": "See imporditee on juba olemas.", "incorrect_email_or_password": "Vale e-posti aadress või parool", "profile_picture_transparent_pixels": "Profiilipildis ei tohi olla läbipaistvaid piksleid. Palun suumi sisse ja/või liiguta pilti.", + "quota_higher_than_disk_size": "Määratud kvoot on suurem kui kettamaht", "unable_to_add_album_users": "Kasutajate lisamine albumisse ebaõnnestus", "unable_to_add_assets_to_shared_link": "Üksuste jagatud lingile lisamine ebaõnnestus", "unable_to_add_comment": "Kommentaari lisamine ebaõnnestus", @@ -463,6 +525,7 @@ "unable_to_add_remove_archive": "{archived, select, true {Üksuse arhiivist taastamine} other {Üksuse arhiveerimine}} ebaõnnestus", "unable_to_add_remove_favorites": "Üksuse {favorite, select, true {lemmikuks lisamine} other {lemmikutest eemaldamine}} ebaõnnestus", "unable_to_archive_unarchive": "{archived, select, true {Arhiveerimine} other {Arhiivist taastamine}} ebaõnnestus", + "unable_to_change_album_user_role": "Kasutaja rolli albumis muutmine ebaõnnestus", "unable_to_change_date": "Kuupäeva muutmine ebaõnnestus", "unable_to_change_favorite": "Üksuse lemmiku staatuse muutmine ebaõnnestus", "unable_to_change_location": "Asukoha muutmine ebaõnnestus", @@ -536,23 +599,32 @@ "expired": "Aegunud", "expires_date": "Aegub {date}", "explore": "Avasta", + "export": "Ekspordi", "export_as_json": "Ekspordi JSON-formaati", "extension": "Laiend", + "external": "Väline", + "external_libraries": "Välised kogud", "face_unassigned": "Seostamata", "favorite": "Lemmik", "favorites": "Lemmikud", "feature_photo_updated": "Esiletõstetud foto muudetud", + "features": "Funktsioonid", + "features_setting_description": "Halda rakenduse funktsioone", "file_name": "Failinimi", "file_name_or_extension": "Failinimi või -laiend", "filename": "Failinimi", "filetype": "Failitüüp", "filter_people": "Filtreeri isikuid", + "find_them_fast": "Leia teda kiiresti nime järgi otsides", "folders": "Kaustad", "folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine", "force_re-scan_library_files": "Sundskaneeri kogu kõik failid uuesti", "forward": "Edasi", "general": "Üldine", + "get_help": "Küsi abi", + "getting_started": "Alustamine", "go_back": "Tagasi", + "go_to_search": "Otsingusse", "group_albums_by": "Grupeeri albumid...", "group_no": "Ära grupeeri", "group_owner": "Grupeeri omaniku kaupa", @@ -575,6 +647,7 @@ "immich_logo": "Immich'i logo", "immich_web_interface": "Immich'i veebiliides", "import_from_json": "Impordi JSON-formaadist", + "in_albums": "{count, plural, one {# albumis} other {# albumis}}", "in_archive": "Arhiivis", "info": "Info", "interval": { @@ -595,9 +668,13 @@ "latest_version": "Uusim versioon", "latitude": "Laiuskraad", "leave": "Lahku", + "let_others_respond": "Luba teistel vastata", "library": "Kogu", "library_options": "Kogu seaded", + "light": "Hele", + "link_options": "Lingi valikud", "list": "Loend", + "loading": "Laadimine", "loading_search_results_failed": "Otsitulemuste laadimine ebaõnnestus", "log_out": "Logi välja", "log_out_all_devices": "Logi kõigist seadmetest välja", @@ -607,15 +684,19 @@ "logout_all_device_confirmation": "Kas oled kindel, et soovid kõigist seadmetest välja logida?", "logout_this_device_confirmation": "Kas oled kindel, et soovid sellest seadmest välja logida?", "longitude": "Pikkuskraad", + "look": "Välimus", "make": "Mark", "manage_shared_links": "Halda jagatud linke", "manage_sharing_with_partners": "Halda partneritega jagamist", + "manage_the_app_settings": "Halda rakenduse seadeid", "manage_your_account": "Halda oma kontot", "manage_your_api_keys": "Halda oma API võtmeid", "manage_your_devices": "Halda oma autenditud seadmeid", "map": "Kaart", "map_settings": "Kaardi seaded", + "media_type": "Meedia tüüp", "memories": "Mälestused", + "memories_setting_description": "Halda, mida sa oma mälestustes näed", "memory": "Mälestus", "menu": "Menüü", "merge": "Ühenda", @@ -642,17 +723,24 @@ "next_memory": "Järgmine mälestus", "no": "Ei", "no_albums_message": "Lisa album fotode ja videote organiseerimiseks", + "no_albums_with_name_yet": "Paistab, et sul pole veel ühtegi selle nimega albumit.", + "no_albums_yet": "Paistab, et sul pole veel ühtegi albumit.", "no_archived_assets_message": "Arhiveeri fotod ja videod, et neid Fotod vaatest peita", "no_assets_message": "KLIKI ESIMESE FOTO ÜLESLAADIMISEKS", "no_duplicates_found": "Ühtegi duplikaati ei leitud.", "no_exif_info_available": "Exif info pole saadaval", + "no_explore_results_message": "Oma kogu avastamiseks laadi üles rohkem fotosid.", "no_favorites_message": "Lisa lemmikud, et oma parimaid fotosid ja videosid kiiresti leida", "no_libraries_message": "Lisa väline kogu oma fotode ja videote vaatamiseks", + "no_results": "Vasteid pole", + "no_results_description": "Proovi sünonüümi või üldisemat märksõna", "no_shared_albums_message": "Lisa album, et fotosid ja videosid teistega jagada", + "notes": "Märkused", "notification_toggle_setting_description": "Luba e-posti teel teavitused", "notifications": "Teavitused", "notifications_setting_description": "Halda teavitusi", "oauth": "OAuth", + "ok": "Ok", "oldest_first": "Vanemad eespool", "onboarding_theme_description": "Vali oma serverile värviteema. Saad seda hiljem seadetes muuta.", "onboarding_welcome_user": "Tere tulemast, {user}", @@ -660,11 +748,13 @@ "only_refreshes_modified_files": "Värskendab ainult muudetud failid", "open_in_map_view": "Ava kaardi vaates", "open_in_openstreetmap": "Ava OpenStreetMap", + "open_the_search_filters": "Ava otsingufiltrid", "options": "Valikud", "or": "või", "organize_your_library": "Korrasta oma kogu", "original": "originaal", "other_devices": "Muud seadmed", + "other_variables": "Muud muutujad", "owned": "Minu omad", "owner": "Omanik", "partner": "Partner", @@ -699,6 +789,8 @@ "permanently_deleted_asset": "Üksus jäädavalt kustutatud", "permanently_deleted_assets_count": "{count, plural, one {# üksus} other {# üksust}} jäädavalt kustutatud", "person": "Isik", + "person_hidden": "{name}{hidden, select, true { (peidetud)} other {}}", + "photo_shared_all_users": "Paistab, et oled oma fotosid kõigi kasutajatega jaganud, või pole ühtegi kasutajat, kellega jagada.", "photos": "Fotod", "photos_and_videos": "Fotod ja videod", "photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotot}}", @@ -739,6 +831,8 @@ "purchase_panel_info_1": "Immich'i arendamine nõuab palju aega ja vaeva ning meie täiskohaga insenerid töötavad selle nimel, et teha see nii heaks kui vähegi võimalik. Meie missiooniks on muuta avatud lähtekoodiga tarkvara ja eetilised äritavad arendajatele jätkusuutlikuks sissetulekuallikaks ning luua privaatsust austav ökosüsteem, mis pakub tõelisi alternatiive ekspluatatiivsetele pilveteenustele.", "purchase_panel_info_2": "Kuna oleme otsustanud maksumüüre mitte lisada, ei anna see ost sulle Immich'is lisavõimalusi. Me loodame Immich'i jätkuvaks arenduseks sinusuguste kasutajate toetusele.", "purchase_panel_title": "Toeta projekti", + "purchase_per_server": "Serveri kohta", + "purchase_per_user": "Kasutaja kohta", "purchase_remove_product_key": "Eemalda tootevõti", "purchase_remove_product_key_prompt": "Kas oled kindel, et soovid tootevõtme eemaldada?", "purchase_remove_server_product_key": "Eemalda serveri tootevõti", @@ -747,10 +841,12 @@ "purchase_server_description_2": "Toetaja staatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Serveri tootevõtit haldab administraator", + "reaction_options": "Reaktsiooni valikud", "read_changelog": "Vaata muudatuste ülevaadet", "reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga", "reassing_hint": "Seosta valitud üksused olemasoleva isikuga", + "recent_searches": "Hiljutised otsingud", "refresh": "Värskenda", "refresh_encoded_videos": "Värskenda kodeeritud videod", "refresh_metadata": "Värskenda metaandmed", @@ -766,11 +862,14 @@ "remove_assets_title": "Eemalda üksused?", "remove_from_album": "Eemalda albumist", "remove_from_favorites": "Eemalda lemmikutest", + "remove_from_shared_link": "Eemalda jagatud lingist", "remove_user": "Eemalda kasutaja", "removed_api_key": "API võti eemaldatud: {name}", "removed_from_archive": "Arhiivist eemaldatud", "removed_from_favorites": "Lemmikutest eemaldatud", + "removed_from_favorites_count": "{count, plural, other {# eemaldatud}} lemmikutest", "removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}", + "rename": "Nimeta ümber", "require_password": "Nõua parooli", "require_user_to_change_password_on_first_login": "Nõua kasutajalt esmakordsel sisenemisel parooli muutmist", "reset": "Lähtesta", @@ -806,8 +905,10 @@ "search_for_existing_person": "Otsi olemasolevat isikut", "search_no_people": "Isikuid ei ole", "search_no_people_named": "Ei ole isikuid nimega \"{name}\"", + "search_options": "Otsingu valikud", "search_people": "Otsi inimesi", "search_places": "Otsi kohti", + "search_settings": "Otsingu seaded", "search_state": "Otsi osariiki...", "search_tags": "Otsi silte...", "search_timezone": "Otsi ajavööndit...", @@ -862,13 +963,18 @@ "show_metadata": "Kuva metaandmed", "show_or_hide_info": "Kuva või peida info", "show_password": "Kuva parooli", + "show_progress_bar": "Kuva edenemisriba", + "show_search_options": "Kuva otsingu valikud", "show_supporter_badge": "Toetaja märk", "show_supporter_badge_description": "Kuva toetaja märki", "sidebar": "Külgmenüü", + "sidebar_display_description": "Kuva külgmenüüs linki vaatele", "sign_out": "Logi välja", "sign_up": "Registreeru", "size": "Suurus", "skip_to_content": "Sisu juurde", + "skip_to_folders": "Kaustade juurde", + "skip_to_tags": "Siltide juurde", "slideshow": "Slaidiesitlus", "slideshow_settings": "Slaidiesitluse seaded", "sort_albums_by": "Järjesta albumid...", @@ -902,6 +1008,7 @@ "theme": "Teema", "theme_selection": "Teema valik", "theme_selection_description": "Sea automaatselt hele või tume teema vastavalt veebilehitseja eelistustele", + "time_based_memories": "Ajapõhised mälestused", "timezone": "Ajavöönd", "to_archive": "Arhiivi", "to_change_password": "Muuda parool", @@ -917,6 +1024,8 @@ "unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?", "unsaved_change": "Salvestamata muudatus", "updated_password": "Parool muudetud", + "upload": "Laadi üles", + "upload_concurrency": "Üleslaadimise samaaegsus", "upload_errors": "Üleslaadimine lõpetatud {count, plural, one {# veaga} other {# veaga}}, uute üksuste nägemiseks värskenda lehte.", "upload_skipped_duplicates": "{count, plural, one {# dubleeritud üksus} other {# dubleeritud üksust}} vahele jäetud", "upload_status_duplicates": "Duplikaadid", @@ -927,6 +1036,7 @@ "user": "Kasutaja", "user_id": "Kasutaja ID", "user_liked": "Kasutajale {user} meeldis {type, select, photo {see foto} video {see video} asset {see üksus} other {see}}", + "user_purchase_settings": "Osta", "user_purchase_settings_description": "Halda oma ostu", "username": "Kasutajanimi", "users": "Kasutajad", @@ -935,6 +1045,7 @@ "variables": "Muutujad", "version": "Versioon", "version_announcement_closing": "Sinu sõber, Alex", + "version_announcement_message": "Hei sõber, saadaval on rakenduse uus versioon. Palun võta aega, et lugeda väljalasketeadet ning veendu, et su docker-compose.yml ja .env failid on ajakohased, et vältida konfiguratsiooniprobleeme, eriti kui kasutad WatchTower'it või muud mehhanismi, mis rakendust automaatselt uuendab.", "video": "Video", "video_hover_setting": "Esita hõljutamisel video eelvaade", "video_hover_setting_description": "Esita video eelvaade, kui hiirt selle kohal hõljutada. Isegi kui keelatud, saab taasesituse alustada taasesitusnupu kohal hõljutades.", diff --git a/web/src/lib/i18n/fa.json b/web/src/lib/i18n/fa.json index 0eb6b7b014f19..7c0fba9f3559b 100644 --- a/web/src/lib/i18n/fa.json +++ b/web/src/lib/i18n/fa.json @@ -144,7 +144,7 @@ "note_cannot_be_changed_later": "توجه: این را نمی توان بعداً تغییر داد!", "note_unlimited_quota": "توجه: برای سهمیه نامحدود، عدد 0 را وارد کنید", "notification_email_from_address": "آدرس فرستنده", - "notification_email_from_address_description": "آدرس ایمیل فرستنده، به عنوان مثال:\"Immich سرور عکس \"", + "notification_email_from_address_description": "آدرس ایمیل فرستنده، به عنوان مثال:\"Immich سرور عکس \"", "notification_email_host_description": "میزبان سرور ایمیل (مثلاً smtp.immich.app)", "notification_email_ignore_certificate_errors": "خطاهای گواهی را نادیده بگیر", "notification_email_ignore_certificate_errors_description": "خطاهای اعتبارسنجی گواهی TLS را نادیده بگیر (توصیه نمی‌شود)", @@ -194,7 +194,7 @@ "refreshing_all_libraries": "بروز رسانی همه کتابخانه ها", "registration": "ثبت نام مدیر", "registration_description": "از آنجایی که شما اولین کاربر در سیستم هستید، به عنوان مدیر تعیین شده‌اید و مسئولیت انجام وظایف مدیریتی بر عهده شما خواهد بود و کاربران اضافی توسط شما ایجاد خواهند شد.", - "removing_offline_files": "حذف فایل‌های آفلاین", + "removing_deleted_files": "حذف فایل‌های آفلاین", "repair_all": "بازسازی همه", "repair_matched_items": "", "repaired_items": "", @@ -524,8 +524,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "", @@ -766,10 +766,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "rename": "", "repair": "", diff --git a/web/src/lib/i18n/fi.json b/web/src/lib/i18n/fi.json index da9a71379cfe5..beeb46a62fd39 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Lisää jaettuun albumiin", "added_to_archive": "Arkistoitu", "added_to_favorites": "Lisätty suosikkeihin", - "added_to_favorites_count": "{count} lisätty suosikkeihin", + "added_to_favorites_count": "{count, number} lisätty suosikkeihin", "admin": { "add_exclusion_pattern_description": "Lisää mallit, jonka mukaan jätetään tiedostoja pois. Jokerimerkit *, ** ja ? ovat tuettuna. Jättääksesi pois kaikki tiedostot mistä tahansa löytyvästä kansiosta \"Raw\" käytä \"**/Raw/**\". Jättääksesi pois kaikki \". tif\" päätteiset tiedot, käytä \"**/*.tif\". Jättääksesi pois tarkan tiedostopolun, käytä \"/path/to/ignore/**\".", "authentication_settings": "Autentikointiasetukset", @@ -41,6 +41,7 @@ "confirm_email_below": "Kirjota \"{email}\" vahvistaaksesi", "confirm_reprocess_all_faces": "Haluatko varmasti käsitellä uudelleen kaikki kasvot? Tämä poistaa myös nimetyt henkilöt.", "confirm_user_password_reset": "Haluatko varmasti nollata käyttäjän {user} salasanan?", + "create_job": "Luo tehtävä", "crontab_guru": "Crontab Guru", "disable_login": "Poista kirjautuminen käytöstä", "disabled": "Ei käytössä", @@ -70,12 +71,13 @@ "image_thumbnail_resolution": "Pikkukuvien resoluutio", "image_thumbnail_resolution_description": "Käytetään katsottaessa useita kuvia kerralla (aikajana, albuminäkymä, jne.) Korkeampi resoluutio antaa enemmän yksityiskohtia, mutta niiden luonti kestää kauemmin, tiedostokoot ovat isompia ja voivat heikentää sovelluksen responsiivisuutta.", "job_concurrency": "{job} yhtäaikaisuus", + "job_created": "Tehtävä luotu", "job_not_concurrency_safe": "Tätä tehtävää ei ole turvallista ajaa yhtäaikaisesti.", "job_settings": "Tehtävän asetukset", "job_settings_description": "Hallitse tehtävän samanaikaisuusasetuksia", "job_status": "Tehtävän tila", - "jobs_delayed": "{jobCount} tehtävää viivästetty", - "jobs_failed": "{jobCount} epäonnistui", + "jobs_delayed": "{jobCount, plural, other {# viivästynyttä}}", + "jobs_failed": "{jobCount, plural, other {# epäonnistunutta}}", "library_created": "Kirjasto {library} luotu", "library_cron_expression": "Cron-lauseke", "library_cron_expression_description": "Anna skannaustiheys cron-formaatissa. Saadaksesi lisätietoja katso esimerkiksi Crontab Guru", @@ -135,11 +137,15 @@ "map_reverse_geocoding": "Käänteinen Geokoodaus", "map_reverse_geocoding_enable_description": "Ota käyttöön osoitteiden poiminta karttakoordinaateista", "map_reverse_geocoding_settings": "Käänteisen Geokoodauksen asetukset", - "map_settings": "Kartta-asetukset", + "map_settings": "Kartta", "map_settings_description": "Hallitse kartan asetuksia", "map_style_description": "style.json -karttateeman URL", "metadata_extraction_job": "Kerää metadata", - "metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS ja resoluutio", + "metadata_extraction_job_description": "Poimi metatiedot aineistoista, kuten GPS, kasvot ja resoluutio", + "metadata_faces_import_setting": "Ota käyttöön kasvojen tuonti", + "metadata_faces_import_setting_description": "Tuo kasvot kuvan EXIF- ja kylkiäistiedostoista", + "metadata_settings": "Metatietoasetukset", + "metadata_settings_description": "Hallitse metatietoja", "migration_job": "Migrointi", "migration_job_description": "Migroi aineiston pikkukuvat ja kasvot uusimpaan kansiorakenteeseen", "no_paths_added": "Polkuja ei asetettu", @@ -148,7 +154,7 @@ "note_cannot_be_changed_later": "Huom: Tätä ei voi enää myöhemmin vaihtaa!", "note_unlimited_quota": "Huom: Määritä 0 rajoittamattomaksi kiintiöksi", "notification_email_from_address": "Lähettäjän osoite", - "notification_email_from_address_description": "Lähettäjän sähköpostiosoite. Esimerkiksi \"Immich Kuvapalvelin \"", + "notification_email_from_address_description": "Lähettäjän sähköpostiosoite. Esimerkiksi \"Immich Kuvapalvelin \"", "notification_email_host_description": "Sähköpostipalvelin (esim. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Älä huomioi sertifikaattivirheitä", "notification_email_ignore_certificate_errors_description": "Älä huomioi TLS sertifikaattien validointivirheitä (ei suositeltu)", @@ -174,9 +180,9 @@ "oauth_issuer_url": "Toimitsijan URL", "oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI", "oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI", - "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun 'app.immich:/' -ohjausta ei tueta.", + "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun OAuth tarjoaja ei salli mobiili URI:a, kuten '{callback}'", "oauth_profile_signing_algorithm": "Profiilin allekirjoitusalgoritmi", - "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoituksessa", + "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoittamiseen.", "oauth_scope": "Skooppi (Scope)", "oauth_settings": "OAuth", "oauth_settings_description": "Hallitse OAuth kirjautumisen asetuksia", @@ -194,11 +200,12 @@ "password_settings": "Kirjaudu salasanalla", "password_settings_description": "Hallitse salasanakirjautumisen asetuksia", "paths_validated_successfully": "Kaikki polut validoitu", + "person_cleanup_job": "Henkilöpuhdistus", "quota_size_gib": "Kiintiön koko (Gt)", "refreshing_all_libraries": "Virkistetään kaikki kirjastot", "registration": "Pääkäyttäjän rekisteröinti", "registration_description": "Pääkäyttäjänä olet vastuussa järjestelmän hallinnallisista tehtävistä ja uusien käyttäjien luomisesta.", - "removing_offline_files": "Poistetaan Offline-tiedostot", + "removing_deleted_files": "Poistetaan Offline-tiedostot", "repair_all": "Korjaa kaikki", "repair_matched_items": "Löytyi {count, plural, one {# osuma} other {# osumaa}}", "repaired_items": "Korjattiin {count, plural, one {# kohta} other {# kohtaa}}", @@ -207,6 +214,7 @@ "reset_settings_to_recent_saved": "Palauta aiemmin tallennetut asetukset", "scanning_library_for_changed_files": "Etsitään kirjaston muuttuneita tiedostoja", "scanning_library_for_new_files": "Etsitään uusia tiedostoja", + "search_jobs": "Etsi tehtäviä...", "send_welcome_email": "Lähetä tervetuloviesti", "server_external_domain_settings": "Ulkoinen osoite", "server_external_domain_settings_description": "Osoite julkisille linkeille, http(s):// mukaan lukien", @@ -234,6 +242,7 @@ "storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä", "storage_template_user_label": "{label} on käyttäjän Tallennustilan Tunniste", "system_settings": "Järjestelmäasetukset", + "tag_cleanup_job": "Merkintäpuhdistus", "theme_custom_css_settings": "Mukautettu CSS", "theme_custom_css_settings_description": "Kustomoi Immichin ulkoasua Cascading Style Sheets:llä.", "theme_settings": "Teeman asetukset", @@ -261,7 +270,7 @@ "transcoding_codecs_learn_more": "Oppiaksesi lisää tässä käytetystä terminologiasta, tutustu FFmpeg- dokumentaatioon H.264 koodaaja, HEVC koodaaja sekä VP9 koodaaja.", "transcoding_constant_quality_mode": "Tasaisen laadun tyyppi", "transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.", - "transcoding_constant_rate_factor": "", + "transcoding_constant_rate_factor": "Vakionopeustekijä", "transcoding_constant_rate_factor_description": "Videon laatu. Yleisimmät arvot ovat 23 H.264:lle, 28 HEVC:lle, 31 VP9:lle ja 35 AV1:lle. Matalampi arvo on parempi, mutta tekee isompia tiedostoja.", "transcoding_disabled_description": "Älä muunna videoita. Voi joissakin päätelaitteissa aiheuttaa videotoiston toimimattomuutta", "transcoding_hardware_acceleration": "Laitteistokiihdytys", @@ -279,7 +288,7 @@ "transcoding_preferred_hardware_device": "Ensisijainen laite", "transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.", "transcoding_preset_preset": "Esiasetus (-asetus)", - "transcoding_preset_preset_description": "Pakkausnopeus. Hitaampi tuottaa pienempiä tiedostoja ja parantaa laatua, kun kohdistetaan tiettyyn bittinopeuteen. VP9 ei huomioi korkeampaa kuin `faster`.", + "transcoding_preset_preset_description": "Pakkausnopeus. Hitaampi tuottaa pienempiä tiedostoja ja parantaa laatua, kun kohdistetaan tiettyyn bittinopeuteen. VP9 ei huomioi korkeampaa kuin 'faster'.", "transcoding_reference_frames": "Kehysviitteet", "transcoding_reference_frames_description": "Viittaavien kehysten määrä kun tiettyä kehystä pakataan. Korkeampi arvo parantaa pakkausta mutta hidastaa enkoodausta. 0 määrittää arvon automaattisesti.", "transcoding_required_description": "Vain videoille, jotka eivät ole hyväksytyssä muodossa", @@ -298,7 +307,7 @@ "transcoding_transcode_policy": "Transkoodauskäytäntö", "transcoding_transcode_policy_description": "Käytäntö miten video tulisi transkoodata. HDR videot transkoodataan aina, paitsi jos transkoodaus on poistettu käytöstä.", "transcoding_two_pass_encoding": "Two-pass enkoodaus", - "transcoding_two_pass_encoding_setting_description": "", + "transcoding_two_pass_encoding_setting_description": "Transkoodaa kahdessa vaiheessa tuottaaksesi paremmin koodattuja videoita. Kun maksimibittinopeus on käytössä (vaaditaan H.264- ja HEVC-koodaukselle), tämä tila käyttää bittinopeusaluetta, joka perustuu maksimibittinopeuteen ja ohittaa CRF. VP9 osalta CRF:ää voidaan käyttää, jos maksimibittinopeus on poistettu käytöstä.", "transcoding_video_codec": "Videokoodekki", "transcoding_video_codec_description": "VP9 on tehokkain ja web-yhteensopiva, mutta muuntaminen kestää kauemmin. HEVC suoriutuu yhtäläisesti, mutta ei ole ihan yhtä yhteensopiva. H.264 on hyvin yhteensopiva ja nopea muuntaa, mutta tuottaa paljon suurempia tiedostoja. AV1 on kaikkein tehokkain koodekki, mutta vanhemmat laitteet eivät sitä tue.", "trash_enabled_description": "Ota käyttöön roskakori", @@ -308,15 +317,22 @@ "trash_settings_description": "Hallitse roskakoriasetuksia", "untracked_files": "Tiedostot joita ei seurata", "untracked_files_description": "Nämä tiedostot eivät ole ohjelman hallitsemia. Ne voivat olla virheellisten siirtojen tai keskeytyneiden latausten tulosta, tai bugista johtuvia jälkeen jääneitä", + "user_cleanup_job": "Käyttäjien puhdistus", + "user_delete_delay": "Käyttäjän {user} tili ja aineistot aikataulutetaan poistettavaksi ajan kuluttua: {delay, plural, one {# day} other {# days}}.", "user_delete_delay_settings": "Poiston viive", "user_delete_delay_settings_description": "Montako päivää poistamisen jälkeen käyttäjä ja hänen aineistonsa poistetaan pysyvästi. Joka keskiyö käydään läpi poistetuiksi merkityt käyttäjät. Tämä muutos astuu voimaan seuraavalla ajokerralla.", + "user_delete_immediately": "{user}:n tili ja sen kohteet on ajastettu poistettavaksi heti.", + "user_delete_immediately_checkbox": "Aseta tili ja sen kohteet jonoon välitöntä poistoa varten", "user_management": "Käyttäjien hallinta", "user_password_has_been_reset": "Käyttäjän salasana on nollattu:", "user_password_reset_description": "Anna väliaikainen salasana ja ohjeista käyttäjää vaihtamaan se seuraavan kirjautumisen yhteydessä.", + "user_restore_description": "{user}:n tili palautetaan.", + "user_restore_scheduled_removal": "Palauta käyttäjä - Aikataulutettu poisto tapahtuu {date, date, long}", "user_settings": "Käyttäjäasetukset", "user_settings_description": "Hallitse käyttäjäasetuksia", "user_successfully_removed": "Käyttäjä {email} on poistettu.", - "version_check_enabled_description": "Ota käyttöön säännölliset uusien versioiden tarkistukset GitHubista", + "version_check_enabled_description": "Ota käyttöön versiotarkastus", + "version_check_implications": "Versiontarkistus vaatii säännöllisen yhteyden github.com:iin", "version_check_settings": "Versiotarkistus", "version_check_settings_description": "Ota käyttöön ilmoitukset, kun uusi versio on saatavilla", "video_conversion_job": "Transkoodaa videot", @@ -332,17 +348,21 @@ "album_added": "Albumi lisätty", "album_added_notification_setting_description": "Saa sähköpostia kun sinut lisätään jaettuun albumiin", "album_cover_updated": "Albumin kansikuva päivitetty", - "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?\nJos albumi on jaettu, muut eivät pääse siihen enää.", + "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?", + "album_delete_confirmation_description": "Jos albumi on jaettu, muut eivät pääse siihen enää.", "album_info_updated": "Albumin tiedot päivitetty", "album_leave": "Poistu albumista?", + "album_leave_confirmation": "Haluatko varmasti poistua albumista {album}?", "album_name": "Albumin nimi", "album_options": "Albumin asetukset", "album_remove_user": "Poista käyttäjä?", - "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", + "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", "album_updated": "Albumi päivitetty", "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä", + "album_user_left": "Poistuttiin albumista {album}", "album_user_removed": "{user} poistettu", + "album_with_link_access": "Anna kenen tahansa nähdä linkin kautta tämän albumin valokuvat ja henkilöt.", "albums": "Albumit", "albums_count": "{count, plural, one {{count, number} albumi} other {{count, number} albumia}}", "all": "Kaikki", @@ -351,7 +371,12 @@ "all_videos": "Kaikki videot", "allow_dark_mode": "Salli tumma tila", "allow_edits": "Salli muutokset", + "allow_public_user_to_download": "Salli julkisten käyttäjien ladata tiedostoja", + "allow_public_user_to_upload": "Salli julkisten käyttäjien lähettää tiedostoja", + "anti_clockwise": "Vastapäivään", "api_key": "API-avain", + "api_key_description": "Tämä arvo näytetään vain kerran. Varmista, että olet kopioinut sen ennen kuin suljet ikkunan.", + "api_key_empty": "API-avaimesi ei pitäisi olla tyhjä", "api_keys": "API-avaimet", "app_settings": "Sovellusasetukset", "appears_in": "Esiintyy albumeissa", @@ -365,14 +390,20 @@ "are_you_sure_to_do_this": "Haluatko varmasti tehdä tämän?", "asset_added_to_album": "Lisätty albumiin", "asset_adding_to_album": "Lisätään albumiin...", + "asset_description_updated": "Kohteen kuvaus on päivitetty", + "asset_filename_is_offline": "Kohde {filename} on offline-tilassa", + "asset_has_unassigned_faces": "Kohteella on määrittämättömiä kasvoja", + "asset_hashing": "Hajautetaan...", "asset_offline": "Aineisto offline-tilassa", + "asset_offline_description": "Tämä kohde on offline-tilassa. Immich ei pääse tiedoston sijaintiin. Varmista, että kohde on saatavilla, ja skannaa sitten kirjasto uudelleen.", "asset_skipped": "Ohitettu", + "asset_skipped_in_trash": "Roskakorissa", "asset_uploaded": "Lähetetty", "asset_uploading": "Lähetetään…", "assets": "kohdetta", "assets_added_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}}", "assets_added_to_album_count": "Albumiin lisätty {count, plural, one {# kohde} other {# kohdetta}}", - "assets_added_to_name_count": "{name}:n lisätty {count, plural, one {# media} other {# mediaa}}", + "assets_added_to_name_count": "Lisätty {count, plural, one {# kohde} other {# kohdetta}} {hasName, select, true {{name}} other {uuteen albumiin}}", "assets_count": "{count, plural, one {# media} other {# mediaa}}", "assets_moved_to_trash": "Siirretty {count, plural, one {# aineisto} other {# aineistoa}} roskakoriin", "assets_moved_to_trash_count": "Siirretty {count, plural, one {# media} other {# mediaa}} roskakoriin", @@ -394,6 +425,7 @@ "bulk_delete_duplicates_confirmation": "Haluatko varmasti poistaa {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} kerralla? Tämä säilyttää kustakin mediasta kookkaimman ja poistaa loput pysyvästi. Et voi perua tätä!", "bulk_keep_duplicates_confirmation": "Haluatko varmasti säilyttää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}}? Tämä merkitsee kaikki kaksoiskappaleet ratkaistuiksi, eikä poista mitään.", "bulk_trash_duplicates_confirmation": "Haluatko varmasti siirtää {count, plural, one {# kaksoiskappaleen} other {# kaksoiskappaleet}} roskakoriin? Tämä säilyttää kustakin mediasta kookkaimman ja siirtää loput roskakoriin.", + "buy": "Osta lisenssi Immich:iin", "camera": "Kamera", "camera_brand": "Kameran merkki", "camera_model": "Kameran malli", @@ -411,7 +443,7 @@ "change_location": "Vaihda sijainti", "change_name": "Vaihda nimi", "change_name_successfully": "Nimi vaihdettu", - "change_password": "Vaihda salasana", + "change_password": "Vaihda Salasana", "change_password_description": "Tämä on joko ensimmäinen kertasi kun kirjaudut järjestelmään, tai salasanasi on pyydetty vaihtamaan. Määritä uusi salasana alle.", "change_your_password": "Vaihda salasanasi", "changed_visibility_successfully": "Näkyvyys vaihdettu", @@ -421,11 +453,14 @@ "city": "Kaupunki", "clear": "Tyhjennä", "clear_all": "Tyhjennä kaikki", + "clear_all_recent_searches": "Tyhjennä viimeisimmät haut", "clear_message": "Tyhjennä viesti", "clear_value": "Tyhjää arvo", + "clockwise": "Myötäpäivään", "close": "Sulje", "collapse": "Supista", "collapse_all": "Sulje kaikki", + "color": "Väri", "color_theme": "Väriteema", "comment_deleted": "Kommentti poistettu", "comment_options": "Kommentin valinnat", @@ -459,13 +494,15 @@ "create_new_person": "Luo uusi henkilö", "create_new_person_hint": "Määritä valitut mediat uudelle henkilölle", "create_new_user": "Luo uusi käyttäjä", + "create_tag": "Luo tunniste", + "create_tag_description": "Luo uusi tunniste. Sisäkkäisiä tunnisteita varten, syötä tunnisteen täydellinen polku kauttaviiva mukaanluettuna.", "create_user": "Luo käyttäjä", "created": "Luotu", "current_device": "Nykyinen laite", "custom_locale": "Muokatut maa-asetukset", "custom_locale_description": "Muotoile päivämäärät ja numerot perustuen alueen kieleen", "dark": "Tumma", - "date_after": "Päivä jälkeen", + "date_after": "Päivämäärän jälkeen", "date_and_time": "Päivämäärä ja aika", "date_before": "Päivä ennen", "date_of_birth_saved": "Syntymäaika tallennettu", @@ -482,6 +519,8 @@ "delete_library": "Poista kirjasto", "delete_link": "Poista linkki", "delete_shared_link": "Poista jaettu linkki", + "delete_tag": "Poista tunniste", + "delete_tag_confirmation_prompt": "Haluatko varmasti poistaa {tagName}-tunnisteen?", "delete_user": "Poista käyttäjä", "deleted_shared_link": "Jaettu linkki poistettu", "description": "Kuvaus", @@ -499,6 +538,8 @@ "do_not_show_again": "Älä näytä tätä enää", "done": "Valmis", "download": "Lataa", + "download_include_embedded_motion_videos": "Upotetut videot", + "download_include_embedded_motion_videos_description": "Sisällytä liikekuviin upotetut videot erillisinä tiedostoina", "download_settings": "Lataukset", "download_settings_description": "Hallitse aineiston lataukseen liittyviä asetuksia", "downloading": "Ladataan", @@ -528,10 +569,15 @@ "edit_location": "Muokkaa sijaintia", "edit_name": "Muokkaa nimeä", "edit_people": "Muokkaa henkilöitä", + "edit_tag": "Muokkaa tunnistetta", "edit_title": "Muokkaa otsikkoa", "edit_user": "Muokkaa käyttäjää", "edited": "Muokattu", - "editor": "", + "editor": "Editori", + "editor_close_without_save_prompt": "Muutoksia ei tallenneta", + "editor_close_without_save_title": "Suljetaanko editori?", + "editor_crop_tool_h2_aspect_ratios": "Kuvasuhteet", + "editor_crop_tool_h2_rotation": "Rotaatio", "email": "Sähköposti", "empty": "", "empty_album": "", @@ -559,6 +605,7 @@ "error_adding_users_to_album": "Käyttäjiä ei voitu lisätä albumiin", "error_deleting_shared_user": "Jaettua käyttäjää ei voitu poistaa", "error_downloading": "Tiedostoa {filename} ei voitu ladata", + "error_hiding_buy_button": "Virhe osta-painikkeen piilottamisessa", "error_removing_assets_from_album": "Medioiden poisto epäonnistui. Katso konsolista lisätietoja", "error_selecting_all_assets": "Kaikkia medioita ei voitu valita", "exclusion_pattern_already_exists": "Tämä poissulkemismalli on jo olemassa.", @@ -569,6 +616,8 @@ "failed_to_get_people": "Henkilöiden haku epäonnistui", "failed_to_load_asset": "Kohteen lataus epäonnistui", "failed_to_load_assets": "Kohteiden lataus epäonnistui", + "failed_to_load_people": "Henkilöiden lataus epäonnistui", + "failed_to_remove_product_key": "Tuoteavaimen poistaminen epäonnistui", "failed_to_stack_assets": "Medioiden pinoaminen epäonnistui", "failed_to_unstack_assets": "Medioiden pinoamisen purku epäonnistui", "import_path_already_exists": "Tämä tuontipolku on jo olemassa.", @@ -576,54 +625,90 @@ "paths_validation_failed": "{paths, plural, one {# polun} other {# polun}} validointi epäonnistui", "profile_picture_transparent_pixels": "Profiilikuvassa ei voi olla läpinäkyviä pikseleitä. Zoomaa lähemmäs ja/tai siirrä kuvaa.", "quota_higher_than_disk_size": "Asettamasi kiintiö on suurempi kuin levyn koko", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", + "repair_unable_to_check_items": "Ei voida tarkistaa {count, select, one {kohdetta} other {kohteita}}", + "unable_to_add_album_users": "Käyttäjiä ei voi lisätä albumiin", + "unable_to_add_assets_to_shared_link": "Medioiden lisääminen jaettuun linkkiin epäonnistui", + "unable_to_add_comment": "Kommentin lisääminen epäonnistui", + "unable_to_add_exclusion_pattern": "Ei voida lisätä poissulkuohjetta", + "unable_to_add_import_path": "Tuontipolkua ei voitu lisätä", + "unable_to_add_partners": "Kumppaneita ei voitu lisätä", + "unable_to_add_remove_archive": "Ei voida {archived, select, true {poistaa kohdetta arkistosta} other {lisätä kohdetta arkistoon}}", + "unable_to_add_remove_favorites": "Ei voida {favorite, select, true {lisätä kohdetta suosikkeihin} other {poistaa kohdetta suosikeista}}", + "unable_to_archive_unarchive": "Ei voida {archived, select, true {arkistoida} other {poistaa arkistosta}}", + "unable_to_change_album_user_role": "Albumin käyttäjän roolia ei voitu muuttaa", + "unable_to_change_date": "Päivämäärää ei voitu muuttaa", + "unable_to_change_favorite": "Ei voida muuttaa suosikkia kohteelle", "unable_to_change_location": "Sijainnin muuttaminen epäonnistui", "unable_to_change_password": "Salasanan vaihto epäonnistui", + "unable_to_change_visibility": "Ei voida muuttaa näkyvyyttä {count, plural, one {# henkilölle} other {# henkilölle}}", "unable_to_check_item": "", "unable_to_check_items": "", + "unable_to_complete_oauth_login": "OAuth-kirjautumista ei voitu suorittaa loppuun", + "unable_to_connect": "Yhteyttä ei voitu muodostaa", "unable_to_connect_to_server": "Palvelimeen ei saatu yhteyttä", + "unable_to_copy_to_clipboard": "Leikepöydälle ei voitu kopioida, varmista että käytät sivua https-yhteyden kautta", "unable_to_create_admin_account": "Pääkäyttäjän luominen epäonnistui", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_create_api_key": "Uuden API-avaimen luominen epäonnistui", + "unable_to_create_library": "Kirjaston luominen epäonnistui", + "unable_to_create_user": "Käyttäjän luominen epäonnistui", + "unable_to_delete_album": "Albumin poistaminen epäonnistui", + "unable_to_delete_asset": "Kohteen poistaminen epäonnistui", + "unable_to_delete_assets": "Virhe kohteen poistamisessa", + "unable_to_delete_exclusion_pattern": "Ei voida poistaa poissulkuohjetta", + "unable_to_delete_import_path": "Tuontipolkua ei voitu poistaa", + "unable_to_delete_shared_link": "Jaetun linkin poistaminen epäonnistui", + "unable_to_delete_user": "Käyttäjän poistaminen epäonnistui", + "unable_to_download_files": "Tiedostojen lataaminen epäonnistui", + "unable_to_edit_exclusion_pattern": "Ei voida muokata poissulkuohjetta", + "unable_to_edit_import_path": "Tuontipolkua ei voitu muokata", + "unable_to_empty_trash": "Roskakorin tyhjentäminen epäonnistui", + "unable_to_enter_fullscreen": "Koko ruudun tilaan siirtyminen epäonnistui", + "unable_to_exit_fullscreen": "Koko ruudun tilasta poistuminen epäonnistui", + "unable_to_get_comments_number": "Kommenttien määrän hakeminen epäonnistui", + "unable_to_get_shared_link": "Jaetun linkin hakeminen epäonnistui", + "unable_to_hide_person": "Henkilön piilottaminen epäonnistui", + "unable_to_link_motion_video": "Liikekuvan linkitys epäonnistui", + "unable_to_link_oauth_account": "OAuth-tilin linkittäminen epäonnistui", + "unable_to_load_album": "Albumin lataaminen epäonnistui", + "unable_to_load_asset_activity": "Ei voitu ladata kohteen toimintaa", + "unable_to_load_items": "Kohteiden lataaminen epäonnistui", + "unable_to_load_liked_status": "Ei voitu ladata tykkäyksen tilaa", + "unable_to_log_out_all_devices": "Kaikkien laitteiden uloskirjautuminen epäonnistui", + "unable_to_log_out_device": "Laitteen uloskirjautuminen epäonnistui", + "unable_to_login_with_oauth": "OAuth-kirjautuminen epäonnistui", + "unable_to_play_video": "Videon toistaminen epäonnistui", + "unable_to_reassign_assets_existing_person": "Ei voida siirtää kohteita {name, select, null {olemassa olevalle henkilölle} other {{name}}}", + "unable_to_reassign_assets_new_person": "Ei voida siirtää kohteita uudelle henkilölle", + "unable_to_refresh_user": "Käyttäjän päivittäminen epäonnistui", + "unable_to_remove_album_users": "Käyttäjien poistaminen albumista epäonnistui", + "unable_to_remove_api_key": "API-avaimen poistaminen epäonnistui", + "unable_to_remove_assets_from_shared_link": "kohteiden poistaminen jaetusta linkistä epäonnistui", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "Kirjaston poistaminen epäonnistui", + "unable_to_remove_offline_files": "Offline-tiedostojen poistaminen epäonnistui", + "unable_to_remove_partner": "Kumppanin poistaminen epäonnistui", + "unable_to_remove_reaction": "Reaktion poistaminen epäonnistui", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", + "unable_to_repair_items": "Kohteiden korjaaminen epäonnistui", + "unable_to_reset_password": "Salasanan nollaaminen epäonnistui", + "unable_to_resolve_duplicate": "Virheilmoitus näkyy, kun palvelin palauttaa virheen painettaessa roskakorin tai säilytä-painiketta.", + "unable_to_restore_assets": "Kohteen palauttaminen epäonnistui", + "unable_to_restore_trash": "Kohteiden palauttaminen epäonnistui", + "unable_to_restore_user": "Käyttäjän palauttaminen epäonnistui", + "unable_to_save_album": "Albumin tallentaminen epäonnistui", + "unable_to_save_api_key": "API-avaimen tallentaminen epäonnistui", + "unable_to_save_date_of_birth": "Syntymäajan tallentaminen epäonnistui", + "unable_to_save_name": "Nimen tallentaminen epäonnistui", + "unable_to_save_profile": "Profiilin tallentaminen epäonnistui", + "unable_to_save_settings": "Asetusten tallentaminen epäonnistui", + "unable_to_scan_libraries": "Kirjastojen skannaaminen epäonnistui", + "unable_to_scan_library": "Kirjaston skannaaminen epäonnistui", + "unable_to_set_feature_photo": "Ei voida asettaa ominaiskuvaa", "unable_to_set_profile_picture": "Profiilikuvan asetus epäonnistui", "unable_to_submit_job": "Työtä ei voitu lähettää", "unable_to_trash_asset": "Median siirto roskakoriin epäonnistui", "unable_to_unlink_account": "Tunnuksen irroitus epäonnistui", + "unable_to_unlink_motion_video": "Ei voida irrottaa liikevideota", "unable_to_update_album_cover": "Albumin kannen päivitys epäonnistui", "unable_to_update_album_info": "Albumin tietojen päivitys epäonnistui", "unable_to_update_library": "Kirjaston päivitys epäonnistui", @@ -644,59 +729,82 @@ "expired": "Voimassaolo päättynyt", "expires_date": "Vanhenee {date}", "explore": "Tutki", + "explorer": "Tutkija", "export": "Vie", "export_as_json": "Vie JSON-muodossa", - "extension": "", - "external_libraries": "", + "extension": "Tiedostopääte", + "external": "Ulkoisesta", + "external_libraries": "Ulkoiset kirjastot", + "face_unassigned": "Ei määritelty", "failed_to_get_people": "", "favorite": "Suosikki", - "favorite_or_unfavorite_photo": "", + "favorite_or_unfavorite_photo": "Suosikki- tai ei-suosikkikuva", "favorites": "Suosikit", "feature": "", "feature_photo_updated": "Kansikuva ladattu", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", + "features": "Ominaisuudet", + "features_setting_description": "Hallitse sovelluksen ominaisuuksia", + "file_name": "Tiedoston nimi", + "file_name_or_extension": "Tiedostonimi tai tiedostopääte", "filename": "Tiedostonimi", "files": "", "filetype": "Tiedostotyyppi", - "filter_people": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", + "filter_people": "Suodata henkilöt", + "find_them_fast": "Löydä nopeasti hakemalla nimellä", + "fix_incorrect_match": "Korjaa virheellinen osuma", + "folders": "Kansiot", + "folders_feature_description": "Käytetään kansionäkymää valokuvien ja videoiden selaamiseen järjestelmässä", + "force_re-scan_library_files": "Pakota kaikkien kirjastotiedostojen uudelleenskannaus", "forward": "Eteenpäin", - "general": "", - "get_help": "", - "getting_started": "", + "general": "Yleinen", + "get_help": "Hae apua", + "getting_started": "Aloittaminen", "go_back": "Palaa", - "go_to_search": "", + "go_to_search": "Siirry hakuun", "go_to_share_page": "", - "group_albums_by": "", + "group_albums_by": "Ryhmitä albumi...", "group_no": "Ei ryhmitystä", "group_owner": "Ryhmitä omistajan mukaan", "group_year": "Ryhmitä vuoden mukaan", - "has_quota": "", + "has_quota": "On kiintiö", "hi_user": "Hei {name} ({email})", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", + "hide_all_people": "Piilota kaikki henkilöt", + "hide_gallery": "Piilota galleria", + "hide_named_person": "Piilota henkilön {name}", + "hide_password": "Piilota salasana", + "hide_person": "Piilota henkilö", + "hide_unnamed_people": "Piilota nimeämättömät henkilöt", + "host": "Isäntä", "hour": "Tunti", "image": "Kuva", + "image_alt_text_date": "{isVideo, select, true {Video} other {Kuva}} otettu {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Kuva}} otettu {person1} kanssa {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n ja {person2}n kanssa {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n, {person2}n ja {person3}n kanssa {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Kuva}} otettu {person1}n, {person2}n ja {additionalCount, number} muissa kanssa {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n kanssa {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n ja {person2}n kanssa {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n, {person2}n ja {person3}n kanssa {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Kuva}} otettu {city}ssä, {country}ssä {person1}n, {person2}n ja {additionalCount, number} muun kanssa {date}", "img": "", - "immich_logo": "", - "import_path": "", + "immich_logo": "Immich Logo", + "immich_web_interface": "Immich verkkoliittymä", + "import_from_json": "Tuo JSON-tiedostosta", + "import_path": "Tuontipolku", "in_albums": "{count, plural, one {# Albumissa} other {# albumissa}}", "in_archive": "Arkistossa", "include_archived": "Sisällytä arkistoidut", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", + "include_shared_albums": "Sisällytä jaetut albumit", + "include_shared_partner_assets": "Sisällytä jaetut kumppanikohteet", + "individual_share": "Yksittäinen jako", "info": "Lisätietoja", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "Joka päivä klo 13:00", + "hours": "Joka {hours, plural, one {tunti} other {{hours, number} tuntia}}", + "night_at_midnight": "Joka yö keskiyöllä", + "night_at_twoam": "Joka yö klo 02:00" }, "invite_people": "Kutsu ihmisiä", "invite_to_album": "Kutsu albumiin", @@ -710,47 +818,58 @@ "language_setting_description": "Valitse suosimasi kieli", "last_seen": "Viimeksi nähty", "latest_version": "Viimeisin versio", + "latitude": "Leveysaste", "leave": "Lähde", "let_others_respond": "Anna muiden vastata", "level": "Taso", "library": "Kirjasto", - "library_options": "", + "library_options": "Kirjastovaihtoehdot", "license_button_buy": "Osta", "license_button_select": "Valitse", "light": "Vaalea", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", + "like_deleted": "Tykkäys poistettu", + "link_motion_video": "Linkitä liikevideo", + "link_options": "Linkin asetukset", + "link_to_oauth": "Linkki OAuth", + "linked_oauth_account": "Linkitetty OAuth-tili", "list": "Lista", "loading": "Ladataan", - "loading_search_results_failed": "", + "loading_search_results_failed": "Hakutulosten lataaminen epäonnistui", "log_out": "Kirjaudu ulos", "log_out_all_devices": "Kirjaudu ulos kaikilta laitteilta", + "logged_out_all_devices": "Kaikki laitteet kirjattu ulos", + "logged_out_device": "Laite kirjattu ulos", "login": "Kirjaudu", "login_has_been_disabled": "Kirjautuminen on otettu pois käytöstä.", "logout_all_device_confirmation": "Haluatko varmasti kirjautua ulos kaikilta laitteilta?", "logout_this_device_confirmation": "Haluatko varmasti kirjautua ulos näiltä laitteilta?", + "longitude": "Pituusaste", "look": "Tyyli", - "loop_videos": "", - "loop_videos_description": "", + "loop_videos": "Toista videot uudelleen", + "loop_videos_description": "Ota käyttöön videon automaattinen toisto tarkemmassa näkymässä.", "make": "Valmistaja", "manage_shared_links": "Hallitse jaettuja linkkejä", - "manage_sharing_with_partners": "", + "manage_sharing_with_partners": "Hallitse jakamista kumppaneille", "manage_the_app_settings": "Hallitse sovelluksen asetuksia", "manage_your_account": "Hallitse tiliäsi", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", + "manage_your_api_keys": "Hallitse API-avaimiasi", + "manage_your_devices": "Hallitse sisäänkirjautuneita laitteitasi", + "manage_your_oauth_connection": "Hallitse OAuth-yhteyttäsi", "map": "Kartta", - "map_marker_with_image": "", + "map_marker_for_images": "Karttamarkerointi kuville, jotka on otettu {city}ssä, {country}ssä", + "map_marker_with_image": "Karttamarkerointi kuvalla", "map_settings": "Kartta-asetukset", + "matches": "Osumia", "media_type": "Median tyyppi", - "memories": "", - "memories_setting_description": "", + "memories": "Muistoja", + "memories_setting_description": "Hallitse mitä näet muistoissasi", "memory": "Muisto", + "memory_lane_title": "Muistojen polku {title}", "menu": "Valikko", "merge": "Yhdistä", "merge_people": "Yhdistä henkilöt", + "merge_people_limit": "Voit yhdistää vain enintään 5 kasvoa kerrallaan", + "merge_people_prompt": "Haluatko yhdistää nämä henkilöt? Tätä valintaa ei voi peruuttaa.", "merge_people_successfully": "Henkilöt yhdistetty", "merged_people_count": "{count, plural, one {# Henkilö} other {# henkilöä}} yhdistetty", "minimize": "PIenennä", @@ -764,6 +883,7 @@ "name": "Nimi", "name_or_nickname": "Nimi tai lempinimi", "never": "ei koskaan", + "new_album": "Uusi Albumi", "new_api_key": "Uusi API Key", "new_password": "Uusi salasana", "new_person": "Uusi henkilö", @@ -776,42 +896,55 @@ "no_albums_message": "Luo albumi pitääksesi kuvat ja videot järjestyksessä", "no_albums_with_name_yet": "Näyttää siltä, ettei sinulla ole yhtään tämän nimistä albumia.", "no_albums_yet": "Näyttää siltä, ettei sinulla ole vielä yhtään albumia.", - "no_archived_assets_message": "", + "no_archived_assets_message": "Arkistoi kuvia ja videoita piilottaaksesi ne kuvat näkymästä", "no_assets_message": "NAPAUTA LATAAKSESI ENSIMMÄISEN KUVASI", + "no_duplicates_found": "Kaksoiskappaleita ei löytynyt.", "no_exif_info_available": "EXIF-tietoa ei saatavilla", - "no_explore_results_message": "", + "no_explore_results_message": "Lataa lisää kuvia tutkiaksesi kokoelmaasi.", "no_favorites_message": "Lisää suosikkeja löytääksesi nopeasti parhaat kuvasi ja videosi", - "no_libraries_message": "", + "no_libraries_message": "Luo ulkoinen kirjasto nähdäksesi valokuvasi ja videot", "no_name": "Ei nimeä", - "no_places": "", + "no_places": "Ei paikkoja", "no_results": "Ei tuloksia", + "no_results_description": "Kokeile synonyymiä tai yleisempää avainsanaa", "no_shared_albums_message": "Luo albumi, jotta voit jakaa kuvia ja videoita toisille", "not_in_any_album": "Ei yhdessäkään albumissa", + "note_apply_storage_label_to_previously_uploaded assets": "Huom: Jotta voit soveltaa tallennustunnistetta aiemmin ladattuihin kohteisiin, suorita", + "note_unlimited_quota": "Huomio: Syötä 0 rajoittamatonta kiintiötä varten", "notes": "Muistiinpanot", "notification_toggle_setting_description": "Ota sähköpostilmoitukset käyttöön", "notifications": "Ilmoitukset", "notifications_setting_description": "Hallitse ilmoituksia", "oauth": "OAuth", - "offline": "", + "offline": "Offline", + "offline_paths": "Offline-polut", + "offline_paths_description": "Nämä tulokset voivat johtua tiedostojen manuaalisesta poistamisesta, jotka eivät ole osa ulkoista kirjastoa.", "ok": "Ok", "oldest_first": "Vanhin ensin", + "onboarding": "Käyttöönotto", + "onboarding_privacy_description": "Seuraavat (valinnaiset) ominaisuudet perustuvat ulkoisiin palveluihin, ja ne voidaan poistaa käytöstä milloin tahansa hallinta asetuksista.", + "onboarding_theme_description": "Valitse väriteema istunnollesi. Voit muuttaa tämän myöhemmin asetuksistasi.", + "onboarding_welcome_description": "Aloitetaa laittamalla istuntoosi joitakin yleisiä asetuksia.", "onboarding_welcome_user": "Tervetuloa {user}", "online": "Online", "only_favorites": "Vain suosikit", - "only_refreshes_modified_files": "", + "only_refreshes_modified_files": "Päivittää vain muakatut tiedostot", + "open_in_map_view": "Avaa karttanäkymässä", "open_in_openstreetmap": "Avaa OpenStreetMapissa", - "open_the_search_filters": "", + "open_the_search_filters": "Avaa hakusuodattimet", "options": "Vaihtoehdot", "or": "tai", "organize_your_library": "Järjestele kirjastosi", "original": "alkuperäinen", "other": "Muut", "other_devices": "Toiset laitteet", - "other_variables": "", + "other_variables": "Muut muuttujat", "owned": "Omistettu", "owner": "Omistaja", "partner": "Kumppani", "partner_can_access": "{partner} voi päästä", + "partner_can_access_assets": "Kaikki valokuvasi ja videosi, lukuun ottamatta arkistoituja ja poistettuja", + "partner_can_access_location": "Sijainti, jossa kuvasi on otettu", "partner_sharing": "Kumppanijako", "partners": "Kumppanit", "password": "Salasana", @@ -819,22 +952,26 @@ "password_required": "Salasana vaaditaan", "password_reset_success": "Salasanan nollaus onnistui", "past_durations": { - "days": "{years, plural, one {Viimeisin päivä} other {Viimeiset # päivää}}", - "hours": "{years, plural, one {Viimeisin tunti} other {Viimeiset # tuntia}}", + "days": "Viime {days, plural, one {päivä} other {# päivää}}", + "hours": "Viime {hours, plural, one {tunti} other {# tuntia}}", "years": "{years, plural, one {Viimeisin vuosi} other {Viimeiset # vuotta}}" }, "path": "Polku", - "pattern": "", + "pattern": "Kaava", "pause": "Tauko", - "pause_memories": "", + "pause_memories": "Pysäytä muistot", "paused": "Tauotettu", "pending": "Odottaa", "people": "Ihmiset", - "people_sidebar_description": "", + "people_edits_count": "Muokattu {count, plural, one {# henkilö} other {# henkilöä}}", + "people_feature_description": "Selataan valokuvia ja videoita, jotka on ryhmitelty henkilöiden mukaan", + "people_sidebar_description": "Näytä linkki Henkilöihin sivupalkissa", "perform_library_tasks": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", + "permanent_deletion_warning": "Pysyvän poiston varoitus", + "permanent_deletion_warning_setting_description": "Näytä varoitus, kun poistat kohteita pysyvästi", "permanently_delete": "Poista pysyvästi", + "permanently_delete_assets_count": "Poista pysyvästi {count, plural, one {kohde} other {kohteita}}", + "permanently_delete_assets_prompt": "Oletko varma, että haluat poistaa pysyvästi {count, plural, one {tämän kohteen?} other {nämä # kohteet?}} Tämä poistaa myös {count, plural, one {sen sen} other {ne niiden}} albumista.", "permanently_deleted_asset": "Media poistettu pysyvästi", "permanently_deleted_assets_count": "{count, plural, one {# media} other {# mediaa}} poistettu pysyvästi", "person": "Henkilö", @@ -849,7 +986,7 @@ "places": "Paikat", "play": "Toista", "play_memories": "Toista muistot", - "play_motion_photo": "", + "play_motion_photo": "Toista Liikekuva", "play_or_pause_video": "Toista tai keskeytä video", "point": "", "port": "Portti", @@ -859,15 +996,53 @@ "previous_memory": "Edellinen muisto", "previous_or_next_photo": "Edellinen tai seuraava kuva", "primary": "Ensisijainen", + "privacy": "Yksityisyys", "profile_image_of_user": "Käyttäjän {user} profiilikuva", "profile_picture_set": "Profiilikuva asetettu.", "public_album": "Julkinen albumi", "public_share": "Julkinen jako", + "purchase_account_info": "Tukija", + "purchase_activated_subtitle": "Kiitos Immichin ja avoimen lähdekoodin ohjelmiston tukemisesta", + "purchase_activated_time": "Aktivoitu {date, date}", + "purchase_activated_title": "Avaimesi on aktivoitu onnistuneesti", + "purchase_button_activate": "Aktivoi", + "purchase_button_buy": "Osta", + "purchase_button_buy_immich": "Osta Immich", + "purchase_button_never_show_again": "Älä näytä koskaan uudelleen", + "purchase_button_reminder": "Muistuta minua 30 päivän kuluessa", + "purchase_button_remove_key": "Poista avain", + "purchase_button_select": "Valitse", + "purchase_failed_activation": "Aktivointi epäonnistui! Tarkista sähköpostisi oikean tuoteavaimen varalta!", + "purchase_individual_description_1": "Yksittäiselle henkilölle", + "purchase_individual_description_2": "Tukijan tila", + "purchase_individual_title": "Yksittäinen", + "purchase_input_suggestion": "Onko sinulla tuoteavain? Syötä avain alle", + "purchase_license_subtitle": "Osta Immich tukeaksesi palvelun jatkuvaa kehittämistä", + "purchase_lifetime_description": "Elinikäinen osto", + "purchase_option_title": "OSTOVAIHTOEHDOT", + "purchase_panel_info_1": "Immichin rakentaminen vie paljon aikaa ja vaivannäköä, ja meillä on kokopäiväisiä insinöörejä työskentelemässä sen parissa, jotta voimme tehdä siitä mahdollisimman hyvän. Missiomme on, että avoimen lähdekoodin ohjelmistosta ja eettisistä liiketoimintakäytännöistä tulee kestävä tulonlähde kehittäjille, sekä luoda yksityisyyttä kunnioittava ekosysteemi, jossa on todellisia vaihtoehtoja hyväksikäyttöön perustuville pilvipalveluille.", + "purchase_panel_info_2": "Koska olemme sitoutuneet siihen, ettemme lisää maksumuuria, tämä osto ei anna sinulle mitään lisäominaisuuksia Immichissa. Luotamme kaltaisiisi käyttäjiin tukeaksemme Immichin jatkuvaa kehittämistä.", + "purchase_panel_title": "Tue projektia", + "purchase_per_server": "Per serveri", + "purchase_per_user": "Per käyttäjä", + "purchase_remove_product_key": "Poista Tuoteavain", + "purchase_remove_product_key_prompt": "Haluatko varmasti poistaa tuoteavaimen?", + "purchase_remove_server_product_key": "Poista palvelimen tuoteavain", + "purchase_remove_server_product_key_prompt": "Haluatko varmasti poistaa palvelimen tuoteavaimen?", + "purchase_server_description_1": "Koko palvelimelle", + "purchase_server_description_2": "Tukijan tila", + "purchase_server_title": "Serveri", + "purchase_settings_server_activated": "Palvelimen tuoteavainta hallinnoi ylläpitäjä", "range": "", + "rating": "Tähtiarvostelu", + "rating_clear": "Tyhjennä arvostelu", + "rating_count": "{count, plural, one {# tähti} other {# tähteä}}", + "rating_description": "Näytä EXIF-arvosana tiedot-paneelissa", "raw": "", - "reaction_options": "", + "reaction_options": "Reaktioasetukset", "read_changelog": "Lue muutosloki", "reassign": "Määritä uudelleen", + "reassigned_assets_to_existing_person": "Uudelleen määritetty {count, plural, one {# kohde} other {# kohdetta}} {name, select, null {olemassa olevalle henkilölle} other {{name}}}", "reassigned_assets_to_new_person": "Määritetty {count, plural, one {# media} other {# mediaa}} uudelle henkilölle", "reassing_hint": "Määritä valitut mediat käyttäjälle", "recent": "Viimeisin", @@ -886,18 +1061,19 @@ "remove_assets_shared_link_confirmation": "Haluatko varmasti poistaa {count, plural, one {# median} other {# mediaa}} tästä jakolinkistä?", "remove_assets_title": "Poistetaanko?", "remove_custom_date_range": "Poista aikaväliltä", + "remove_deleted_assets": "Poista Offline-tiedostot", "remove_from_album": "Poista albumista", "remove_from_favorites": "Poista suosikeista", "remove_from_shared_link": "Poista jakolinkistä", - "remove_offline_files": "Poista Offline-tiedostot", "remove_user": "Poista käyttäjä", "removed_api_key": "API Key {name} poistettu", "removed_from_archive": "Poistettu arkistosta", "removed_from_favorites": "Poistettu suosikeista", "removed_from_favorites_count": "{count, plural, other {Poistettu #}} suosikeista", + "removed_tagged_assets": "Poistettu tunniste {count, plural, one {# kohteesta} other {# kohteesta}}", "rename": "Nimeä uudelleen", "repair": "Korjaa", - "repair_no_results_message": "", + "repair_no_results_message": "Seuraamattomat ja puuttuvat tiedostot näkyvät täällä", "replace_with_upload": "Korvaa tiedostolla", "repository": "Tietovarasto", "require_password": "Vaadi salasana", @@ -907,6 +1083,7 @@ "reset_people_visibility": "Nollaa henkilöiden näkyvyysasetukset", "reset_settings_to_default": "", "reset_to_default": "Palauta oletusasetukset", + "resolve_duplicates": "Ratkaise kaksoiskappaleet", "resolved_all_duplicates": "Kaikki kaksoiskappaleet selvitetty", "restore": "Palauta", "restore_all": "Palauta kaikki", @@ -916,7 +1093,7 @@ "retry_upload": "Yritä latausta uudelleen", "review_duplicates": "Tarkastele kaksoiskappaleita", "role": "Rooli", - "role_editor": "Muokkain", + "role_editor": "Editori", "role_viewer": "Toistin", "save": "Tallenna", "saved_api_key": "API Key tallennettu", @@ -931,6 +1108,8 @@ "search": "Haku", "search_albums": "Etsi albumeita", "search_by_context": "Etsi kontekstin perusteella", + "search_by_filename": "Hae tiedostonimen tai -päätteen mukaan", + "search_by_filename_example": "esim. IMG_1234.JPG tai PNG", "search_camera_make": "Etsi kameramerkkiä...", "search_camera_model": "Etsi kameramallia...", "search_city": "Etsi kaupunkia...", @@ -938,9 +1117,12 @@ "search_for_existing_person": "Etsi olemassa olevaa henkilöä", "search_no_people": "Ei henkilöitä", "search_no_people_named": "Ei \"{name}\" nimisiä henkilöitä", + "search_options": "Hakuvaihtoehdot", "search_people": "Etsi ihmisiä", "search_places": "Etsi paikkoja", + "search_settings": "Hakuasetukset", "search_state": "Etsi tilaa...", + "search_tags": "Haku tageja...", "search_timezone": "Etsi aikavyöhyke...", "search_type": "Etsinnän tyyppi", "search_your_photos": "Etsi kuvia", @@ -949,6 +1131,7 @@ "see_all_people": "Näytä kaikki henkilöt", "select_album_cover": "Valitse albmin kansi", "select_all": "Valitse kaikki", + "select_all_duplicates": "Valitse kaikki kaksoiskappaleet", "select_avatar_color": "Valitse avatarin väri", "select_face": "Valitse kasvo", "select_featured_photo": "Valitse esittelykuva", @@ -963,6 +1146,8 @@ "send_message": "Lähetä viesti", "send_welcome_email": "Lähetä tervetuloviesti", "server": "Palvelin", + "server_offline": "Serveri Offline-tilassa", + "server_online": "Palvelin on linjalla", "server_stats": "Palvelimen tilastot", "server_version": "Palvelimen versio", "set": "Aseta", @@ -979,6 +1164,7 @@ "shared_by_user": "Käyttäjän {user} jakama", "shared_by_you": "Sinun jakamasi", "shared_from_partner": "{partner}n kuvia", + "shared_link_options": "Jaetun linkin vaihtoehdot", "shared_links": "Jaetut linkit", "shared_photos_and_videos_count": "{assetCount, plural, other {# jaettua kuvaa ja videota.}}", "shared_with_partner": "Jaa {partner} kanssa", @@ -987,6 +1173,7 @@ "sharing_sidebar_description": "Näytä jakamislinkki sivupalkissa", "shift_to_permanent_delete": "Paina ⇧ poistaaksesi median pysyvästi", "show_album_options": "Näytä albumin asetukset", + "show_albums": "Näytä albumit", "show_all_people": "Näytä kaikki henkilöt", "show_and_hide_people": "Näytä / piilota henkilöitä", "show_file_location": "Näytä tiedostosijainti", @@ -1001,11 +1188,17 @@ "show_person_options": "Näytä henkilöasetukset", "show_progress_bar": "Näytä eteneminen", "show_search_options": "Näytä hakuvaihtoehdot", + "show_supporter_badge": "Kannattajan merkki", + "show_supporter_badge_description": "Näytä kannattajan merkki", "shuffle": "Sekoita", + "sidebar": "Sivupalkki", + "sidebar_display_description": "Näytä linkki näkymään sivupalkissa", "sign_out": "Kirjaudu ulos", "sign_up": "Rekisteröidy", "size": "Koko", "skip_to_content": "Siirry sisältöön", + "skip_to_folders": "Siirry kansioihin", + "skip_to_tags": "Siirry tageihin", "slideshow": "Diaesitys", "slideshow_settings": "Diaesityksen asetukset", "sort_albums_by": "Järjestä albumit...", @@ -1017,6 +1210,8 @@ "sort_title": "Otsikko", "source": "Lähde", "stack": "Pinoa", + "stack_duplicates": "Pinoa kaksoiskappaleet", + "stack_select_one_photo": "Valitse yksi pääkuva pinolle", "stack_selected_photos": "Pinoa valitut kuvat", "stacked_assets_count": "Pinottu {count, plural, one {# media} other {# mediaa}}", "stacktrace": "Vianetsintätiedot", @@ -1036,6 +1231,14 @@ "sunrise_on_the_beach": "Auringonnousu rannalla", "swap_merge_direction": "Käännä yhdistämissuunta", "sync": "Synkronoi", + "tag": "Tagi", + "tag_assets": "Merkitse kohde", + "tag_created": "Luotu tunniste: {tag}", + "tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tagiotsikoiden mukaan", + "tag_not_found_question": "Etkö löydä tunnistetta? Luo yksi tästä", + "tag_updated": "Päivitetty tunniste: {tag}", + "tagged_assets": "Tunnistettu {count, plural, one {# kohde} other {# kohdetta}}", + "tags": "Tagit", "template": "Template", "theme": "Teema", "theme_selection": "Teeman valinta", @@ -1047,14 +1250,15 @@ "to_change_password": "Vaihda salasana", "to_favorite": "Aseta suosikiksi", "to_login": "Kirjaudu sisään", + "to_parent": "Siirry vanhempaan", "to_trash": "Roskakoriin", "toggle_settings": "Määritä asetukset", - "toggle_theme": "Aseta teema", + "toggle_theme": "Aseta tumma teema", "toggle_visibility": "Aseta näkyvyys", "total_usage": "Käyttö yhteensä", "trash": "Roskakori", "trash_all": "Vie kaikki roskakoriin", - "trash_count": "Vie {count} roskakoriin", + "trash_count": "Roskakori {count, number}", "trash_delete_asset": "Poista / vie roskakoriin", "trash_no_results_message": "Roskakorissa olevat kuvat ja videot näytetään täällä.", "trashed_items_will_be_permanently_deleted_after": "Roskakorin kohteet poistetaan pysyvästi {days, plural, one {# päivän} other {# päivän}} päästä.", @@ -1068,13 +1272,17 @@ "unknown_album": "", "unknown_year": "Tuntematon vuosi", "unlimited": "Rajoittamaton", + "unlink_motion_video": "Poista liikevideon linkitys", "unlink_oauth": "Poista OAuth-linkitys", "unlinked_oauth_account": "Linkittämätön OAuth-tili", "unnamed_album": "Nimetön albumi", + "unnamed_album_delete_confirmation": "Haluatko varmasti poistaa tämän albumin?", "unnamed_share": "Nimetön jako", "unsaved_change": "Tallentamaton muutos", "unselect_all": "Poista valinnat", + "unselect_all_duplicates": "Poista kaikkien kaksoiskappaleiden valinta", "unstack": "Pura pino", + "unstacked_assets_count": "Poistettu pinosta {count, plural, one {# kohde} other {# kohdetta}}", "untracked_files": "Tiedostot joita ei seurata", "untracked_files_decription": "Järjestelmä ei seuraa näitä tiedostoja. Ne voivat johtua epäonnistuneista siirroista, keskeytyneistä latauksista, tai ovat jääneet ohjelmavian seurauksena", "up_next": "Seuraavaksi", @@ -1082,7 +1290,7 @@ "upload": "Siirrä palvelimelle", "upload_concurrency": "Latausten samanaikaisuus", "upload_errors": "Lataus valmistui {count, plural, one {# virheen} other {# virheen}} kanssa. Päivitä sivu nähdäksesi ladatut tiedot.", - "upload_progress": "{remaining} jäljellä - {processed}/{total} käsitelty", + "upload_progress": "Jäljellä {remaining, number} - Käsitelty {processed, number}/{total, number}", "upload_skipped_duplicates": "Ohitettiin {count, plural, one {# kaksoiskappale} other {# kaksoiskappaletta}}", "upload_status_duplicates": "Kaksoiskappaleet", "upload_status_errors": "Virheet", @@ -1094,6 +1302,8 @@ "user": "Käyttäjä", "user_id": "Käyttäjän ID", "user_liked": "{user} tykkäsi {type, select, photo {kuvasta} video {videosta} asset {mediasta} other {tästä}}", + "user_purchase_settings": "Osta", + "user_purchase_settings_description": "Hallitse ostostasi", "user_role_set": "Tee käyttäjästä {user} {role}", "user_usage_detail": "Käyttäjän käytön tiedot", "username": "Käyttäjänimi", @@ -1113,6 +1323,7 @@ "view_album": "Näytä albumi", "view_all": "Näytä kaikki", "view_all_users": "Näytä kaikki käyttäjät", + "view_in_timeline": "Näytä aikajanalla", "view_links": "Näytä linkit", "view_next_asset": "Näytä seuraava", "view_previous_asset": "Näytä edellinen", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index c9fe916c03cca..0aca181a5ec47 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -41,6 +41,7 @@ "confirm_email_below": "Pour confirmer, tapez « {email} » ci-dessous", "confirm_reprocess_all_faces": "Êtes-vous sûr de vouloir retraiter tous les visages ? Cela effacera également les personnes déjà identifiées.", "confirm_user_password_reset": "Êtes-vous sûr de vouloir réinitialiser le mot de passe de {user} ?", + "create_job": "Créer une tâche", "crontab_guru": "Générateur de règles Cron", "disable_login": "Désactiver la connexion", "disabled": "Désactivé", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Résolution des miniatures", "image_thumbnail_resolution_description": "Utilisée lors du visionnage de groupes de photos (vue principale, albums, etc.). Une résolution plus élevée préserve davantage de détails, mais est plus longue à encoder, produit des fichiers plus lourds, et peut réduire la réactivité de l'application.", "job_concurrency": "{job} : nombre de tâches simultanées", + "job_created": "Tâche créée", "job_not_concurrency_safe": "Cette tâche ne peut pas être exécutée en multitâche de façon sûre.", "job_settings": "Paramètres des tâches", "job_settings_description": "Gestion des tâches simultanées", @@ -148,11 +150,11 @@ "migration_job_description": "Migration des miniatures pour les médias et les visages vers la dernière structure de dossiers", "no_paths_added": "Aucun chemin n'a été ajouté", "no_pattern_added": "Aucun schéma d'exclusion n'a été ajouté", - "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'étiquette de stockage à des médias précédemment téléversés, exécutez la commande", + "note_apply_storage_label_previous_assets": "Remarque : pour appliquer l'étiquette de stockage à des médias précédemment envoyés, exécutez la commande", "note_cannot_be_changed_later": "REMARQUE : Il n'est pas possible de modifier ce paramètre ultérieurement !", "note_unlimited_quota": "Note : saisir 0 pour un quota illimité", "notification_email_from_address": "Depuis l'adresse", - "notification_email_from_address_description": "Adresse courriel de l'expéditeur, par exemple : « Serveur de photos Immich  »", + "notification_email_from_address_description": "Adresse courriel de l'expéditeur, par exemple : « Serveur de photos Immich  »", "notification_email_host_description": "Hôte du serveur de messagerie électronique (par exemple, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorer les erreurs de certificat", "notification_email_ignore_certificate_errors_description": "Ignorer les erreurs de validation du certificat TLS (non recommandé)", @@ -198,11 +200,12 @@ "password_settings": "Connexion par mot de passe", "password_settings_description": "Gérer les paramètres de connexion par mot de passe", "paths_validated_successfully": "Tous les chemins ont été validés avec succès", + "person_cleanup_job": "Nettoyage des personnes", "quota_size_gib": "Taille du quota (Go)", "refreshing_all_libraries": "Actualisation de toutes les bibliothèques", "registration": "Enregistrement de l'administrateur", "registration_description": "Puisque vous êtes le premier utilisateur sur le système, vous serez désigné en tant qu'administrateur et responsable des tâches administratives, et vous pourrez alors créer d'autres utilisateurs.", - "removing_offline_files": "Suppression des fichiers hors ligne", + "removing_deleted_files": "Suppression des fichiers hors ligne", "repair_all": "Réparer tout", "repair_matched_items": "{count, plural, one {# Élément correspondant} other {# Éléments correspondants}}", "repaired_items": "{count, plural, one {# Élément corrigé} other {# Éléments corrigés}}", @@ -211,6 +214,7 @@ "reset_settings_to_recent_saved": "Paramètres réinitialisés avec les derniers paramètres enregistrés", "scanning_library_for_changed_files": "Recherche de fichiers modifiés dans la bibliothèque", "scanning_library_for_new_files": "Recherche de nouveaux fichiers dans la bibliothèque", + "search_jobs": "Recherche des tâches ...", "send_welcome_email": "Envoyer un courriel de bienvenue", "server_external_domain_settings": "Domaine externe", "server_external_domain_settings_description": "Nom de domaine pour les liens partagés publics, y compris http(s)://", @@ -228,16 +232,17 @@ "storage_template_hash_verification_enabled": "Vérification du hachage activée", "storage_template_hash_verification_enabled_description": "Active la vérification du hachage, ne désactivez pas cette option à moins d'être sûr de ce que vous faites", "storage_template_migration": "Migration du modèle de stockage", - "storage_template_migration_description": "Appliquer le modèle courant {template} aux médias précédemment téléchargés", - "storage_template_migration_info": "Les changements de modèle ne s'appliqueront qu'aux nouveaux médias. Pour appliquer rétroactivement le modèle aux médias précédemment téléchargés, exécutez la tâche {job}.", + "storage_template_migration_description": "Appliquer le modèle courant {template} aux médias précédemment envoyés", + "storage_template_migration_info": "Les changements de modèle ne s'appliqueront qu'aux nouveaux médias. Pour appliquer rétroactivement le modèle aux médias précédemment envoyés, exécutez la tâche {job}.", "storage_template_migration_job": "Tâche de migration du modèle de stockage", "storage_template_more_details": "Pour plus de détails sur cette fonctionnalité, reportez-vous au Modèle de stockage et à ses implications", "storage_template_onboarding_description": "Lorsqu'elle est activée, cette fonctionnalité réorganise les fichiers basés sur un modèle défini par l'utilisateur. En raison de problèmes de stabilité, la fonction a été désactivée par défaut. Pour plus d'informations, veuillez consulter la documentation.", "storage_template_path_length": "Limite approximative de la longueur du chemin : {length, number}/{limit, number}", "storage_template_settings": "Modèle de stockage", - "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média téléversé", + "storage_template_settings_description": "Gérer la structure des dossiers et le nom des fichiers du média envoyé", "storage_template_user_label": "{label} est l'étiquette de stockage de l'utilisateur", "system_settings": "Paramètres du système", + "tag_cleanup_job": "Nettoyage des étiquettes", "theme_custom_css_settings": "CSS personnalisé", "theme_custom_css_settings_description": "Les feuilles de style en cascade (CSS) permettent de personnaliser l'apparence d'Immich.", "theme_settings": "Paramètres du thème", @@ -311,7 +316,8 @@ "trash_settings": "Corbeille", "trash_settings_description": "Gérer les paramètres de la corbeille", "untracked_files": "Fichiers non suivis", - "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, téléchargements interrompus, ou abandons en raison d'un bug", + "untracked_files_description": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat d'erreurs de déplacement, d'envois interrompus, ou d'abandons en raison d'un bug", + "user_cleanup_job": "Nettoyage des utilisateurs", "user_delete_delay": "La suppression définitive du compte et des médias de {user} sera programmée dans {delay, plural, one {# jour} other {# jours}}.", "user_delete_delay_settings": "Délai de suppression", "user_delete_delay_settings_description": "Nombre de jours après la validation pour supprimer définitivement le compte et les médias d'un utilisateur. La suppression des utilisateurs se lance à minuit. Les modifications apportées à ce paramètre seront pris en compte lors de la prochaine exécution.", @@ -366,7 +372,7 @@ "allow_dark_mode": "Autoriser le mode sombre", "allow_edits": "Autoriser les modifications", "allow_public_user_to_download": "Permettre aux utilisateurs non connectés de télécharger", - "allow_public_user_to_upload": "Permettre aux utilisateurs non connectés de téléverser", + "allow_public_user_to_upload": "Permettre l'envoi aux utilisateurs non connectés", "anti_clockwise": "Sens anti-horaire", "api_key": "Clé API", "api_key_description": "Cette valeur ne sera affichée qu'une seule fois. Assurez-vous de la copier avant de fermer la fenêtre.", @@ -391,8 +397,9 @@ "asset_offline": "Média hors ligne", "asset_offline_description": "Ce média est hors ligne. Immich ne peut pas accéder à son emplacement physique. Veuillez vous assurez que le média est disponible, puis relancez l'analyse de la bibliothèque.", "asset_skipped": "Sauté", - "asset_uploaded": "Téléversé", - "asset_uploading": "Chargement...", + "asset_skipped_in_trash": "À la corbeille", + "asset_uploaded": "Envoyé", + "asset_uploading": "Envoi...", "assets": "Médias", "assets_added_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}}", "assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album", @@ -487,8 +494,8 @@ "create_new_person": "Créer une nouvelle personne", "create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne", "create_new_user": "Créer un nouvel utilisateur", - "create_tag": "Créer un tag", - "create_tag_description": "Créer un nouveau tag. Pour les tags imbriqués, veuillez entrer le chemin complet du tag, y compris les \"/\" avant.", + "create_tag": "Créer une étiquette", + "create_tag_description": "Créer une nouvelle étiquette. Pour les étiquettes imbriquées, veuillez entrer le chemin complet de l'étiquette, y compris les caractères \"/\".", "create_user": "Créer un utilisateur", "created": "Créé", "current_device": "Appareil actuel", @@ -512,8 +519,8 @@ "delete_library": "Supprimer la bibliothèque", "delete_link": "Supprimer le lien", "delete_shared_link": "Supprimer le lien partagé", - "delete_tag": "Supprimer le tag", - "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer le tag {tagName} ?", + "delete_tag": "Supprimer l'étiquette", + "delete_tag_confirmation_prompt": "Êtes-vous sûr de vouloir supprimer l'étiquette {tagName} ?", "delete_user": "Supprimer l'utilisateur", "deleted_shared_link": "Lien partagé supprimé", "description": "Description", @@ -537,7 +544,7 @@ "download_settings_description": "Gérer les paramètres de téléchargement des médias", "downloading": "Téléchargement", "downloading_asset_filename": "Téléchargement du média {filename}", - "drop_files_to_upload": "Déposer des fichiers n'importe où pour téléverser", + "drop_files_to_upload": "Déposez les fichiers n'importe où pour envoyer", "duplicates": "Doublons", "duplicates_description": "Examiner chaque groupe et indiquer s'il y a des doublons", "duration": "Durée", @@ -562,7 +569,7 @@ "edit_location": "Modifier la localisation", "edit_name": "Modifier le nom", "edit_people": "Modifier les personnes", - "edit_tag": "Modifier le tag", + "edit_tag": "Modifier l'étiquette", "edit_title": "Modifier le title", "edit_user": "Modifier l'utilisateur", "edited": "Modifié", @@ -613,7 +620,7 @@ "failed_to_remove_product_key": "Échec de suppression de la clé du produit", "failed_to_stack_assets": "Impossible d'empiler les médias", "failed_to_unstack_assets": "Impossible de dépiler les médias", - "import_path_already_exists": "Ce chemin d'import existe déjà.", + "import_path_already_exists": "Ce chemin d'importation existe déjà.", "incorrect_email_or_password": "Courriel ou mot de passe incorrect", "paths_validation_failed": "Validation échouée pour {paths, plural, one {# un chemin} other {# plusieurs chemins}}", "profile_picture_transparent_pixels": "Les images de profil ne peuvent pas avoir de pixels transparents. Veuillez agrandir et/ou déplacer l'image.", @@ -623,7 +630,7 @@ "unable_to_add_assets_to_shared_link": "Impossible d'ajouter des médias au lien partagé", "unable_to_add_comment": "Impossible d'ajouter un commentaire", "unable_to_add_exclusion_pattern": "Impossible d'ajouter un schéma d'exclusion", - "unable_to_add_import_path": "Impossible d'ajouter un chemin d'import", + "unable_to_add_import_path": "Impossible d'ajouter le chemin d'importation", "unable_to_add_partners": "Impossible d'ajouter des partenaires", "unable_to_add_remove_archive": "Impossible {archived, select, true {de supprimer des médias de} other {d'ajouter des médias à}} l'archive", "unable_to_add_remove_favorites": "Impossible {favorite, select, true {d'ajouter des médias aux} other {de supprimer des médias des}} favoris", @@ -648,18 +655,19 @@ "unable_to_delete_asset": "Suppression du média impossible", "unable_to_delete_assets": "Erreur lors de la suppression des médias", "unable_to_delete_exclusion_pattern": "Suppression du modèle d'exclusion impossible", - "unable_to_delete_import_path": "Suppression du chemin d'import impossible", + "unable_to_delete_import_path": "Suppression du chemin d'importation impossible", "unable_to_delete_shared_link": "Suppression du lien de partage impossible", "unable_to_delete_user": "Suppression de l'utilisateur impossible", "unable_to_download_files": "Impossible de télécharger les fichiers", "unable_to_edit_exclusion_pattern": "Modification du modèle d'exclusion impossible", - "unable_to_edit_import_path": "Modification du chemin d'import impossible", + "unable_to_edit_import_path": "Modification du chemin d'importation impossible", "unable_to_empty_trash": "Impossible de vider la corbeille", "unable_to_enter_fullscreen": "Mode plein écran indisponible", "unable_to_exit_fullscreen": "Sortie du mode plein écran impossible", "unable_to_get_comments_number": "Impossible d'obtenir le nombre de commentaires", "unable_to_get_shared_link": "Échec de la récupération du lien partagé", "unable_to_hide_person": "Impossible de cacher la personne", + "unable_to_link_motion_video": "Impossible de lier la photo animée", "unable_to_link_oauth_account": "Impossible de lier le compte OAuth", "unable_to_load_album": "Impossible de charger l'album", "unable_to_load_asset_activity": "Impossible de charger l'activité du média", @@ -676,8 +684,8 @@ "unable_to_remove_api_key": "Impossible de supprimer la clé API", "unable_to_remove_assets_from_shared_link": "Impossible de supprimer des médias du lien partagé", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Impossible de supprimer les fichiers hors ligne", "unable_to_remove_library": "Impossible de supprimer la bibliothèque", - "unable_to_remove_offline_files": "Impossible de supprimer les fichiers hors ligne", "unable_to_remove_partner": "Impossible de supprimer le partenaire", "unable_to_remove_reaction": "Impossible de supprimer la réaction", "unable_to_remove_user": "", @@ -700,6 +708,7 @@ "unable_to_submit_job": "Impossible d'exécuter la tâche", "unable_to_trash_asset": "Impossible de mettre le média à la corbeille", "unable_to_unlink_account": "Impossible de détacher le compte", + "unable_to_unlink_motion_video": "Impossible de détacher la photo animée", "unable_to_update_album_cover": "Impossible de mettre à jour la couverture de l'album", "unable_to_update_album_info": "Impossible de mettre à jour les informations de l'album", "unable_to_update_library": "Impossible de mettre à jour la bibliothèque", @@ -707,7 +716,7 @@ "unable_to_update_settings": "Impossible de mettre à jour les paramètres", "unable_to_update_timeline_display_status": "Impossible de mettre à jour le statut d'affichage de la timeline", "unable_to_update_user": "Impossible de mettre à jour l'utilisateur", - "unable_to_upload_file": "Impossible de téléverser le fichier" + "unable_to_upload_file": "Impossible d'envoyer le fichier" }, "every_day_at_onepm": "", "every_night_at_midnight": "", @@ -845,6 +854,7 @@ "license_trial_info_4": "Pensez à acheter une licence pour soutenir le développement du service", "light": "Clair", "like_deleted": "Réaction « j'aime » supprimée", + "link_motion_video": "Lier la photo animée", "link_options": "Options de lien", "link_to_oauth": "Lien au service OAuth", "linked_oauth_account": "Compte OAuth rattaché", @@ -913,10 +923,10 @@ "no_albums_with_name_yet": "Il semble que vous n'ayez pas encore d'albums avec ce nom.", "no_albums_yet": "Il semble que vous n'ayez pas encore d'album.", "no_archived_assets_message": "Archiver des photos et vidéos pour les masquer dans votre bibliothèque", - "no_assets_message": "CLIQUER ICI POUR IMPORTER VOTRE PREMIÈRE PHOTO", + "no_assets_message": "CLIQUER ICI POUR ENVOYER VOTRE PREMIÈRE PHOTO", "no_duplicates_found": "Aucun doublon n'a été trouvé.", "no_exif_info_available": "Aucune information exif disponible", - "no_explore_results_message": "Importer plus de photos pour explorer votre collection.", + "no_explore_results_message": "Envoyez plus de photos pour explorer votre collection.", "no_favorites_message": "Ajouter des photos et vidéos à vos favoris pour les retrouver plus rapidement", "no_libraries_message": "Créer une bibliothèque externe pour voir vos photos et vidéos dans un autre espace de stockage", "no_name": "Pas de nom", @@ -925,7 +935,7 @@ "no_results_description": "Essayez un synonyme ou un mot-clé plus général", "no_shared_albums_message": "Créer un album pour partager vos photos et vidéos avec les personnes de votre réseau", "not_in_any_album": "Dans aucun album", - "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias déjà importés, lancer la", + "note_apply_storage_label_to_previously_uploaded assets": "Note : Pour appliquer l'étiquette de stockage aux médias déjà envoyés, lancer la", "note_unlimited_quota": "Note : Saisir 0 pour définir un quota illimité", "notes": "Notes", "notification_toggle_setting_description": "Activer les notifications par courriel", @@ -1078,10 +1088,10 @@ "remove_assets_shared_link_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# média} other {# médias}} de ce lien partagé ?", "remove_assets_title": "Supprimer les médias ?", "remove_custom_date_range": "Supprimer la plage de date personnalisée", + "remove_deleted_assets": "Supprimer les fichiers hors ligne", "remove_from_album": "Supprimer de l'album", "remove_from_favorites": "Supprimer des favoris", "remove_from_shared_link": "Supprimer des liens partagés", - "remove_offline_files": "Supprimer les fichiers hors ligne", "remove_user": "Supprimer l'utilisateur", "removed_api_key": "Clé API supprimée : {name}", "removed_from_archive": "Supprimé de l'archive", @@ -1134,10 +1144,12 @@ "search_for_existing_person": "Rechercher une personne existante", "search_no_people": "Aucune personne", "search_no_people_named": "Aucune personne nommée « {name} »", + "search_options": "Rechercher une option", "search_people": "Rechercher une personne", "search_places": "Rechercher un lieu", + "search_settings": "Paramètres de recherche", "search_state": "Rechercher par état/région...", - "search_tags": "Recherche de tags...", + "search_tags": "Recherche d'étiquettes...", "search_timezone": "Rechercher par fuseau horaire...", "search_type": "Rechercher par type", "search_your_photos": "Rechercher vos photos", @@ -1212,6 +1224,8 @@ "sign_up": "S'enregistrer", "size": "Taille", "skip_to_content": "Passer", + "skip_to_folders": "Passer vers les dossiers", + "skip_to_tags": "Passer vers les étiquettes", "slideshow": "Diaporama", "slideshow_settings": "Paramètres du diaporama", "sort_albums_by": "Trier les albums par...", @@ -1246,12 +1260,12 @@ "sync": "Synchroniser", "tag": "Tag", "tag_assets": "Taguer les médias", - "tag_created": "Tag créé : {tag}", + "tag_created": "Étiquette créée : {tag}", "tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques", - "tag_not_found_question": "Vous ne trouvez pas un tag ? Créez-en un ici", - "tag_updated": "Tag mis à jour : {tag}", + "tag_not_found_question": "Vous ne trouvez pas une étiquette ? Créez-en une ici", + "tag_updated": "Étiquette mise à jour : {tag}", "tagged_assets": "Tag ajouté à {count, plural, one {# média} other {# médias}}", - "tags": "Tags", + "tags": "Étiquettes", "template": "Modèle", "theme": "Thème", "theme_selection": "Sélection du thème", @@ -1286,6 +1300,7 @@ "unknown_album": "", "unknown_year": "Année inconnue", "unlimited": "Illimité", + "unlink_motion_video": "Détacher la photo animée", "unlink_oauth": "Déconnecter OAuth", "unlinked_oauth_account": "Compte OAuth non connecté", "unnamed_album": "Album sans nom", @@ -1297,18 +1312,18 @@ "unstack": "Désempiler", "unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}", "untracked_files": "Fichiers non suivis", - "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, de téléchargements interrompus ou laissés pour compte à cause d'un bug", + "untracked_files_decription": "Ces fichiers ne sont pas suivis par l'application. Ils peuvent être le résultat de déplacements échoués, d'envois interrompus ou laissés pour compte à cause d'un bug", "up_next": "Suite", "updated_password": "Mot de passe mis à jour", - "upload": "Téléverser", - "upload_concurrency": "Envoi simultané", - "upload_errors": "Le téléversement s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias téléversés.", + "upload": "Envoyer", + "upload_concurrency": "Envois simultanés", + "upload_errors": "L'envoi s'est achevé avec {count, plural, one {# erreur} other {# erreurs}}. Rafraîchir la page pour voir les nouveaux médias envoyés.", "upload_progress": "{remaining, number} restant(s) - {processed, number} traité(s)/{total, number}", "upload_skipped_duplicates": "{count, plural, one {# doublon ignoré} other {# doublons ignorés}}", "upload_status_duplicates": "Doublons", "upload_status_errors": "Erreurs", - "upload_status_uploaded": "Téléversé", - "upload_success": "Téléversement réussi. Rafraîchir la page pour voir les nouveaux médias téléversés.", + "upload_status_uploaded": "Envoyé", + "upload_success": "Envoi réussi. Rafraîchir la page pour voir les nouveaux médias envoyés.", "url": "URL", "usage": "Utilisation", "use_custom_date_range": "Utilisez une plage de date personnalisée à la place", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index aefa831897de7..587216cf19196 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -41,6 +41,7 @@ "confirm_email_below": "כדי לאשר, יש להקליד \"{email}\" למטה", "confirm_reprocess_all_faces": "האם את/ה בטוח/ה שברצונך לעבד מחדש את כל הפנים? זה גם ינקה אנשים בעלי שם.", "confirm_user_password_reset": "האם את/ה בטוח/ה שברצונך לאפס את הסיסמה של המשתמש {user}?", + "create_job": "צור עבודה", "crontab_guru": "Crontab Guru", "disable_login": "השבת כניסה", "disabled": "מושבת", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "רזולוציית תמונה ממוזערת", "image_thumbnail_resolution_description": "משמש בעת צפייה בקבוצות של תמונות (ציר זמן ראשי, תצוגת אלבום וכו'). רזולוציות גבוהות יותר יכולות לשמר פירוט רב יותר אך לוקחות יותר זמן לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.", "job_concurrency": "בו-זמניות של {job}", + "job_created": "עבודה נוצרה", "job_not_concurrency_safe": "משימה זו אינה בטוחה במקביל.", "job_settings": "הגדרות משימה", "job_settings_description": "ניהול בו-זמניות של משימה", @@ -139,7 +141,11 @@ "map_settings_description": "נהל הגדרות מפה", "map_style_description": "כתובת אתר לערכת נושא של מפה style.json", "metadata_extraction_job": "חלץ מטא-נתונים", - "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS ורזולוציה", + "metadata_extraction_job_description": "חלץ מידע מטא-נתונים מכל נכס, כגון GPS, פנים ורזולוציה", + "metadata_faces_import_setting": "אפשר יבוא פנים", + "metadata_faces_import_setting_description": "יבא פנים מנתוני EXIF של תמונה ומקבצים נלווים", + "metadata_settings": "הגדרות מטא-נתונים", + "metadata_settings_description": "נהל הגדרות מטא-נתונים", "migration_job": "העברה", "migration_job_description": "העבר תמונות ממוזערות של נכסים ופנים למבנה התיקיות העדכני ביותר", "no_paths_added": "לא נוספו נתיבים", @@ -148,7 +154,7 @@ "note_cannot_be_changed_later": "הערה: אי אפשר לשנות זאת מאוחר יותר!", "note_unlimited_quota": "הערה: הזן 0 עבור מכסת אחסון בלתי מוגבלת", "notification_email_from_address": "מכתובת", - "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות \"", + "notification_email_from_address_description": "כתובת דוא\"ל של השולח, לדוגמה: \"Immich שרת תמונות \"", "notification_email_host_description": "מארח שרת הדוא\"ל (למשל smtp.immich.app)", "notification_email_ignore_certificate_errors": "התעלם משגיאות תעודה", "notification_email_ignore_certificate_errors_description": "התעלם משגיאות אימות תעודת TLS (לא מומלץ)", @@ -194,11 +200,12 @@ "password_settings": "סיסמת התחברות", "password_settings_description": "נהל הגדרות סיסמת התחברות", "paths_validated_successfully": "כל הנתיבים אומתו בהצלחה", + "person_cleanup_job": "ניקוי אדם", "quota_size_gib": "גודל מכסה (GiB)", "refreshing_all_libraries": "מרענן את כל הספריות", "registration": "רישום מנהל מערכת", "registration_description": "מכיוון שאתה המשתמש הראשון במערכת, אתה תוקצה כמנהל ואתה אחראי על משימות ניהול, ומשתמשים נוספים ייווצרו על ידך.", - "removing_offline_files": "הסרת קבצים לא מקוונים", + "removing_deleted_files": "הסרת קבצים לא מקוונים", "repair_all": "תקן הכל", "repair_matched_items": "{count, plural, one {פריט # תואם} other {# פריטים תואמים}}", "repaired_items": "{count, plural, one {פריט # תוקן} other {# פריטים תוקנו}}", @@ -207,6 +214,7 @@ "reset_settings_to_recent_saved": "אפס הגדרות להגדרות שנשמרו לאחרונה", "scanning_library_for_changed_files": "סורק ספרייה לאיתור קבצים שהשתנו", "scanning_library_for_new_files": "סורק ספרייה לאיתור קבצים חדשים", + "search_jobs": "חיפוש עבודות...", "send_welcome_email": "שלח דוא\"ל ברוכים הבאים", "server_external_domain_settings": "דומיין חיצוני", "server_external_domain_settings_description": "דומיין עבור קישורים משותפים ציבוריים, כולל http(s)://", @@ -234,6 +242,7 @@ "storage_template_settings_description": "נהל את מבנה התיקיות ואת שם הקובץ של נכס ההעלאה", "storage_template_user_label": "{label} היא תווית האחסון של המשתמש", "system_settings": "הגדרות מערכת", + "tag_cleanup_job": "ניקוי תגים", "theme_custom_css_settings": "CSS בהתאמה אישית", "theme_custom_css_settings_description": "גיליונות סגנון מדורגים (CSS) מאפשרים התאמה אישית של העיצוב של Immich.", "theme_settings": "הגדרות ערכת נושא", @@ -308,6 +317,7 @@ "trash_settings_description": "נהל את הגדרות האשפה", "untracked_files": "קבצים ללא מעקב", "untracked_files_description": "קבצים אלה אינם נמצאים במעקב של היישום. הם יכולים להיות תוצאות של העברות כושלות, העלאות שנקטעו, או שנותרו מאחור בגלל שיבוש בתוכנה", + "user_cleanup_job": "ניקוי משתמשים", "user_delete_delay": "החשבון והנכסים של {user} יתוזמנו למחיקה לצמיתות בעוד {delay, plural, one {יום #} other {# ימים}}.", "user_delete_delay_settings": "עיכוב מחיקה", "user_delete_delay_settings_description": "מספר הימים לאחר ההסרה עד מחיקה לצמיתות של החשבון והנכסים של המשתמש. משימת מחיקת המשתמש פועלת בחצות כדי לבדוק אם יש משתמשים שמוכנים למחיקה. שינויים בהגדרה זו יוערכו בביצוע הבא.", @@ -387,6 +397,7 @@ "asset_offline": "נכס לא מקוון", "asset_offline_description": "הנכס הזה אינו מקוון. Immich לא יכול לגשת למיקום הקובץ שלו. נא לוודא שהנכס זמין ואז סרוק מחדש את הספרייה.", "asset_skipped": "דילג", + "asset_skipped_in_trash": "באשפה", "asset_uploaded": "הועלה", "asset_uploading": "מעלה...", "assets": "נכסים", @@ -656,6 +667,7 @@ "unable_to_get_comments_number": "לא ניתן להשיג את מספר התגובות", "unable_to_get_shared_link": "קבלת קישור משותף נכשלה", "unable_to_hide_person": "לא ניתן להסתיר אדם", + "unable_to_link_motion_video": "לא ניתן לקשר סרטון תנועה", "unable_to_link_oauth_account": "לא ניתן לקשר חשבון OAuth", "unable_to_load_album": "לא ניתן לטעון אלבום", "unable_to_load_asset_activity": "לא ניתן לטעון את פעילות הנכס", @@ -672,8 +684,8 @@ "unable_to_remove_api_key": "לא ניתן להסיר מפתח API", "unable_to_remove_assets_from_shared_link": "לא ניתן להסיר נכסים מקישור משותף", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "לא ניתן להסיר קבצים לא מקוונים", "unable_to_remove_library": "לא ניתן להסיר ספרייה", - "unable_to_remove_offline_files": "לא ניתן להסיר קבצים לא מקוונים", "unable_to_remove_partner": "לא ניתן להסיר שותף", "unable_to_remove_reaction": "לא ניתן להסיר תגובה", "unable_to_remove_user": "", @@ -696,6 +708,7 @@ "unable_to_submit_job": "לא ניתן לשלוח משימה", "unable_to_trash_asset": "לא ניתן להעביר נכס לאשפה", "unable_to_unlink_account": "לא ניתן לבטל קישור חשבון", + "unable_to_unlink_motion_video": "לא ניתן לבטל קישור סרטון תנועה", "unable_to_update_album_cover": "לא ניתן לעדכן עטיפת אלבום", "unable_to_update_album_info": "לא ניתן לעדכן פרטי אלבום", "unable_to_update_library": "לא ניתן לעדכן ספרייה", @@ -841,6 +854,7 @@ "license_trial_info_4": "אנא שקול לרכוש רישיון כדי לתמוך בפיתוח המתמשך של השירות", "light": "בהיר", "like_deleted": "לייק נמחק", + "link_motion_video": "קשר סרטון תנועה", "link_options": "אפשרויות קישור", "link_to_oauth": "קישור ל-OAuth", "linked_oauth_account": "חשבון OAuth מקושר", @@ -1074,10 +1088,10 @@ "remove_assets_shared_link_confirmation": "האם את/ה בטוח/ה שברצונך להסיר {count, plural, one {נכס #} other {# נכסים}} מהקישור המשותף הזה?", "remove_assets_title": "הסר נכסים?", "remove_custom_date_range": "הסר טווח תאריכים מותאם", + "remove_deleted_assets": "הסר קבצים לא מקוונים", "remove_from_album": "הסר מאלבום", "remove_from_favorites": "הסר מהמועדפים", "remove_from_shared_link": "הסר מקישור משותף", - "remove_offline_files": "הסר קבצים לא מקוונים", "remove_user": "הסר משתמש", "removed_api_key": "מפתח API הוסר: {name}", "removed_from_archive": "הוסר מארכיון", @@ -1130,8 +1144,10 @@ "search_for_existing_person": "חפש אדם קיים", "search_no_people": "אין אנשים", "search_no_people_named": "אין אנשים בשם \"{name}\"", + "search_options": "אפשרויות חיפוש", "search_people": "חפש אנשים", "search_places": "חפש מקומות", + "search_settings": "הגדרות חיפוש", "search_state": "חפש מדינה...", "search_tags": "חיפוש תגים...", "search_timezone": "חפש אזור זמן...", @@ -1208,6 +1224,8 @@ "sign_up": "הרשמה", "size": "גודל", "skip_to_content": "דלג לתוכן", + "skip_to_folders": "דלג לתיקיות", + "skip_to_tags": "דלג לתגים", "slideshow": "מצגת שקופיות", "slideshow_settings": "הגדרות מצגת שקופיות", "sort_albums_by": "מיין אלבומים לפי...", @@ -1282,6 +1300,7 @@ "unknown_album": "אלבום לא ידוע", "unknown_year": "שנה לא ידועה", "unlimited": "בלתי מוגבל", + "unlink_motion_video": "בטל קישור סרטון תנועה", "unlink_oauth": "בטל קישור OAuth", "unlinked_oauth_account": "בוטל קישור חשבון OAuth", "unnamed_album": "אלבום ללא שם", diff --git a/web/src/lib/i18n/hi.json b/web/src/lib/i18n/hi.json index 99f2ef24587fd..58b99dbc3cbfd 100644 --- a/web/src/lib/i18n/hi.json +++ b/web/src/lib/i18n/hi.json @@ -114,7 +114,7 @@ "machine_learning_max_recognition_distance_description": "एक ही व्यक्ति माने जाने वाले दो चेहरों के बीच अधिकतम दूरी 0-2 के बीच है।", "machine_learning_min_detection_score": "न्यूनतम पहचान स्कोर", "machine_learning_min_detection_score_description": "किसी चेहरे का पता लगाने के लिए न्यूनतम आत्मविश्वास स्कोर 0-1 होना चाहिए।", - "machine_learning_min_recognized_faces": "न्यूनतम पहचाने गए चेहरे", + "machine_learning_min_recognized_faces": "निम्नतम पहचाने चेहरे", "machine_learning_min_recognized_faces_description": "किसी व्यक्ति के लिए पहचाने जाने वाले चेहरों की न्यूनतम संख्या।", "machine_learning_settings": "मशीन लर्निंग सेटिंग्स", "machine_learning_settings_description": "मशीन लर्निंग सुविधाओं और सेटिंग्स को प्रबंधित करें", @@ -147,7 +147,7 @@ "note_cannot_be_changed_later": "नोट: इसे बाद में बदला नहीं जा सकता!", "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें", "notification_email_from_address": "इस पते से", - "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर \"", + "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर \"", "notification_email_host_description": "ईमेल सर्वर का होस्ट (उदा. smtp.immitch.app)", "notification_email_ignore_certificate_errors": "प्रमाणपत्र त्रुटियों पर ध्यान न दें", "notification_email_ignore_certificate_errors_description": "टीएलएस प्रमाणपत्र सत्यापन त्रुटियों पर ध्यान न दें (अनुशंसित नहीं)", @@ -197,7 +197,7 @@ "refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है", "registration": "व्यवस्थापक पंजीकरण", "registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।", - "removing_offline_files": "ऑफ़लाइन फ़ाइलें हटाना", + "removing_deleted_files": "ऑफ़लाइन फ़ाइलें हटाना", "repair_all": "सभी की मरम्मत", "require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", "reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें", @@ -605,8 +605,8 @@ "unable_to_remove_api_key": "API कुंजी निकालने में असमर्थ", "unable_to_remove_assets_from_shared_link": "साझा लिंक से संपत्तियों को निकालने में असमर्थ", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ", "unable_to_remove_library": "लाइब्रेरी हटाने में असमर्थ", - "unable_to_remove_offline_files": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ", "unable_to_remove_partner": "पार्टनर को हटाने में असमर्थ", "unable_to_remove_reaction": "प्रतिक्रिया निकालने में असमर्थ", "unable_to_remove_user": "", @@ -933,10 +933,10 @@ "remove": "निकालना", "remove_assets_title": "संपत्तियाँ हटाएँ?", "remove_custom_date_range": "कस्टम दिनांक सीमा हटाएँ", + "remove_deleted_assets": "ऑफ़लाइन फ़ाइलें हटाएँ", "remove_from_album": "एल्बम से हटाएँ", "remove_from_favorites": "पसंदीदा से निकालें", "remove_from_shared_link": "साझा लिंक से हटाएँ", - "remove_offline_files": "ऑफ़लाइन फ़ाइलें हटाएँ", "remove_user": "उपयोगकर्ता को हटाएँ", "removed_from_archive": "संग्रह से हटा दिया गया", "removed_from_favorites": "पसंदीदा से हटाया गया", diff --git a/web/src/lib/i18n/hr.json b/web/src/lib/i18n/hr.json index 291abaa6908d5..886a0ae49b1c4 100644 --- a/web/src/lib/i18n/hr.json +++ b/web/src/lib/i18n/hr.json @@ -27,10 +27,11 @@ "added_to_favorites": "Dodano u omiljeno", "added_to_favorites_count": "Dodano {count, number} u omiljeno", "admin": { - "add_exclusion_pattern_description": "", + "add_exclusion_pattern_description": "Dodajte uzorke izuzimanja. Globiranje pomoću *, ** i ? je podržano. Za ignoriranje svih datoteka u bilo kojem direktoriju pod nazivom \"Raw\", koristite \"**/Raw/**\". Da biste zanemarili sve datoteke koje završavaju na \".tif\", koristite \"**/*.tif\". Da biste zanemarili apsolutni put, koristite \"/path/to/ignore/**\".", "authentication_settings": "Postavke autentikacije", "authentication_settings_description": "Uredi lozinku, OAuth, i druge postavke autentikacije", "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.", + "authentication_settings_reenable": "Za ponovno uključivanje upotrijebite naredbu poslužitelja.", "background_task_job": "Pozadinski zadaci", "check_all": "Provjeri sve", "cleared_jobs": "Izbrisani poslovi za: {job}", @@ -40,6 +41,7 @@ "confirm_email_below": "Za potvrdu upišite \"{email}\" ispod", "confirm_reprocess_all_faces": "Jeste li sigurni da želite ponovno obraditi sva lica? Ovo će također obrisati imenovane osobe.", "confirm_user_password_reset": "Jeste li sigurni da želite poništiti lozinku korisnika {user}?", + "create_job": "Izradi zadatak", "crontab_guru": "Crontab Guru", "disable_login": "Onemogući prijavu", "duplicate_detection_job_description": "Pokrenite strojno učenje na materijalima kako biste otkrili slične slike. Oslanja se na Pametno Pretraživanje", @@ -68,12 +70,13 @@ "image_thumbnail_resolution": "Razlučivost sličica", "image_thumbnail_resolution_description": "Koristi se prilikom pregledavanja grupa fotografija (glavna vremenska traka, prikaz albuma itd.). Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.", "job_concurrency": "{job} istovremenost", + "job_created": "Zadatak je kreiran", "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.", "job_settings": "Postavke posla", "job_settings_description": "Upravljajte istovremenošću poslova", "job_status": "Status posla", - "jobs_delayed": "", - "jobs_failed": "", + "jobs_delayed": "{jobCount, plural, other {# delayed}}", + "jobs_failed": "{jobCount, plural, other {# failed}}", "library_created": "Stvorena biblioteka: {library}", "library_cron_expression": "Cron izraz", "library_cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", @@ -90,14 +93,14 @@ "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)", "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke", "logging_enable_description": "Omogući zapisivanje", - "logging_level_description": "Kada je omogućeno, koju razinu zapisavanje koristiti.", - "logging_settings": "Zapisavanje", + "logging_level_description": "Kada je omogućeno, koju razinu zapisivanja koristiti.", + "logging_settings": "Zapisivanje", "machine_learning_clip_model": "CLIP model", "machine_learning_clip_model_description": "Naziv CLIP modela navedenog ovdje. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.", "machine_learning_duplicate_detection": "Detekcija Duplikata", "machine_learning_duplicate_detection_enabled": "Omogući detekciju duplikata", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", + "machine_learning_duplicate_detection_enabled_description": "Ako je onemogućeno, potpuno identična sredstva i dalje će biti deduplicirana.", + "machine_learning_duplicate_detection_setting_description": "Upotrijebite CLIP ugradnje da biste pronašli vjerojatne duplikate", "machine_learning_enabled": "Uključi strojsko učenje", "machine_learning_enabled_description": "Ukoliko je ovo isključeno, sve funkcije strojnoga učenja biti će isključene bez obzira na postavke ispod.", "machine_learning_facial_recognition": "Detekcija lica", @@ -137,7 +140,11 @@ "map_settings_description": "Upravljanje postavkama karte", "map_style_description": "URL na style.json temu karte", "metadata_extraction_job": "Izdvoj metapodatke", - "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS i rezolucija", + "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS, lica i rezolucija", + "metadata_faces_import_setting": "Omogući uvoz lica", + "metadata_faces_import_setting_description": "Uvezite lica iz EXIF podataka slike i sidecar datoteka", + "metadata_settings": "Postavke Metapodataka", + "metadata_settings_description": "Upravljanje postavkama metapodataka", "migration_job": "Migracija", "migration_job_description": "Premjestite minijature za sredstva i lica u najnoviju strukturu mapa", "no_paths_added": "Nema dodanih putanja", @@ -146,7 +153,7 @@ "note_cannot_be_changed_later": "NAPOMENA: Ovo se ne može promijeniti kasnije!", "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu", "notification_email_from_address": "Od adrese", - "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server \"", + "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server \"", "notification_email_host_description": "Poslužitelja e-pošte (npr. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoriraj pogreške certifikata", "notification_email_ignore_certificate_errors_description": "Ignoriraj pogreške provjere valjanosti TLS certifikata (nije preporučeno)", @@ -171,18 +178,20 @@ "oauth_enable_description": "Prijavite se putem OAutha", "oauth_issuer_url": "URL Izdavatelja", "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", + "oauth_mobile_redirect_uri_override": "Nadjačavanje URI-preusmjeravanja za mobilne uređaje", + "oauth_mobile_redirect_uri_override_description": "Omogući kada pružatelj OAuth ne dopušta mobilni URI, poput '{callback}'", + "oauth_profile_signing_algorithm": "Algoritam za potpisivanje profila", + "oauth_profile_signing_algorithm_description": "Algoritam koji se koristi za potpisivanje korisničkog profila.", + "oauth_scope": "Opseg", "oauth_settings": "OAuth", "oauth_settings_description": "Upravljanje postavkama za prijavu kroz OAuth", "oauth_settings_more_details": "Za više pojedinosti o ovoj značajci pogledajte uputstva.", - "oauth_signing_algorithm": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", + "oauth_signing_algorithm": "Algoritam potpisivanja", + "oauth_storage_label_claim": "Potraživanje oznake za pohranu", + "oauth_storage_label_claim_description": "Automatski postavite korisničku oznaku za pohranu na vrijednost ovog zahtjeva.", + "oauth_storage_quota_claim": "Zahtjev za kvotom pohrane", + "oauth_storage_quota_claim_description": "Automatski postavite korisničku kvotu pohrane na vrijednost ovog zahtjeva.", + "oauth_storage_quota_default": "Zadana kvota pohrane (GiB)", "oauth_storage_quota_default_description": "Kvota u GiB koja će se koristiti kada nema zahtjeva (unesite 0 za neograničenu kvotu).", "offline_paths": "Izvanmrežne putanje", "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", @@ -190,19 +199,21 @@ "password_settings": "Prijava zaporkom", "password_settings_description": "Upravljanje postavkama za prijavu zaporkom", "paths_validated_successfully": "Sve su putanje uspješno potvrđene", + "person_cleanup_job": "Čišćenje lica", "quota_size_gib": "Veličina kvote (GiB)", "refreshing_all_libraries": "Osvježavanje svih biblioteka", "registration": "Registracija administratora", "registration_description": "Budući da ste prvi korisnik na sustavu, bit ćete dodijeljeni administratorsku ulogu i odgovorni ste za administrativne poslove, a dodatne korisnike kreirat ćete sami.", - "removing_offline_files": "Uklanjanje izvanmrežnih datoteka", + "removing_deleted_files": "Uklanjanje izvanmrežnih datoteka", "repair_all": "Popravi sve", - "repair_matched_items": "", - "repaired_items": "", + "repair_matched_items": "Podudaranje {count, plural, one {# item} other {# items}}", + "repaired_items": "Popravljeno {count, plural, one {# item} other {# items}}", "require_password_change_on_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", "reset_settings_to_default": "Vrati postavke na zadane", "reset_settings_to_recent_saved": "Resetirajte postavke na nedavno spremljene postavke", "scanning_library_for_changed_files": "Skeniranje biblioteke za promijenjene datoteke", "scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke", + "search_jobs": "Traži zadatke…", "send_welcome_email": "Pošaljite email dobrodošlice", "server_external_domain_settings": "Vanjska domena", "server_external_domain_settings_description": "Domena za javno dijeljene linkove, uključujući http(s)://", @@ -210,25 +221,34 @@ "server_settings_description": "Upravljanje postavkama servera", "server_welcome_message": "Poruka dobrodošlice", "server_welcome_message_description": "Poruka koja je prikazana na prijavi.", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "system_settings": "", + "sidecar_job": "Sidecar metapodaci", + "sidecar_job_description": "Otkrijte ili sinkronizirajte sidecar metapodatke iz datotečnog sustava", + "slideshow_duration_description": "Broj sekundi za prikaz svake slike", + "smart_search_job_description": "Pokrenite strojno učenje na sredstvima za podršku pametnog pretraživanja", + "storage_template_date_time_description": "Vremenska oznaka stvaranja sredstva koristi se za informacije o datumu i vremenu", + "storage_template_date_time_sample": "Vrijeme uzorka {date}", + "storage_template_enable_description": "Omogući mehanizam predloška za pohranu", + "storage_template_hash_verification_enabled": "Omogućena hash provjera", + "storage_template_hash_verification_enabled_description": "Omogućuje hash provjeru, nemojte je onemogućiti osim ako niste sigurni u implikacije", + "storage_template_migration": "Migracija predloška za pohranu", + "storage_template_migration_description": "Primijenite trenutni {template} na prethodno prenesena sredstva", + "storage_template_migration_info": "Promjene predloška primjenjivat će se samo na nova sredstva. Za retroaktivnu primjenu predloška na prethodno prenesena sredstva, pokrenite {job}.", + "storage_template_migration_job": "Posao Migracije Predloška Pohrane", + "storage_template_more_details": "Za više pojedinosti o ovoj značajci pogledajte Predložak pohrane i njegove implikacije", + "storage_template_onboarding_description": "Kada je omogućena, ova će značajka automatski organizirati datoteke na temelju korisnički definiranog predloška. Zbog problema sa stabilnošću značajka je isključena prema zadanim postavkama. Za više informacija pogledajte dokumentaciju.", + "storage_template_path_length": "Približno ograničenje duljine putanje: {length, number}/{limit, number}", + "storage_template_settings": "Predložak pohrane", + "storage_template_settings_description": "Upravljajte strukturom mape i nazivom datoteke učitanog sredstva", + "storage_template_user_label": "{label} je korisnička oznaka za pohranu", + "system_settings": "Postavke Sustava", + "tag_cleanup_job": "Čišćenje oznaka", "theme_custom_css_settings": "Prilagođeni CSS", "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućuju prilagođavanje dizajna Immicha.", "theme_settings": "Postavke tema", "theme_settings_description": "Upravljajte prilagodbom Immich web sučelja", - "these_files_matched_by_checksum": "", + "these_files_matched_by_checksum": "Ove datoteke se podudaraju prema njihovim kontrolnim zbrojevima", "thumbnail_generation_job": "Generirajte sličice", - "thumbnail_generation_job_description": "", + "thumbnail_generation_job_description": "Generirajte velike, male i zamućene sličice za svaki materijal, kao i sličice za svaku osobu", "transcoding_acceleration_api": "API ubrzanja", "transcoding_acceleration_api_description": "API koji će komunicirati s vašim uređajem radi ubrzanja transkodiranja. Ova postavka je 'najveći trud': vratit će se na softversko transkodiranje u slučaju kvara. VP9 može ili ne mora raditi ovisno o vašem hardveru.", "transcoding_acceleration_nvenc": "NVENC (zahtjeva NVIDIA GPU)", @@ -240,201 +260,291 @@ "transcoding_accepted_containers": "Prihvaćeni kontenjeri", "transcoding_accepted_containers_description": "Odaberite koji formati spremnika ne moraju biti remulksirani u MP4. Koristi se samo za određena pravila transkodiranja.", "transcoding_accepted_video_codecs": "Prihvaćeni video kodeci", - "transcoding_accepted_video_codecs_description": "", + "transcoding_accepted_video_codecs_description": "Odaberite koje video kodeke nije potrebno transkodirati. Koristi se samo za određena pravila transkodiranja.", "transcoding_advanced_options_description": "Postavke većina korisnika ne treba mjenjati", "transcoding_audio_codec": "Audio kodek", "transcoding_audio_codec_description": "Opus je opcija s najvećom kvalitetom, no ima manju podršku s starim uređajima i softverima.", "transcoding_bitrate_description": "Videozapisi veći od maksimalne brzine prijenosa ili nisu u prihvatljivom formatu", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", + "transcoding_codecs_learn_more": "Da biste saznali više o terminologiji koja se ovdje koristi, pogledajte FFmpeg dokumentaciju za H.264 kodek, HEVC kodek i VP9 kodek.", + "transcoding_constant_quality_mode": "Način stalne kvalitete", + "transcoding_constant_quality_mode_description": "ICQ je bolji od CQP-a, ali neki uređaji za hardversko ubrzanje ne podržavaju ovaj način rada. Postavljanje ove opcije daje prednost navedenom načinu rada kada se koristi kodiranje temeljeno na kvaliteti. NVENC je zanemaren jer ne podržava ICQ.", + "transcoding_constant_rate_factor": "Faktor konstantne stope (-crf)", + "transcoding_constant_rate_factor_description": "Razina kvalitete videa. Uobičajene vrijednosti su 23 za H.264, 28 za HEVC, 31 za VP9 i 35 za AV1. Niže je bolje, ali stvara veće datoteke.", + "transcoding_disabled_description": "Nemojte transkodirati nijedan videozapis, može prekinuti reprodukciju na nekim klijentima", + "transcoding_hardware_acceleration": "Hardversko Ubrzanje", + "transcoding_hardware_acceleration_description": "Eksperimentalno; puno brže, ali će imati nižu kvalitetu pri istoj bitrate postavci", + "transcoding_hardware_decoding": "Hardversko dekodiranje", + "transcoding_hardware_decoding_setting_description": "Odnosi se samo na NVENC, QSV i RKMPP. Omogućuje ubrzanje s kraja na kraj umjesto samo ubrzavanja kodiranja. Možda neće raditi na svim videozapisima.", "transcoding_hevc_codec": "HEVC kodek", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", + "transcoding_max_b_frames": "Maksimalni B-frameovi", + "transcoding_max_b_frames_description": "Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. Možda nije kompatibilan s hardverskim ubrzanjem na starijim uređajima. 0 onemogućuje B-frameove, dok -1 automatski postavlja ovu vrijednost.", "transcoding_max_bitrate": "Maksimalne brzina prijenosa (bitrate)", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_transcode_policy_description": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "untracked_files": "", - "untracked_files_description": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_description": "", - "user_settings": "", - "user_settings_description": "", - "user_successfully_removed": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job": "", - "video_conversion_job_description": "" + "transcoding_max_bitrate_description": "Postavljanje maksimalne brzine prijenosa može učiniti veličine datoteka predvidljivijima uz manji trošak za kvalitetu. Pri 720p, tipične vrijednosti su 2600k za VP9 ili HEVC ili 4500k za H.264. Onemogućeno ako je postavljeno na 0.", + "transcoding_max_keyframe_interval": "Maksimalni interval ključnih sličica", + "transcoding_max_keyframe_interval_description": "Postavlja maksimalnu udaljenost slika između ključnih kadrova. Niže vrijednosti pogoršavaju učinkovitost kompresije, ali poboljšavaju vrijeme traženja i mogu poboljšati kvalitetu u scenama s brzim kretanjem. 0 automatski postavlja ovu vrijednost.", + "transcoding_optimal_description": "Videozapisi koji su veći od ciljne rezolucije ili nisu u prihvatljivom formatu", + "transcoding_preferred_hardware_device": "Preferirani hardverski uređaj", + "transcoding_preferred_hardware_device_description": "Odnosi se samo na VAAPI i QSV. Postavlja dri node koji se koristi za hardversko transkodiranje.", + "transcoding_preset_preset": "Preset (-preset)", + "transcoding_preset_preset_description": "Brzina kompresije. Sporije postavke proizvode manje datoteke i povećavaju kvalitetu pri ciljanju određene postavke bitratea. VP9 zanemaruje brzine iznad 'brže'.", + "transcoding_reference_frames": "Referentne slike", + "transcoding_reference_frames_description": "Broj slika za referencu prilikom komprimiranja određene slike. Više vrijednosti poboljšavaju učinkovitost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrijednost.", + "transcoding_required_description": "Samo videozapisi koji nisu u prihvaćenom formatu", + "transcoding_settings": "Postavke Video Transkodiranja", + "transcoding_settings_description": "Upravljajte informacijama o razlučivosti i kodiranju video datoteka", + "transcoding_target_resolution": "Ciljana rezolucija", + "transcoding_target_resolution_description": "Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odziv aplikacije.", + "transcoding_temporal_aq": "Vremenski AQ", + "transcoding_temporal_aq_description": "Odnosi se samo na NVENC. Povećava kvalitetu scena s puno detalja i malo pokreta. Možda nije kompatibilan sa starijim uređajima.", + "transcoding_threads": "Sljedovi (Threads)", + "transcoding_threads_description": "Više vrijednosti dovode do bržeg kodiranja, ali ostavljaju manje prostora poslužitelju za obradu drugih zadataka dok je aktivan. Ova vrijednost ne smije biti veća od broja CPU jezgri. Maksimalno povećava iskorištenje ako je postavljeno na 0.", + "transcoding_tone_mapping": "Tonsko preslikavanje", + "transcoding_tone_mapping_description": "Pokušava sačuvati izgled HDR videozapisa kada se pretvori u SDR. Svaki algoritam čini različite kompromise za boju, detalje i svjetlinu. Hable čuva detalje, Mobius čuva boju, a Reinhard svjetlinu.", + "transcoding_tone_mapping_npl": "Tone-mapping NPL", + "transcoding_tone_mapping_npl_description": "Boje će se prilagoditi tako da izgledaju normalno za zaslon ove svjetline. Suprotno intuiciji, niže vrijednosti povećavaju svjetlinu videa i obrnuto budući da kompenziraju svjetlinu zaslona. 0 automatski postavlja ovu vrijednost.", + "transcoding_transcode_policy": "Pravila transkodiranja", + "transcoding_transcode_policy_description": "Pravila o tome kada se video treba transkodirati. HDR videozapisi uvijek će biti transkodirani (osim ako je transkodiranje onemogućeno).", + "transcoding_two_pass_encoding": "Kodiranje u dva prolaza", + "transcoding_two_pass_encoding_setting_description": "Transkodiranje u dva prolaza za proizvodnju bolje kodiranih videozapisa. Kada je omogućena maksimalna brzina prijenosa (potrebna za rad s H.264 i HEVC), ovaj način rada koristi raspon brzine prijenosa na temelju maksimalne brzine prijenosa i zanemaruje CRF. Za VP9, CRF se može koristiti ako je maksimalna brzina prijenosa onemogućena.", + "transcoding_video_codec": "Video Kodek", + "transcoding_video_codec_description": "VP9 ima visoku učinkovitost i web-kompatibilnost, ali treba dulje za transkodiranje. HEVC ima sličnu izvedbu, ali ima slabiju web kompatibilnost. H.264 široko je kompatibilan i brzo se transkodira, ali proizvodi mnogo veće datoteke. AV1 je najučinkovitiji kodek, ali nema podršku na starijim uređajima.", + "trash_enabled_description": "Omogućite značajke Smeća", + "trash_number_of_days": "Broj dana", + "trash_number_of_days_description": "Broj dana za držanje sredstava u smeću prije njihovog trajnog uklanjanja", + "trash_settings": "Postavke Smeća", + "trash_settings_description": "Upravljanje postavkama smeća", + "untracked_files": "Nepraćene datoteke", + "untracked_files_description": "Aplikacija ne prati ove datoteke. Mogu biti rezultat neuspjelih premještanja, prekinutih prijenosa ili izostale zbog pogreške", + "user_cleanup_job": "Čišćenje korisnika", + "user_delete_delay": "Račun i sredstva korisnika {user} bit će zakazani za trajno brisanje za {delay, plural, one {# day} other {# days}}.", + "user_delete_delay_settings": "Brisanje odgode", + "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog računa i imovine. Posao brisanja korisnika pokreće se u ponoć kako bi se provjerili korisnici koji su spremni za brisanje. Promjene ove postavke bit će procijenjene pri sljedećem izvršavanju.", + "user_delete_immediately": "Račun i sredstva korisnika {user} bit će stavljeni u red čekanja za trajno brisanje odmah.", + "user_delete_immediately_checkbox": "Stavite korisnika i imovinu u red za trenutačno brisanje", + "user_management": "Upravljanje Korisnicima", + "user_password_has_been_reset": "Korisnička lozinka je poništena:", + "user_password_reset_description": "Molimo dostavite privremenu lozinku korisniku i obavijestite ga da će morati promijeniti lozinku pri sljedećoj prijavi.", + "user_restore_description": "Račun korisnika {user} bit će vraćen.", + "user_restore_scheduled_removal": "Vrati korisnika - zakazano uklanjanje {date, date, long}", + "user_settings": "Korisničke Postavke", + "user_settings_description": "Upravljanje korisničkim postavkama", + "user_successfully_removed": "Korisnik {email} je uspješno uklonjen.", + "version_check_enabled_description": "Omogući provjeru verzije", + "version_check_implications": "Značajka provjere verzije oslanja se na periodičnu komunikaciju s github.com", + "version_check_settings": "Provjera Verzije", + "version_check_settings_description": "Omogućite/onemogućite obavijest o novoj verziji", + "video_conversion_job": "Transkodiranje videozapisa", + "video_conversion_job_description": "Transkodiranje videozapisa za veću kompatibilnost s preglednicima i uređajima" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "albums_count": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", + "admin_email": "E-pošta administratora", + "admin_password": "Admin Lozinka", + "administration": "Administracija", + "advanced": "Napredno", + "age_months": "Dob {months, plural, one {# month} other {# months}}", + "age_year_months": "Dob 1 godina, {months, plural, one {# month} other {# months}}", + "age_years": "{years, plural, other {Age #}}", + "album_added": "Album dodan", + "album_added_notification_setting_description": "Primite obavijest e-poštom kada ste dodani u dijeljeni album", + "album_cover_updated": "Naslovnica albuma ažurirana", + "album_delete_confirmation": "Jeste li sigurni da želite izbrisati album {album}?", + "album_delete_confirmation_description": "Ako se ovaj album dijeli, drugi korisnici mu više neće moći pristupiti.", + "album_info_updated": "Podaci o albumu ažurirani", + "album_leave": "Napustiti album?", + "album_leave_confirmation": "Jeste li sigurni da želite napustiti {album}?", + "album_name": "Naziv Albuma", + "album_options": "Opcije albuma", + "album_remove_user": "Ukloni korisnika?", + "album_remove_user_confirmation": "Jeste li sigurni da želite ukloniti {user}?", + "album_share_no_users": "Čini se da ste podijelili ovaj album sa svim korisnicima ili nemate nijednog korisnika s kojim biste ga dijelili.", + "album_updated": "Album ažuriran", + "album_updated_setting_description": "Primite obavijest e-poštom kada dijeljeni album ima nova sredstva", + "album_user_left": "Napušten {album}", + "album_user_removed": "Uklonjen {user}", + "album_with_link_access": "Dopusti svima s poveznicom pristup fotografijama i osobama u ovom albumu.", + "albums": "Albumi", + "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albumi}}", + "all": "Sve", + "all_albums": "Svi albumi", + "all_people": "Svi ljudi", + "all_videos": "Svi videi", + "allow_dark_mode": "Dozvoli tamni način", + "allow_edits": "Dozvoli izmjene", + "allow_public_user_to_download": "Dopusti javnom korisniku preuzimanje", + "allow_public_user_to_upload": "Dopusti javnom korisniku učitavanje", + "anti_clockwise": "Suprotno smjeru kazaljke na satu", + "api_key": "API Ključ", + "api_key_description": "Ova će vrijednost biti prikazana samo jednom. Obavezno ju kopirajte prije zatvaranja prozora.", + "api_key_empty": "Naziv vašeg API ključa ne smije biti prazan", + "api_keys": "API Ključevi", + "app_settings": "Postavke Aplikacije", + "appears_in": "Pojavljuje se u", + "archive": "Arhiva", + "archive_or_unarchive_photo": "Arhivirajte ili dearhivirajte fotografiju", + "archive_size": "Veličina arhive", + "archive_size_description": "Konfigurirajte veličinu arhive za preuzimanja (u GiB)", "archived": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", - "back": "", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", + "archived_count": "{count, plural, other {Archived #}}", + "are_these_the_same_person": "Je li ovo ista osoba?", + "are_you_sure_to_do_this": "Jeste li sigurni da to želite učiniti?", + "asset_added_to_album": "Dodano u album", + "asset_adding_to_album": "Dodavanje u album...", + "asset_description_updated": "Opis imovine je ažuriran", + "asset_filename_is_offline": "Sredstvo {filename} je izvan mreže", + "asset_has_unassigned_faces": "Materijal ima nedodijeljena lica", + "asset_hashing": "Hashiranje...", + "asset_offline": "Sredstvo izvan mreže", + "asset_offline_description": "Ovaj materijal je izvan mreže. Immich ne može pristupiti lokaciji datoteke. Provjerite je li sredstvo dostupno, a zatim ponovno skenirajte biblioteku.", + "asset_skipped": "Preskočeno", + "asset_skipped_in_trash": "U smeću", + "asset_uploaded": "Učitano", + "asset_uploading": "Učitavanje...", + "assets": "Sredstva", + "assets_added_count": "Dodano {count, plural, one {# asset} other {# assets}}", + "assets_added_to_album_count": "Dodano {count, plural, one {# asset} other {# assets}} u album", + "assets_added_to_name_count": "Dodano {count, plural, one {# asset} other {# assets}} u {hasName, select, true {{name}} other {new album}}", + "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_moved_to_trash_count": "{count, plural, one {# asset} other {# asset}} premješteno u smeće", + "assets_permanently_deleted_count": "Trajno izbrisano {count, plural, one {# asset} other {# assets}}", + "assets_removed_count": "Uklonjeno {count, plural, one {# asset} other {# assets}}", + "assets_restore_confirmation": "Jeste li sigurni da želite vratiti sve svoje resurse bačene u otpad? Ne možete poništiti ovu radnju!", + "assets_restored_count": "Vraćeno {count, plural, one {# asset} other {# assets}}", + "assets_trashed_count": "Bačeno u smeće {count, plural, one {# asset} other {# assets}}", + "assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} već dio albuma", + "authorized_devices": "Ovlašteni Uređaji", + "back": "Nazad", + "back_close_deselect": "Natrag, zatvorite ili poništite odabir", + "backward": "Unazad", + "birthdate_saved": "Datum rođenja uspješno spremljen", + "birthdate_set_description": "Datum rođenja se koristi za izračunavanje godina ove osobe u trenutku fotografije.", + "blurred_background": "Zamućena pozadina", + "build": "Sagradi (Build)", + "build_image": "Sagradi (Build) Image", + "bulk_delete_duplicates_confirmation": "Jeste li sigurni da želite skupno izbrisati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!", + "bulk_keep_duplicates_confirmation": "Jeste li sigurni da želite zadržati {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će riješiti sve duplicirane grupe bez brisanja ičega.", + "bulk_trash_duplicates_confirmation": "Jeste li sigurni da želite na veliko baciti u smeće {count, plural, one {# duplicate asset} other {# duplicate asset}}? Ovo će zadržati najveće sredstvo svake grupe i baciti sve ostale duplikate u smeće.", + "buy": "Kupi Immich", + "camera": "Kamera", + "camera_brand": "Marka kamere", + "camera_model": "Model kamere", + "cancel": "Otkaži", + "cancel_search": "Otkaži pretragu", + "cannot_merge_people": "Nije moguće spojiti osobe", + "cannot_undo_this_action": "Ne možete poništiti ovu radnju!", + "cannot_update_the_description": "Nije moguće ažurirati opis", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", - "change_expiration_time": "", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_all": "", - "check_logs": "", - "choose_matching_people_to_merge": "", - "city": "", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_delete_shared_link": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copied_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", - "create_link": "", - "create_link_to_share": "", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_api_key_prompt": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", - "delete_shared_link": "", - "delete_user": "", - "deleted_shared_link": "", - "description": "", - "details": "", - "direction": "", - "disabled": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "downloading": "", - "duration": "", + "change_date": "Promjena datuma", + "change_expiration_time": "Promjena vremena isteka", + "change_location": "Promjena lokacije", + "change_name": "Promjena imena", + "change_name_successfully": "Promijena imena uspješna", + "change_password": "Promjena Lozinke", + "change_password_description": "Ovo je ili prvi put da se prijavljujete u sustav ili je poslan zahtjev za promjenom lozinke. Unesite novu lozinku ispod.", + "change_your_password": "Promijenite lozinku", + "changed_visibility_successfully": "Vidljivost je uspješno promijenjena", + "check_all": "Provjeri Sve", + "check_logs": "Provjera Zapisa", + "choose_matching_people_to_merge": "Odaberite odgovarajuće osobe za spajanje", + "city": "Grad", + "clear": "Očisti", + "clear_all": "Očisti sve", + "clear_all_recent_searches": "Izbriši sva nedavna pretraživanja", + "clear_message": "Jasna poruka", + "clear_value": "Očisti vrijednost", + "clockwise": "U smjeru kazaljke na satu", + "close": "Zatvori", + "collapse": "Sažmi", + "collapse_all": "Sažmi sve", + "color": "Boja", + "color_theme": "Tema boja", + "comment_deleted": "Komentar izbrisan", + "comment_options": "Opcije komentara", + "comments_and_likes": "Komentari i lajkovi", + "comments_are_disabled": "Komentari onemogućeni", + "confirm": "Potvrdi", + "confirm_admin_password": "Potvrdite lozinku administratora", + "confirm_delete_shared_link": "Jeste li sigurni da želite izbrisati ovu zajedničku vezu?", + "confirm_password": "Potvrdite lozinku", + "contain": "Sadrži", + "context": "Kontekst", + "continue": "Nastavi", + "copied_image_to_clipboard": "Slika je kopirana u međuspremnik.", + "copied_to_clipboard": "Kopirano u međuspremnik!", + "copy_error": "Greška kopiranja", + "copy_file_path": "Kopiraj put datoteke", + "copy_image": "Kopiraj Sliku", + "copy_link": "Kopiraj poveznicu", + "copy_link_to_clipboard": "Kopiraj poveznicu u međuspremnik", + "copy_password": "Kopiraj lozinku", + "copy_to_clipboard": "Kopiraj u međuspremnik", + "country": "Država", + "cover": "Naslovnica", + "covers": "Naslovnice", + "create": "Kreiraj", + "create_album": "Kreiraj album", + "create_library": "Kreiraj Biblioteku", + "create_link": "Kreiraj poveznicu", + "create_link_to_share": "Izradite vezu za dijeljenje", + "create_link_to_share_description": "Dopusti svakome s vezom da vidi odabrane fotografije", + "create_new_person": "Stvorite novu osobu", + "create_new_person_hint": "Dodijelite odabrana sredstva novoj osobi", + "create_new_user": "Kreiraj novog korisnika", + "create_tag": "Stvori oznaku", + "create_tag_description": "Napravite novu oznaku. Za ugniježđene oznake unesite punu putanju oznake uključujući kose crte.", + "create_user": "Stvori korisnika", + "created": "Stvoreno", + "current_device": "Trenutačni uređaj", + "custom_locale": "Prilagođena Lokalizacija", + "custom_locale_description": "Formatiranje datuma i brojeva na temelju jezika i regije", + "dark": "Tamno", + "date_after": "Datum nakon", + "date_and_time": "Datum i Vrijeme", + "date_before": "Datum prije", + "date_of_birth_saved": "Datum rođenja uspješno spremljen", + "date_range": "Razdoblje", + "day": "Dan", + "deduplicate_all": "Dedupliciraj Sve", + "default_locale": "Zadana lokalizacija", + "default_locale_description": "Oblikujte datume i brojeve na temelju jezika preglednika", + "delete": "Izbriši", + "delete_album": "Izbriši album", + "delete_api_key_prompt": "Jeste li sigurni da želite izbrisati ovaj API ključ?", + "delete_duplicates_confirmation": "Jeste li sigurni da želite trajno izbrisati ove duplikate?", + "delete_key": "Ključ za brisanje", + "delete_library": "Izbriši knjižnicu", + "delete_link": "Izbriši poveznicu", + "delete_shared_link": "Izbriši dijeljenu poveznicu", + "delete_tag": "Izbriši oznaku", + "delete_tag_confirmation_prompt": "Jeste li sigurni da želite izbrisati oznaku {tagName}?", + "delete_user": "Izbriši korisnika", + "deleted_shared_link": "Izbrisana dijeljena poveznica", + "description": "Opis", + "details": "Detalji", + "direction": "Smjer", + "disabled": "Onemogućeno", + "disallow_edits": "Zabrani izmjene", + "discover": "Otkrij", + "dismiss_all_errors": "Odbaci sve pogreške", + "dismiss_error": "Odbaci pogrešku", + "display_options": "Mogućnosti prikaza", + "display_order": "Redoslijed prikaza", + "display_original_photos": "Prikaz originalnih fotografija", + "display_original_photos_setting_description": "Radije prikažite izvornu fotografiju kada gledate materijal umjesto sličica kada je izvorni materijal kompatibilan s webom. To može rezultirati sporijim brzinama prikaza fotografija.", + "do_not_show_again": "Ne prikazuj više ovu poruku", + "done": "Gotovo", + "download": "Preuzmi", + "download_include_embedded_motion_videos": "Ugrađeni videozapisi", + "download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku", + "download_settings": "Preuzmi", + "download_settings_description": "Upravljajte postavkama koje se odnose na preuzimanje sredstava", + "downloading": "Preuzimanje", + "downloading_asset_filename": "Preuzimanje materijala {filename}", + "drop_files_to_upload": "Ispustite datoteke bilo gdje za prijenos", + "duplicates": "Duplikati", + "duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima", + "duration": "Trajanje", "durations": { "days": "", "hours": "", @@ -442,346 +552,551 @@ "months": "", "years": "" }, - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", - "edit_link": "", - "edit_location": "", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", - "editor": "", - "email": "", + "edit": "Izmjena", + "edit_album": "Uredi album", + "edit_avatar": "Uredi avatar", + "edit_date": "Uredi datum", + "edit_date_and_time": "Uredite datum i vrijeme", + "edit_exclusion_pattern": "Uredi uzorak izuzimanja", + "edit_faces": "Uređivanje lica", + "edit_import_path": "Uredi put uvoza", + "edit_import_paths": "Uredi Uvozne Putanje", + "edit_key": "Ključ za uređivanje", + "edit_link": "Uredi poveznicu", + "edit_location": "Uredi lokaciju", + "edit_name": "Uredi ime", + "edit_people": "Uredi ljude", + "edit_tag": "Uredi oznaku", + "edit_title": "Uredi Naslov", + "edit_user": "Uredi korisnika", + "edited": "Uređeno", + "editor": "Urednik", + "editor_close_without_save_prompt": "Promjene neće biti spremljene", + "editor_close_without_save_title": "Zatvoriti uređivač?", + "editor_crop_tool_h2_aspect_ratios": "Omjeri stranica", + "editor_crop_tool_h2_rotation": "Rotacija", + "email": "E-pošta", "empty_album": "", - "empty_trash": "", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash": "Isprazni smeće", + "empty_trash_confirmation": "Jeste li sigurni da želite isprazniti smeće? Time će se iz Immicha trajno ukloniti sva sredstva u otpadu.\nNe možete poništiti ovu radnju!", + "enable": "Omogući", + "enabled": "Omogućeno", + "end_date": "Datum završetka", + "error": "Greška", + "error_loading_image": "Pogreška pri učitavanju slike", + "error_title": "Greška - Nešto je pošlo krivo", "errors": { - "cleared_jobs": "", - "exclusion_pattern_already_exists": "", - "failed_job_command": "", - "import_path_already_exists": "", - "paths_validation_failed": "", - "quota_higher_than_disk_size": "", - "repair_unable_to_check_items": "", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_exclusion_pattern": "", - "unable_to_add_import_path": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", - "unable_to_change_password": "", - "unable_to_copy_to_clipboard": "", - "unable_to_create_api_key": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_exclusion_pattern": "", - "unable_to_delete_import_path": "", - "unable_to_delete_shared_link": "", - "unable_to_delete_user": "", - "unable_to_edit_exclusion_pattern": "", - "unable_to_edit_import_path": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_link_oauth_account": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", - "unable_to_remove_api_key": "", - "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_api_key": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_timeline_display_status": "", - "unable_to_update_user": "" + "cannot_navigate_next_asset": "Nije moguće prijeći na sljedeći materijal", + "cannot_navigate_previous_asset": "Nije moguće prijeći na prethodni materijal", + "cant_apply_changes": "Nije moguće primijeniti promjene", + "cant_change_activity": "Ne mogu {enabled, select, true {disable} druge {enable}} aktivnosti", + "cant_change_asset_favorite": "Nije moguće promijeniti favorita za sredstvo", + "cant_change_metadata_assets_count": "Nije moguće promijeniti metapodatke {count, plural, one {# asset} other {# assets}}", + "cant_get_faces": "Ne mogu dobiti lica", + "cant_get_number_of_comments": "Ne mogu dobiti broj komentara", + "cant_search_people": "Ne mogu pretraživati ljude", + "cant_search_places": "Ne mogu pretraživati mjesta", + "cleared_jobs": "Izbrisani poslovi za: {job}", + "error_adding_assets_to_album": "Pogreška pri dodavanju materijala u album", + "error_adding_users_to_album": "Pogreška pri dodavanju korisnika u album", + "error_deleting_shared_user": "Pogreška pri brisanju dijeljenog korisnika", + "error_downloading": "Pogreška pri preuzimanju {filename}", + "error_hiding_buy_button": "Pogreška pri skrivanju gumba za kupnju", + "error_removing_assets_from_album": "Pogreška prilikom uklanjanja materijala iz albuma, provjerite konzolu za više pojedinosti", + "error_selecting_all_assets": "Pogreška pri odabiru svih sredstava", + "exclusion_pattern_already_exists": "Ovaj uzorak izuzimanja već postoji.", + "failed_job_command": "Naredba {command} nije uspjela za posao: {job}", + "failed_to_create_album": "Izrada albuma nije uspjela", + "failed_to_create_shared_link": "Stvaranje dijeljene veze nije uspjelo", + "failed_to_edit_shared_link": "Nije uspjelo uređivanje dijeljene poveznice", + "failed_to_get_people": "Dohvaćanje ljudi nije uspjelo", + "failed_to_load_asset": "Učitavanje sredstva nije uspjelo", + "failed_to_load_assets": "Učitavanje sredstava nije uspjelo", + "failed_to_load_people": "Učitavanje ljudi nije uspjelo", + "failed_to_remove_product_key": "Uklanjanje ključa proizvoda nije uspjelo", + "failed_to_stack_assets": "Slaganje sredstava nije uspjelo", + "failed_to_unstack_assets": "Nije uspjelo uklanjanje snopa sredstava", + "import_path_already_exists": "Ovaj uvozni put već postoji.", + "incorrect_email_or_password": "Netočna adresa e-pošte ili lozinka", + "paths_validation_failed": "{paths, plural, one {# putanja nije prošla} other {# putanje nisu prošle}} provjeru valjanosti", + "profile_picture_transparent_pixels": "Profilne slike ne smiju imati prozirne piksele. Povećajte i/ili pomaknite sliku.", + "quota_higher_than_disk_size": "Postavili ste kvotu veću od veličine diska", + "repair_unable_to_check_items": "Nije moguće provjeriti {count, select, one {item} other {items}}", + "unable_to_add_album_users": "Nije moguće dodati korisnike u album", + "unable_to_add_assets_to_shared_link": "Nije moguće dodati sredstva na dijeljenu poveznicu", + "unable_to_add_comment": "Nije moguće dodati komentar", + "unable_to_add_exclusion_pattern": "Nije moguće dodati uzorak izuzimanja", + "unable_to_add_import_path": "Nije moguće dodati putanju uvoza", + "unable_to_add_partners": "Nije moguće dodati partnere", + "unable_to_add_remove_archive": "Nije moguće {arhivirano, odabrati, istinito {ukloniti sredstvo iz} druge {dodati sredstvo u}} arhivu", + "unable_to_add_remove_favorites": "Nije moguće {favorite, select, true {add asset to} other {remove asset from}} favorite", + "unable_to_archive_unarchive": "Nije moguće {arhivirati, odabrati, istinito {arhivirati} ostalo {dearhivirati}}", + "unable_to_change_album_user_role": "Nije moguće promijeniti ulogu korisnika albuma", + "unable_to_change_date": "Nije moguće promijeniti datum", + "unable_to_change_favorite": "Nije moguće promijeniti favorita za sredstvo", + "unable_to_change_location": "Nije moguće promijeniti lokaciju", + "unable_to_change_password": "Nije moguće promijeniti lozinku", + "unable_to_change_visibility": "Nije moguće promijeniti vidljivost za {count, plural, one {# osobu} other {# osobe}}", + "unable_to_complete_oauth_login": "Nije moguće dovršiti OAuth prijavu", + "unable_to_connect": "Povezivanje nije moguće", + "unable_to_connect_to_server": "Nije moguće spojiti se na poslužitelj", + "unable_to_copy_to_clipboard": "Nije moguće kopirati u međuspremnik, provjerite pristupate li stranici putem https-a", + "unable_to_create_admin_account": "Nije moguće stvoriti administratorski račun", + "unable_to_create_api_key": "Nije moguće izraditi novi API ključ", + "unable_to_create_library": "Nije moguće stvoriti biblioteku", + "unable_to_create_user": "Nije moguće stvoriti korisnika", + "unable_to_delete_album": "Nije moguće izbrisati album", + "unable_to_delete_asset": "Nije moguće izbrisati sredstvo", + "unable_to_delete_assets": "Pogreška pri brisanju sredstava", + "unable_to_delete_exclusion_pattern": "Nije moguće izbrisati uzorak izuzimanja", + "unable_to_delete_import_path": "Nije moguće izbrisati put uvoza", + "unable_to_delete_shared_link": "Nije moguće izbrisati dijeljenu poveznicu", + "unable_to_delete_user": "Nije moguće izbrisati korisnika", + "unable_to_download_files": "Nije moguće preuzeti datoteke", + "unable_to_edit_exclusion_pattern": "Nije moguće urediti uzorak izuzimanja", + "unable_to_edit_import_path": "Nije moguće urediti put uvoza", + "unable_to_empty_trash": "Nije moguće isprazniti otpad", + "unable_to_enter_fullscreen": "Nije moguće otvoriti cijeli zaslon", + "unable_to_exit_fullscreen": "Nije moguće izaći iz cijelog zaslona", + "unable_to_get_comments_number": "Nije moguće dobiti broj komentara", + "unable_to_get_shared_link": "Dohvaćanje dijeljene veze nije uspjelo", + "unable_to_hide_person": "Nije moguće sakriti osobu", + "unable_to_link_motion_video": "Nije moguće povezati videozapis pokreta", + "unable_to_link_oauth_account": "Nije moguće povezati OAuth račun", + "unable_to_load_album": "Nije moguće učitati album", + "unable_to_load_asset_activity": "Nije moguće učitati aktivnost sredstva", + "unable_to_load_items": "Nije moguće učitati stavke", + "unable_to_load_liked_status": "Nije moguće učitati status sviđanja", + "unable_to_log_out_all_devices": "Nije moguće odjaviti sve uređaje", + "unable_to_log_out_device": "Nije moguće odjaviti uređaj", + "unable_to_login_with_oauth": "Nije moguće prijaviti se pomoću OAutha", + "unable_to_play_video": "Nije moguće reproducirati video", + "unable_to_reassign_assets_existing_person": "Nije moguće ponovno dodijeliti imovinu na {name, select, null {postojeću osobu} other {{name}}}", + "unable_to_reassign_assets_new_person": "Nije moguće ponovno dodijeliti imovinu novoj osobi", + "unable_to_refresh_user": "Nije moguće osvježiti korisnika", + "unable_to_remove_album_users": "Nije moguće ukloniti korisnike iz albuma", + "unable_to_remove_api_key": "Nije moguće ukloniti API ključ", + "unable_to_remove_assets_from_shared_link": "Nije moguće ukloniti sredstva iz dijeljene poveznice", + "unable_to_remove_library": "Nije moguće ukloniti biblioteku", + "unable_to_remove_offline_files": "Nije moguće ukloniti izvanmrežne datoteke", + "unable_to_remove_partner": "Nije moguće ukloniti partnera", + "unable_to_remove_reaction": "Nije moguće ukloniti reakciju", + "unable_to_repair_items": "Nije moguće popraviti stavke", + "unable_to_reset_password": "Nije moguće ponovno postaviti lozinku", + "unable_to_resolve_duplicate": "Nije moguće razriješiti duplikat", + "unable_to_restore_assets": "Nije moguće vratiti imovinu", + "unable_to_restore_trash": "Nije moguće vratiti otpad", + "unable_to_restore_user": "Nije moguće vratiti korisnika", + "unable_to_save_album": "Nije moguće spremiti album", + "unable_to_save_api_key": "Nije moguće spremiti API ključ", + "unable_to_save_date_of_birth": "Nije moguće spremiti datum rođenja", + "unable_to_save_name": "Nije moguće spremiti ime", + "unable_to_save_profile": "Nije moguće spremiti profil", + "unable_to_save_settings": "Nije moguće spremiti postavke", + "unable_to_scan_libraries": "Nije moguće skenirati knjižnice", + "unable_to_scan_library": "Nije moguće skenirati knjižnicu", + "unable_to_set_feature_photo": "Nije moguće postaviti istaknutu fotografiju", + "unable_to_set_profile_picture": "Nije moguće postaviti profilnu sliku", + "unable_to_submit_job": "Nije moguće poslati posao", + "unable_to_trash_asset": "Nije moguće baciti sredstvo u smeće", + "unable_to_unlink_account": "Nije moguće prekinuti vezu računa", + "unable_to_unlink_motion_video": "Nije moguće prekinuti vezu videozapisa pokreta", + "unable_to_update_album_cover": "Nije moguće ažurirati omot albuma", + "unable_to_update_album_info": "Nije moguće ažurirati informacije o albumu", + "unable_to_update_library": "Nije moguće ažurirati biblioteku", + "unable_to_update_location": "Nije moguće ažurirati lokaciju", + "unable_to_update_settings": "Nije moguće ažurirati postavke", + "unable_to_update_timeline_display_status": "Nije moguće ažurirati status prikaza vremenske trake", + "unable_to_update_user": "Nije moguće ažurirati korisnika", + "unable_to_upload_file": "Nije moguće učitati datoteku" }, - "exit_slideshow": "", - "expand_all": "", - "expire_after": "", - "expired": "", - "explore": "", - "export": "", - "export_as_json": "", - "extension": "", - "external": "", - "external_libraries": "", + "exif": "Exif", + "exit_slideshow": "Izađi iz projekcije slideova", + "expand_all": "Proširi sve", + "expire_after": "Istječe nakon", + "expired": "Isteklo", + "expires_date": "Ističe {date}", + "explore": "Istraži", + "explorer": "Pretraživač (Explorer)", + "export": "Izvoz", + "export_as_json": "Izvezi kao JSON", + "extension": "Proširenje (Extension)", + "external": "Vanjski", + "external_libraries": "Vanjske Biblioteke", + "face_unassigned": "Nedodijeljeno", "failed_to_get_people": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", - "feature_photo_updated": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", - "filetype": "", - "filter_people": "", - "find_them_fast": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", + "favorite": "Omiljeno", + "favorite_or_unfavorite_photo": "Omiljena ili neomiljena fotografija", + "favorites": "Omiljene", + "feature_photo_updated": "Istaknuta fotografija ažurirana", + "features": "Značajke (Features)", + "features_setting_description": "Upravljajte značajkama aplikacije", + "file_name": "Naziv datoteke", + "file_name_or_extension": "Naziv ili ekstenzija datoteke", + "filename": "Naziv datoteke", + "filetype": "Vrsta datoteke", + "filter_people": "Filtrirajte ljude", + "find_them_fast": "Pronađite ih brzo po imenu pomoću pretraživanja", + "fix_incorrect_match": "Ispravite netočno podudaranje", + "folders": "Mape", + "folders_feature_description": "Pregledavanje prikaza mape za fotografije i videozapise u sustavu datoteka", + "force_re-scan_library_files": "Prisilno ponovno skeniraj sve datoteke biblioteke", + "forward": "Naprijed", + "general": "Općenito", + "get_help": "Potražite pomoć", + "getting_started": "Početak Rada", + "go_back": "Idi natrag", + "go_to_search": "Idi na pretragu", "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", - "immich_logo": "", - "import_from_json": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", + "group_albums_by": "Grupiraj albume po...", + "group_no": "Nema grupiranja", + "group_owner": "Grupiraj po vlasniku", + "group_year": "Grupiraj po godini", + "has_quota": "Ima kvotu", + "hi_user": "Bok {name} ({email})", + "hide_all_people": "Sakrij sve ljude", + "hide_gallery": "Sakrij galeriju", + "hide_named_person": "Sakrij osobu {name}", + "hide_password": "Sakrij lozinku", + "hide_person": "Sakrij osobu", + "hide_unnamed_people": "Sakrij neimenovane osobe", + "host": "Domaćin", + "hour": "Sat", + "image": "Slika", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1} i {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1}, {person2} i {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno s {person1}, {person2} i {additionalCount, number} drugih {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1} i {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} s {person1}, {person2} i {additionalCount, number} drugih {date}", + "immich_logo": "Immich Logo", + "immich_web_interface": "Immich Web Sučelje", + "import_from_json": "Uvoz iz JSON-a", + "import_path": "Putanja uvoza", + "in_albums": "U {count, plural, one {# album} other {# albuma}}", + "in_archive": "U arhivi", + "include_archived": "Uključi arhivirano", + "include_shared_albums": "Uključi dijeljene albume", + "include_shared_partner_assets": "Uključite zajedničku imovinu partnera", + "individual_share": "Pojedinačni udio", + "info": "Informacije", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "Svaki dan u 13 sati", + "hours": "{hours, plural, one {Svaki sat} few {Svakih {hours, number} sata} other {Svakih {hours, number} sati}}", + "night_at_midnight": "Svaku večer u ponoć", + "night_at_twoam": "Svake noći u 2 ujutro" }, - "invite_people": "", - "invite_to_album": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", - "let_others_respond": "", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", - "manage_shared_links": "", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "", - "matches": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_duplicates_found": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "note_apply_storage_label_to_previously_uploaded assets": "", - "note_unlimited_quota": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", - "offline_paths": "", - "offline_paths_description": "", - "ok": "", - "oldest_first": "", - "online": "", - "only_favorites": "", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_can_access": "", - "partner_can_access_assets": "", - "partner_can_access_location": "", - "partner_sharing": "", - "partners": "", - "password": "", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "invite_people": "Pozovite ljude", + "invite_to_album": "Pozovi u album", + "items_count": "{count, plural, one {# datoteka} other {# datoteke}}", + "jobs": "Poslovi", + "keep": "Zadrži", + "keep_all": "Zadrži Sve", + "keyboard_shortcuts": "Prečaci tipkovnice", + "language": "Jezik", + "language_setting_description": "Odaberite željeni jezik", + "last_seen": "Zadnji put viđen", + "latest_version": "Najnovija verzija", + "latitude": "Zemljopisna širina", + "leave": "Izađi", + "let_others_respond": "Dozvoli da drugi odgovore", + "level": "Razina", + "library": "Biblioteka", + "library_options": "Mogućnosti biblioteke", + "light": "Svjetlo", + "like_deleted": "Like izbrisan", + "link_motion_video": "Povežite videozapis pokreta", + "link_options": "Opcije veze", + "link_to_oauth": "Veza na OAuth", + "linked_oauth_account": "Povezani OAuth račun", + "list": "Popis", + "loading": "Učitavanje", + "loading_search_results_failed": "Učitavanje rezultata pretraživanja nije uspjelo", + "log_out": "Odjavi se", + "log_out_all_devices": "Odjava sa svih uređaja", + "logged_out_all_devices": "Odjavljeni su svi uređaji", + "logged_out_device": "Odjavljen uređaj", + "login": "Prijava", + "login_has_been_disabled": "Prijava je onemogućena.", + "logout_all_device_confirmation": "Jeste li sigurni da želite odjaviti sve uređaje?", + "logout_this_device_confirmation": "Jeste li sigurni da se želite odjaviti s ovog uređaja?", + "longitude": "Zemljopisna dužina", + "look": "Izgled", + "loop_videos": "Ponavljajte videozapise", + "loop_videos_description": "Omogućite automatsko ponavljanje videozapisa u pregledniku detalja.", + "make": "Proizvođač", + "manage_shared_links": "Upravljanje dijeljenim vezama", + "manage_sharing_with_partners": "Upravljajte dijeljenjem s partnerima", + "manage_the_app_settings": "Upravljajte postavkama aplikacije", + "manage_your_account": "Upravljajte svojim računom", + "manage_your_api_keys": "Upravljajte svojim API ključevima", + "manage_your_devices": "Upravljajte uređajima na kojima ste prijavljeni", + "manage_your_oauth_connection": "Upravljajte svojom OAuth vezom", + "map": "Karta", + "map_marker_for_images": "Oznaka karte za slike snimljene u {city}, {country}", + "map_marker_with_image": "Oznaka karte sa slikom", + "map_settings": "Postavke karte", + "matches": "Podudaranja", + "media_type": "Vrsta medija", + "memories": "Sjećanja", + "memories_setting_description": "Upravljajte onim što vidite u svojim sjećanjima", + "memory": "Memorija", + "memory_lane_title": "Traka sjećanja {title}", + "menu": "Izbornik", + "merge": "Spoji", + "merge_people": "Spajanje ljudi", + "merge_people_limit": "Možete spojiti najviše 5 lica odjednom", + "merge_people_prompt": "Želite li spojiti ove ljude? Ova radnja je nepovratna.", + "merge_people_successfully": "Uspješno spajanje ljudi", + "merged_people_count": "{count, plural, one {# Spojena osoba} other {# Spojene osobe}}", + "minimize": "Minimiziraj", + "minute": "Minuta", + "missing": "Nedostaje", + "model": "Model", + "month": "Mjesec", + "more": "Više", + "moved_to_trash": "Premješteno u smeće", + "my_albums": "Moji albumi", + "name": "Ime", + "name_or_nickname": "Ime ili nadimak", + "never": "Nikada", + "new_album": "Novi Album", + "new_api_key": "Novi API ključ", + "new_password": "Nova lozinka", + "new_person": "Nova osoba", + "new_user_created": "Stvoren novi korisnik", + "new_version_available": "DOSTUPNA NOVA VERZIJA", + "newest_first": "Prvo najnovije", + "next": "Sljedeće", + "next_memory": "Sljedeće sjećanje", + "no": "Ne", + "no_albums_message": "Izradite album za organiziranje svojih fotografija i videozapisa", + "no_albums_with_name_yet": "Čini se da još nemate nijedan album s ovim imenom.", + "no_albums_yet": "Čini se da još nemate nijedan album.", + "no_archived_assets_message": "Arhivirajte fotografije i videozapise kako biste ih sakrili iz prikaza fotografija", + "no_assets_message": "KLIKNITE DA PRENESETE SVOJU PRVU FOTOGRAFIJU", + "no_duplicates_found": "Nisu pronađeni duplikati.", + "no_exif_info_available": "Nema dostupnih exif podataka", + "no_explore_results_message": "Prenesite više fotografija da istražite svoju zbirku.", + "no_favorites_message": "Dodajte favorite kako biste brzo pronašli svoje najbolje slike i videozapise", + "no_libraries_message": "Stvorite vanjsku biblioteku za pregled svojih fotografija i videozapisa", + "no_name": "Bez imena", + "no_places": "Nema mjesta", + "no_results": "Nema rezultata", + "no_results_description": "Pokušajte sa sinonimom ili općenitijom ključnom riječi", + "no_shared_albums_message": "Stvorite album za dijeljenje fotografija i videozapisa s osobama u svojoj mreži", + "not_in_any_album": "Ni u jednom albumu", + "note_apply_storage_label_to_previously_uploaded assets": "Napomena: Da biste primijenili Oznaku za skladištenje na prethodno prenesena sredstva, pokrenite", + "note_unlimited_quota": "napomena: Unesite 0 za neograni%C4%8Denu kvotu", + "notes": "Bilješke", + "notification_toggle_setting_description": "Omogući obavijesti putem e-pošte", + "notifications": "Obavijesti", + "notifications_setting_description": "Upravljanje obavijestima", + "oauth": "OAuth", + "offline": "Izvan mreže", + "offline_paths": "Izvanmrežne putanje", + "offline_paths_description": "Ovi rezultati mogu biti posljedica ručnog brisanja datoteka koje nisu dio vanjske biblioteke.", + "ok": "Ok", + "oldest_first": "Prvo najstarije", + "onboarding": "Uključivanje (Onboarding)", + "onboarding_privacy_description": "Sljedeće (neobavezne) značajke oslanjaju se na vanjske usluge i mogu se onemogućiti u bilo kojem trenutku u postavkama administracije.", + "onboarding_theme_description": "Odaberite temu boja za svoj primjer. To možete kasnije promijeniti u postavkama.", + "onboarding_welcome_description": "Postavimo vašu instancu s nekim uobičajenim postavkama.", + "onboarding_welcome_user": "Dobro došli, {user}", + "online": "Dostupan (Online)", + "only_favorites": "Samo omiljeno", + "only_refreshes_modified_files": "Osvježava samo izmijenjene datoteke", + "open_in_map_view": "Otvori u prikazu karte", + "open_in_openstreetmap": "Otvori u OpenStreetMap", + "open_the_search_filters": "Otvorite filtre pretraživanja", + "options": "Opcije", + "or": "ili", + "organize_your_library": "Organizirajte svoju knjižnicu", + "original": "original", + "other": "Ostalo", + "other_devices": "Ostali uređaji", + "other_variables": "Ostale varijable", + "owned": "Vlasništvo", + "owner": "Vlasnik", + "partner": "Partner", + "partner_can_access": "{partner} može pristupiti", + "partner_can_access_assets": "Sve vaše fotografije i videi osim onih u arhivi i smeću", + "partner_can_access_location": "Mjesto otkuda je slika otkinuta", + "partner_sharing": "Dijeljenje s partnerom", + "partners": "Partneri", + "password": "Zaporka", + "password_does_not_match": "Zaporka se ne podudara", + "password_required": "Zaporka je obavezna", + "password_reset_success": "Reset zaporke je uspješan", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "{days, plural, one {Prošli dan} few {Prošlih # dana} other {Prošlih # dana}}", + "hours": "{hours, plural, one {Prošli sat} few {Prošla # sata} other {Prošlih # sati}}", + "years": "{years, plural, one {Prošle godine} few {Prošle # godine} other {Prošlih # godina}}" }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", - "people": "", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", - "photos": "", - "photos_count": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_from_album": "", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "remove_offline_files": "", - "removed_api_key": "", - "rename": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "require_user_to_change_password_on_first_login": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", - "restore": "", - "restore_all": "", - "restore_user": "", - "resume": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "", - "saved_api_key": "", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", - "search": "", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", + "path": "Putanja", + "pattern": "Uzorak", + "pause": "Pauza", + "pause_memories": "Pauziraj sjećanja", + "paused": "Pauzirano", + "pending": "Na čekanju", + "people": "Ljudi", + "people_edits_count": "Izmjenjeno {count, plural, one {# osoba} other {# osobe}}", + "people_feature_description": "Pregledavanje fotografija i videozapisa grupiranih po osobama", + "people_sidebar_description": "Prikažite poveznicu na Osobe na bočnoj traci", + "permanent_deletion_warning": "Upozorenje za nepovratno brisanje", + "permanent_deletion_warning_setting_description": "Prikaži upozorenje prilikom trajnog brisanja sredstava", + "permanently_delete": "Nepovratno obriši", + "permanently_delete_assets_count": "Trajno izbriši {count, plural, one {datoteku} other {datoteke}}", + "permanently_delete_assets_prompt": "Da li ste sigurni da želite trajni izbrisati {count, plural, one {ovu datoteku?} other {ove # datoteke?}}Ovo će ih također ukloniti {count, plural, one {iz njihovog} other {iz njihovih}} albuma.", + "permanently_deleted_asset": "Trajno izbrisano sredstvo", + "permanently_deleted_assets_count": "Trajno izbrisano {count, plural, one {# datoteka} other {# datoteke}}", + "person": "Osoba", + "person_hidden": "{name}{hidden, select, true { (skriveno)} other {}}", + "photo_shared_all_users": "Čini se da ste svoje fotografije podijelili sa svim korisnicima ili nemate nijednog korisnika s kojim biste ih podijelili.", + "photos": "Fotografije", + "photos_and_videos": "Fotografije i videozapisi", + "photos_count": "{count, plural, one {{count, number} fotografija} few {{count, number} fotografije} other {{count, number} fotografija}}", + "photos_from_previous_years": "Fotografije iz prethodnih godina", + "pick_a_location": "Odaberite lokaciju", + "place": "Mjesto", + "places": "Mjesta", + "play": "Pokreni", + "play_memories": "Pokreni sjećanja", + "play_motion_photo": "Reproduciraj Pokretnu fotografiju", + "play_or_pause_video": "Reproducirajte ili pauzirajte video", + "port": "Port", + "preset": "Unaprijed postavljeno", + "preview": "Pregled", + "previous": "Prethodno", + "previous_memory": "Prethodno sjećanje", + "previous_or_next_photo": "Prethodna ili sljedeća fotografija", + "primary": "Primarna (Primary)", + "privacy": "Privatnost", + "profile_image_of_user": "Profilna slika korisnika {user}", + "profile_picture_set": "Profilna slika postavljena.", + "public_album": "Javni album", + "public_share": "Javno dijeljenje", + "purchase_account_info": "Podržava softver", + "purchase_activated_subtitle": "Hvala što podržavate Immich i softver otvorenog koda", + "purchase_activated_time": "Aktivirano {date, date}", + "purchase_activated_title": "Vaš ključ je uspješno aktiviran", + "purchase_button_activate": "Aktiviraj", + "purchase_button_buy": "Kupi", + "purchase_button_buy_immich": "Kupi Immich", + "purchase_button_never_show_again": "Nikad više ne prikazuj", + "purchase_button_reminder": "Podsjeti me za 30 dana", + "purchase_button_remove_key": "Ukloni ključ", + "purchase_button_select": "Odaberite", + "purchase_failed_activation": "Aktivacija nije uspjela! Provjerite svoju e-poštu za točan ključ proizvoda!", + "purchase_individual_description_1": "Za pojedinca", + "purchase_individual_description_2": "Status podržavanja", + "purchase_individual_title": "Pojedinačna licenca", + "purchase_input_suggestion": "Imate ključ proizvoda? Unesite ključ ispod", + "purchase_license_subtitle": "Kupite Immich kako biste podržali kontinuirani razvoj usluge", + "purchase_lifetime_description": "Doživotna kupnja", + "purchase_option_title": "MOGUĆNOSTI KUPNJE", + "purchase_panel_info_1": "Za izgradnju Immicha potrebno je puno vremena i truda, a mi imamo inženjere koji rade na tome s punim radnim vremenom kako bismo ga učinili što boljim. Naša je misija da softver otvorenog koda i etička poslovna praksa postanu održivi izvor prihoda za programere i da se stvori ekosustav koji poštuje privatnost sa stvarnim alternativama eksploatacijskim uslugama u oblaku.", + "purchase_panel_info_2": "Budući da se obvezujemo da nećemo dodavati dodatne pretplate, ova vam kupnja neće dodijeliti nikakve dodatne značajke u Immichu. Oslanjamo se na korisnike poput vas da podržimo stalni razvoj Immicha.", + "purchase_panel_title": "Podrži projekt", + "purchase_per_server": "Po serveru", + "purchase_per_user": "Po korisniku", + "purchase_remove_product_key": "Ukloni ključ proizvoda", + "purchase_remove_product_key_prompt": "Jeste li sigurni da želite ukloniti ključ proizvoda?", + "purchase_remove_server_product_key": "Uklonite ključ proizvoda poslužitelja (Server)", + "purchase_remove_server_product_key_prompt": "Jeste li sigurni da želite ukloniti ključ proizvoda poslužitelja (Server)?", + "purchase_server_description_1": "Za cijeli server", + "purchase_server_description_2": "Status podupiratelja", + "purchase_server_title": "Poslužitelj (Server)", + "purchase_settings_server_activated": "Ključem proizvoda poslužitelja upravlja administrator", + "rating": "Broj zvjezdica", + "rating_clear": "Obriši ocjenu", + "rating_count": "{count, plural, one {# zvijezda} other {# zvijezde}}", + "rating_description": "Prikaži EXIF ocjenu na info ploči", + "reaction_options": "Mogućnosti reakcije", + "read_changelog": "Pročitajte Dnevnik promjena", + "reassign": "Ponovno dodijeli", + "reassigned_assets_to_existing_person": "Ponovo dodijeljeno{count, plural, one {# datoteka} other {# datoteke}} postojećoj {name, select, null {osobi} other {{name}}}", + "reassigned_assets_to_new_person": "Ponovo dodijeljeno {count, plural, one {# datoteka} other {# datoteke}} novoj osobi", + "reassing_hint": "Dodijelite odabrane datoteke postojećoj osobi", + "recent": "Nedavno", + "recent_searches": "Nedavne pretrage", + "refresh": "Osvježi", + "refresh_encoded_videos": "Osvježite kodirane videozapise", + "refresh_metadata": "Osvježi metapodatke", + "refresh_thumbnails": "Osvježi sličice", + "refreshed": "Osvježeno", + "refreshes_every_file": "Osvježava svaku datoteku", + "refreshing_encoded_video": "Osvježavanje kodiranog videa", + "refreshing_metadata": "Osvježavanje metapodataka", + "regenerating_thumbnails": "Obnavljanje sličica", + "remove": "Ukloni", + "remove_assets_album_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz albuma?", + "remove_assets_shared_link_confirmation": "Jeste li sigurni da želite ukloniti {count, plural, one {# datoteku} other {# datoteke}} iz ove dijeljene veze?", + "remove_assets_title": "Ukloniti datoteke?", + "remove_custom_date_range": "Ukloni prilagođeni datumski raspon", + "remove_deleted_assets": "", + "remove_from_album": "Ukloni iz albuma", + "remove_from_favorites": "Ukloni iz favorita", + "remove_from_shared_link": "Ukloni iz dijeljene poveznice", + "remove_user": "Ukloni korisnika", + "removed_api_key": "Uklonjen API ključ: {name}", + "removed_from_archive": "Uklonjeno iz arhive", + "removed_from_favorites": "Uklonjeno iz favorita", + "removed_from_favorites_count": "{count, plural, other {Uklonjeno #}} iz omiljenih", + "removed_tagged_assets": "Uklonjena oznaka iz {count, plural, one {# datoteke} other {# datoteka}}", + "rename": "Preimenuj", + "repair": "Popravi", + "repair_no_results_message": "Nepraćene datoteke i datoteke koje nedostaju pojavit će se ovdje", + "replace_with_upload": "Zamijeni s prijenosom", + "repository": "Spremište (Repository)", + "require_password": "Zahtijevaj lozinku", + "require_user_to_change_password_on_first_login": "Zahtijevajte od korisnika promjenu lozinke pri prvoj prijavi", + "reset": "Reset", + "reset_password": "Resetiraj lozinku", + "reset_people_visibility": "Poništi vidljivost ljudi", + "reset_to_default": "Vrati na zadano", + "resolve_duplicates": "Riješite duplikate", + "resolved_all_duplicates": "Razriješi sve duplikate", + "restore": "Oporavi", + "restore_all": "Oporavi sve", + "restore_user": "Vrati korisnika", + "restored_asset": "Obnovljena datoteka", + "resume": "Nastavi", + "retry_upload": "Ponovi prijenos", + "review_duplicates": "Pregledajte duplikate", + "role": "Uloga", + "role_editor": "Urednik", + "role_viewer": "Gledatelj", + "save": "Spremi", + "saved_api_key": "Spremljen API ključ", + "saved_profile": "Spremljen profil", + "saved_settings": "Spremljene postavke", + "say_something": "Reci nešto", + "scan_all_libraries": "Skeniraj sve Knjižnice", + "scan_all_library_files": "Ponovno skenirajte sve datoteke Knjižnice", + "scan_new_library_files": "Skeniraj nove datoteke Knjižnice", + "scan_settings": "Postavke skeniranja", + "scanning_for_album": "Skeniranje albuma...", + "search": "Pretraživanje", + "search_albums": "Traži albume", + "search_by_context": "Pretraživanje po kontekstu", + "search_by_filename": "Pretražujte prema nazivu datoteke ili ekstenziji", + "search_by_filename_example": "npr. IMG_1234.JPG ili PNG", + "search_camera_make": "Pretražite marku kamere...", + "search_camera_model": "Pretražite model kamere...", + "search_city": "Pretražite grad...", + "search_country": "Pretražite državu...", + "search_for_existing_person": "Potražite postojeću osobu", + "search_no_people": "Nema ljudi", + "search_no_people_named": "Nema osoba s imenom \"{name}\"", + "search_options": "Opcije pretraživanja", + "search_people": "Traži ljude", + "search_places": "Traži mjesta", + "search_settings": "Postavke pretraživanja", "search_state": "", "search_timezone": "", "search_type": "", diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json index 7869e956c7b01..d21cfa0911b52 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -34,13 +34,14 @@ "authentication_settings_reenable": "Az újbóli engedélyezéshez használjon egySzerver Parancsot.", "background_task_job": "Háttérfolyamatok", "check_all": "Összes Kipiálása", - "cleared_jobs": "{job} munkák kitörölve", + "cleared_jobs": "{job}: feladatok törölve", "config_set_by_file": "A konfigurációt jelenleg egy konfigurációs fájl állítja be", "confirm_delete_library": "Biztosan ki szeretné törölni a {library} képtárat?", "confirm_delete_library_assets": "Biztosan kitörli ezt a képtárat? Ez kitöröl {count, plural, one {#} other {#}} benne lévő fájlt az Immichből és nem visszavonható. A fájlok a lemezen maradnak.", "confirm_email_below": "A megerősítéshez írja \"{email}\"-t alább", "confirm_reprocess_all_faces": "Biztos benne, hogy újra szeretné feldolgozni az összes arcot? Ez a megnevezett személyeket is törli.", "confirm_user_password_reset": "Biztosan vissza szeretné állítani {user} jelszavát?", + "create_job": "Feladat létrehozása", "crontab_guru": "Crontab Guru", "disable_login": "Belépés letiltása", "disabled": "Letiltva", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Bélyegkép felbontás", "image_thumbnail_resolution_description": "Képek csoportosított nézetekor használatos (idővonal, album nézet stb). Nagyobb felbontás esetén a kép részletgazdagabb marad, de tovább tart elkészíteni, nagyobb fájl méretet eredményes, és ronthatja az alkalmazás reagálását.", "job_concurrency": "{job} párhuzamosság", + "job_created": "Feladat létrehozva", "job_not_concurrency_safe": "Ez a feladat nem párhuzamosság-biztos.", "job_settings": "Feladat beállítások", "job_settings_description": "Feladatok párhuzamosságának beállítása", @@ -96,8 +98,8 @@ "logging_settings": "Naplózás", "machine_learning_clip_model": "CLIP modell", "machine_learning_clip_model_description": "Egy CLIP modell neve az itt felsoroltak közül. A modell megváltoztatása után újra kell futtatni az 'Okos Keresés' munkát minden képre.", - "machine_learning_duplicate_detection": "Másolatok Észlelése", - "machine_learning_duplicate_detection_enabled": "Másolatkeresés engedélyezése", + "machine_learning_duplicate_detection": "Duplikáltak Észlelése", + "machine_learning_duplicate_detection_enabled": "Duplikáltak keresésének engedélyezése", "machine_learning_duplicate_detection_enabled_description": "Ha ki van kapcsolva, a pontosan azonos fájlok akkor sem lesznek duplikálva.", "machine_learning_duplicate_detection_setting_description": "CLIP beágyazások használata a valószínű másolatok kereséséhez", "machine_learning_enabled": "Gépi tanulás engedélyezése", @@ -107,7 +109,7 @@ "machine_learning_facial_recognition_model": "Arcfelismerési modell", "machine_learning_facial_recognition_model_description": "A modellek méret szerint csökkenő sorrendben vannak felsorolva. A nagyobb modellek lassabbak és több memóriát használnak, de jobb eredményt produkálnak. Modellváltás után az összes képen újra le kell futtatni az arcfelismerési feladatot.", "machine_learning_facial_recognition_setting": "Arckeresés engedélyezése", - "machine_learning_facial_recognition_setting_description": "Ha ki van kapcsolva, a képek nem lesznek az arcfelismerésen lefuttatva és a Felfedezés oldalon az Személyek szekcióban nem fog szerepelni senki.", + "machine_learning_facial_recognition_setting_description": "Ha ki van kapcsolva, a képek nem lesznek az arcfelismerésen lefuttatva és a Böngészés oldalon az Személyek szekcióban nem fog szerepelni senki.", "machine_learning_max_detection_distance": "Maximum észlelési távolság", "machine_learning_max_detection_distance_description": "Két kép közötti maximális távolság, amely esetében még másolatnak tekintjük őket (0.001 és 0.1 közötti érték). Magasabb értékek több másolatot találnak meg, de a hamis találatok esélye is nagyobb.", "machine_learning_max_recognition_distance": "Maximum felismerési távolság", @@ -138,17 +140,21 @@ "map_settings": "Térkép", "map_settings_description": "Térkép beállítások kezelése", "map_style_description": "Egy style.json térképstílusra mutató URL", - "metadata_extraction_job": "Metaadatok feldolgozása", - "metadata_extraction_job_description": "Metaadat-információk kinyerése minden tartalomból, például GPS és felbontás", - "migration_job": "Migráció", - "migration_job_description": "Az képi vagyon és arcok bélyegképeinek migrálása a legújabb mappastruktúrába", + "metadata_extraction_job": "Metaadatok kinyerése", + "metadata_extraction_job_description": "Metaadat-információk kinyerése minden fájlból, például GPS, arcok és felbontás", + "metadata_faces_import_setting": "Arc importálás engedélyezése", + "metadata_faces_import_setting_description": "Arcok importálása a kép Exif adatából és metaadat fájlokból", + "metadata_settings": "Metaadat beállítások", + "metadata_settings_description": "Metaadat-beállítások kezelése", + "migration_job": "Migrálás", + "migration_job_description": "A fájlok és arcok bélyegképeinek migrálása a legújabb mappastruktúrába", "no_paths_added": "Nincs megadva elérési útvonal", "no_pattern_added": "Nincs megadva illesztési minta (pattern)", "note_apply_storage_label_previous_assets": "Megjegyzés: Tárolási Cimkék már korábban feltöltött képi vagyonra ragasztásához futtasd a következőt -", "note_cannot_be_changed_later": "FIGYELEM: ezt később nem lehet megváltoztatni!", "note_unlimited_quota": "Megjegyzés: 0 - korlátlan kvóta", "notification_email_from_address": "Feladó cím", - "notification_email_from_address_description": "Küldő email címe, például: \"Immich Fotószerver \"", + "notification_email_from_address_description": "Küldő email címe, például: \"Immich Fotószerver \"", "notification_email_host_description": "Email szerver kiszolgálója (pl. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Tanúsítvány hibák figyelmen kívül hagyása", "notification_email_ignore_certificate_errors_description": "TLS tanúsítvány érvényességi hibák figyelmen kívül hagyása (nem ajánlott)", @@ -174,7 +180,7 @@ "oauth_issuer_url": "Kibocsátó URL", "oauth_mobile_redirect_uri": "Mobil átirányítási URI", "oauth_mobile_redirect_uri_override": "Mobil átirányítási URI felülírás", - "oauth_mobile_redirect_uri_override_description": "Engedélyezze, ha az 'app.immich:/' érvénytelen átirányítási URI.", + "oauth_mobile_redirect_uri_override_description": "Engedélyezze, ha az OAuth szolgáltató tiltja a mobil URI-t, mint például '{callback}'", "oauth_profile_signing_algorithm": "Profil aláíró algoritmus", "oauth_profile_signing_algorithm_description": "A felhasználói profil aláírásához használt algoritmus.", "oauth_scope": "Hatókör", @@ -194,11 +200,12 @@ "password_settings": "Jelszavas Bejelentkezés", "password_settings_description": "Jelszavas bejelentkezés beállítások kezelése", "paths_validated_successfully": "Összes útvonal sikeresen érvényesítve", + "person_cleanup_job": "Személy törlése", "quota_size_gib": "Kvóta Mérete (GiB)", "refreshing_all_libraries": "Összes képtár újratöltése", "registration": "Admin Regisztráció", "registration_description": "Mivel ez az első felhasználó a rendszerben, ez a felhasználó lesz az Admin és lesz felelős adminisztratív teendőkért, illetve további felhasználókat ő tud létrehozni.", - "removing_offline_files": "Offline Fájlok eltávolítása", + "removing_deleted_files": "Offline Fájlok eltávolítása", "repair_all": "Összes Javítása", "repair_matched_items": "{count, plural, one {# egyezés} other {# egyezés}}", "repaired_items": "Javítva {count, plural, one {# fájl} other {# fájl}}", @@ -207,6 +214,7 @@ "reset_settings_to_recent_saved": "Beállítások visszaállítása a legutóbb mentettre", "scanning_library_for_changed_files": "Képtár átfésülése megváltozott fájlok után", "scanning_library_for_new_files": "Képtár átfésülése új fájlok után", + "search_jobs": "Feladat keresés...", "send_welcome_email": "Üdvözlő email küldése", "server_external_domain_settings": "Külső domain", "server_external_domain_settings_description": "Nyilvánosan megosztott linkek domainje (http(s)://-sel)", @@ -214,10 +222,10 @@ "server_settings_description": "Szerver beállítások kezelése", "server_welcome_message": "Üdvözlő üzenet", "server_welcome_message_description": "A bejelentkezőoldalon megjelenő üzenet.", - "sidecar_job": "Oldalkocsi fájl metaadatok", - "sidecar_job_description": "Fedezze fel vagy szinkronizálja az oldalkocsi fájlokban tárolt metaadatokat a fájlrendszerből", + "sidecar_job": "Metaadat feldolgozás", + "sidecar_job_description": "Metaadatok keresése vagy szinkronizálása a fájlrendszer alapján", "slideshow_duration_description": "Az egyes képek megjelenítésének ideje másodpercben", - "smart_search_job_description": "Futtasson gépi tanulást a képi vagyonon az intelligens keresés támogatása érdekében", + "smart_search_job_description": "Gépi tanulás futtatása a fájlokon az okos keresés támogatásához", "storage_template_date_time_description": "A fájl készítési időpontja lesz felhasználva az időpont információhoz", "storage_template_date_time_sample": "Példa időpont {date}", "storage_template_enable_description": "Tárolási sablon motor engedélyezése", @@ -234,13 +242,14 @@ "storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét", "storage_template_user_label": "A felhasználó Tároló Címkéje {label}", "system_settings": "Rendszerbeállítások", + "tag_cleanup_job": "Címke törlés", "theme_custom_css_settings": "Egyedi CSS", "theme_custom_css_settings_description": "CSS Stíluslapokkal az Immich stílusa megváltoztatható.", "theme_settings": "Stílus Beállítások", "theme_settings_description": "Kezelje az Immich webes felület testreszabását", "these_files_matched_by_checksum": "Ezek a fájlok egyeznek az ellenőrző összegük alapján", "thumbnail_generation_job": "Bélyegképek Generálása", - "thumbnail_generation_job_description": "Hozzon létre nagy, kicsi és elmosódott bélyegképeket minden egyes elemhez, valamint bélyegképeket minden egyes személyhez", + "thumbnail_generation_job_description": "Nagy, kicsi és elmosódott bélyegképek létrehozása minden elemhez, valamint bélyegképek generálása minden személyhez", "transcode_policy_description": "", "transcoding_acceleration_api": "Gyorsító API", "transcoding_acceleration_api_description": "Az API, amely interakcióba lép az eszközzel az átkódolás felgyorsítása érdekében. Ez a beállítás a „legtöbb, amit megtehetünk” alapon működik: hiba esetén visszaáll a szoftveres átkódolásra. A VP9 a hardvertől függően vagy működik, vagy nem.", @@ -256,11 +265,11 @@ "transcoding_accepted_video_codecs_description": "Válassza ki, mely videó kodexeket nem kell átkódolni. Csak bizonyos átkódolási szabályzatokhoz használatos.", "transcoding_advanced_options_description": "Ezeket az opciókat a legtöbb felhasználónak nem kell módosítania", "transcoding_audio_codec": "Audio kodek", - "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb minőség ugyanannyi helyet foglalva), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.", + "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb hangminőség ugyanakkora tárhelyen), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.", "transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat", "transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a H.264 kodekhez, a HEVC kodekhez és a VP9 kodekhez.", "transcoding_constant_quality_mode": "Állandó minőségi mód", - "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.", + "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont az előbbit nem minden hardver támogatja. A rendszer az itt beállított módot preferálja a minőség orientált enkódoláshoz. Az NVENC nem használja ezt a beállítást, mivel nem támogatja az ICQ-t.", "transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)", "transcoding_constant_rate_factor_description": "Videó minőségi szint. Jellemző értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.", "transcoding_disabled_description": "Ne transzkódoljon videót. Nem lejátszható videókhoz vezethet néhány kliensen", @@ -286,7 +295,7 @@ "transcoding_settings": "Videó Transzkódolási Beállítások", "transcoding_settings_description": "Videófájlok felbontásának és kódólásának kezelése", "transcoding_target_resolution": "Célfelbontás", - "transcoding_target_resolution_description": "Magasabb felbontás jobb minőségben őrzi meg a részleteket, de tovább tart kódolni, nagyobb fájlmérethez vezet, és csökkentheti az alkalmazás teljesítményét.", + "transcoding_target_resolution_description": "Magasabb felbontás jobb minőségben őrzi meg a részleteket, de tovább tart kódolni, nagyobb fájlmérethez vezet, és csökkentheti az alkalmazás válaszidejét.", "transcoding_temporal_aq": "Időbeli (Temporal) AQ", "transcoding_temporal_aq_description": "Csak NVENC esetén. Növeli a nagyon részletes, keveset mozgó videóanyag minőségét. Nem minden régi hardver támogatja.", "transcoding_threads": "Folyamatok száma", @@ -298,16 +307,17 @@ "transcoding_transcode_policy": "Transzkódolási szabályzat", "transcoding_transcode_policy_description": "Mely videókat transzkódolja. HDR videók mindig transzkódolásra kerülnek (kivéve, ha a transzkódolás ki van kapcsolva).", "transcoding_two_pass_encoding": "Enkódolás két menetben", - "transcoding_two_pass_encoding_setting_description": "Ha két menetben lettek transzkódolva, az elkészült videok jobbak. Ha engedélyezve van a bitráta maximalizálása (amely egyébként szükséges a H.264 és a HEVC használatakor), ez a funkció figyelmen kívül hagyja a CRF-et és a maximális bitráta alapján választja ki a megfelelő bitráta sávot. VP9 használata esetén CRF használható, ha a bitráta nincs maxmalizáva (ki van kapcsolva).", + "transcoding_two_pass_encoding_setting_description": "Ha két menetben lettek transzkódolva, az elkészült videók jobb minőségűek. Ha engedélyezve van a bitráta maximalizálása (amely szükséges a H.264 és a HEVC használatakor), ez a funkció figyelmen kívül hagyja a CRF-et. VP9 használata esetén a CRF használható, ha a bitráta nincs maximalizálva (ki van kapcsolva).", "transcoding_video_codec": "Videó Kodek", "transcoding_video_codec_description": "VP9 hatékonyabb és kompatibilisebb webre, de tovább tart a transzkódolás. HEVC hasonló teljesítményű, de több web kompatibilitási problémát okozhat. H.264 széles körben kompatibilis és gyors a transzkódolása, de sokkal nagyobb fájlokat készít. AV1 a leghatékonyabb kodek, de régebbi eszközök nem támogatják.", "trash_enabled_description": "Lomtár engedélyezése", "trash_number_of_days": "Napok száma", - "trash_number_of_days_description": "Hány napig legyenek a lomtárban tárolva a törölt képek, videok, mielőtt véglegesen kiürítődnek", + "trash_number_of_days_description": "Hány napig legyenek a lomtárban a fájlok a végleges törlés előtt", "trash_settings": "Lomtár Beállítások", "trash_settings_description": "Lomtár beállítások kezelése", "untracked_files": "Nem kezelt fájlok", "untracked_files_description": "Ezekkel a fájlokkal semmit nem csinál az alkalmazás. Ez lehetséges pl. meghiúsult mozgatás, megszakított feltöltés miatt, vagy valamilyen alkalmazáshiba következtében", + "user_cleanup_job": "Felhasználó adatainak törlése", "user_delete_delay": "{user} felhasználói fiókja és képi vagyona véglegesen törölve lesz {delay, plural, one {# nap} other {# nap}} múlva.", "user_delete_delay_settings": "Törlési késleltetés", "user_delete_delay_settings_description": "Ennyi nap teljen el az eltávolítás után a felhasználói fiók és ahhoz tartozó elemek végleges törlése között. A törlésért felelős folyamat éjfélkor indul, és megnézi van-e törlésre kész felhasználó. A beállítás változtatása a következő végrehajtás során lép életbe.", @@ -338,7 +348,8 @@ "album_added": "Albumhoz hozzáadva", "album_added_notification_setting_description": "Küldjön emailes értesítőt, amikor hozzáadnak egy megosztott albumhoz", "album_cover_updated": "Album borító frissítve", - "album_delete_confirmation": "Biztos, hogy ki szeretné törölni a {album} albumot?\nAmennyiben ez egy megosztott album, a többi felhasználó sem fog tudni hozzáférni.", + "album_delete_confirmation": "Biztos, hogy ki szeretné törölni a(z) {album} albumot?", + "album_delete_confirmation_description": "Amennyiben ez egy megosztott album, a többi felhasználó sem fog tudni hozzáférni.", "album_info_updated": "Album infó frissítve", "album_leave": "Elhagyja az albumot?", "album_leave_confirmation": "Biztos, hogy el szeretné hagyni a {album} albumot?", @@ -374,7 +385,7 @@ "archive_size": "Archívum mérete", "archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)", "archived": "Archíválva", - "archived_count": "{count, plural, other {Archived #}}", + "archived_count": "{count, plural, other {Archiválva #}}", "are_these_the_same_person": "Ugyanaz a személy?", "are_you_sure_to_do_this": "Biztosan ezt akarod csinálni?", "asset_added_to_album": "Hozzáadva az albumhoz", @@ -386,6 +397,7 @@ "asset_offline": "Elem offline", "asset_offline_description": "Ez az elem nem elérhető. Immich nem képes elérni a file helyét. Győződjön meg az elem elérhetőségéről és szkennelje újra a könyvtárat.", "asset_skipped": "Kihagyva", + "asset_skipped_in_trash": "Lomtárban", "asset_uploaded": "Feltöltve", "asset_uploading": "Feltöltés...", "assets": "elemek", @@ -394,12 +406,12 @@ "assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva a(z) {hasName, select, true {{name}} other {új}} albumba", "assets_count": "{count, plural, other {# elem}}", "assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva", - "assets_moved_to_trash_count": "{count, plural, other {# elem}} szemétbe mozgatva", + "assets_moved_to_trash_count": "{count, plural, other {# elem}} lomtárba mozgatva", "assets_permanently_deleted_count": "{count, plural, other {# elem}} örökre törölve", "assets_removed_count": "{count, plural, other {# elem}} eltávolítva", - "assets_restore_confirmation": "Biztosan visszaállítja a lomtárbeli elemeket? Ez a művelet nem visszavonható!", + "assets_restore_confirmation": "Biztosan visszaállítja a lomtárban lévő elemeket? Ez a művelet nem visszavonható!", "assets_restored_count": "{count, plural, other {# elem}} visszaállítva", - "assets_trashed_count": "{count, plural, other {# elem}} kidobva", + "assets_trashed_count": "{count, plural, other {# elem}} lomtárba helyezve", "assets_were_part_of_album_count": "{count, plural, other {# elem}} már az album része volt", "authorized_devices": "Engedélyezett készülékek", "back": "Vissza", @@ -448,6 +460,7 @@ "close": "Bezárás", "collapse": "Összecsuk", "collapse_all": "Mindet összecsuk", + "color": "Szín", "color_theme": "Szín stílus", "comment_deleted": "Megjegyzés törölve", "comment_options": "Megjegyzés beállítások", @@ -481,6 +494,8 @@ "create_new_person": "Új személy létrehozása", "create_new_person_hint": "A kiválasztott képekhez új személyt rendel hozzá", "create_new_user": "Új felhasználó létrehozása", + "create_tag": "Címke létrehozása", + "create_tag_description": "Új címke létrehozása. Beágyazott címkék esetén adja meg a címke teljes elérési útvonalát, beleértve a perjeleket is.", "create_user": "Felhasználó létrehozása", "created": "Készült", "current_device": "Ez az eszköz", @@ -504,6 +519,8 @@ "delete_library": "Képtár törlése", "delete_link": "Link törlése", "delete_shared_link": "Megosztott link törlése", + "delete_tag": "Címke törlése", + "delete_tag_confirmation_prompt": "Biztos, hogy törölni szeretné a {tagName} címkét?", "delete_user": "Felhasználó törlése", "deleted_shared_link": "Törölt megosztott link", "description": "Leírás", @@ -552,6 +569,7 @@ "edit_location": "Hely módosítása", "edit_name": "Név módosítása", "edit_people": "Személyek módosítása", + "edit_tag": "Címke szerkesztése", "edit_title": "Cím Módosítása", "edit_user": "Felhasználó módosítása", "edited": "Módosítva", @@ -564,7 +582,7 @@ "empty": "", "empty_album": "Üres Album", "empty_trash": "Lomtár Ürítése", - "empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárbeli fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!", + "empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárban lévő fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!", "enable": "Engedélyezés", "enabled": "Engedélyezve", "end_date": "Vég dátum", @@ -582,7 +600,7 @@ "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen", "cant_search_people": "Emberek keresése sikertelen", "cant_search_places": "Helyek keresése sikertelen", - "cleared_jobs": "A {job} munkák törölve", + "cleared_jobs": "A {job} feladatok törölve", "error_adding_assets_to_album": "Hiba történt az elemek albumhoz való hozzáadása során", "error_adding_users_to_album": "Hiba történt a felhasználók albumhoz való hozzáadása során", "error_deleting_shared_user": "Hiba történt megosztott felhasználó törlése során", @@ -591,7 +609,7 @@ "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat", "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel", "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.", - "failed_job_command": "Parancs {command} hibával zárult a {job} munkában", + "failed_job_command": "A(z) {command} parancs hibával zárult a(z) {job} feladatban", "failed_to_create_album": "Album készítése sikertelen", "failed_to_create_shared_link": "Megosztott link készítése sikertelen", "failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen", @@ -649,6 +667,7 @@ "unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen", "unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen", "unable_to_hide_person": "Személy elrejtése sikertelen", + "unable_to_link_motion_video": "Nem lehet a motion videót hozzákapcsolni", "unable_to_link_oauth_account": "OAuth felhasználó csatlakoztatása sikertelen", "unable_to_load_album": "Album betöltése sikertelen", "unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen", @@ -665,8 +684,8 @@ "unable_to_remove_api_key": "API kulcs eltávolítása sikertelen", "unable_to_remove_assets_from_shared_link": "Elemek eltávolítása megosztott linkből sikertelen", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Offline fájlok törlése sikertelen", "unable_to_remove_library": "Könyvtár törlése sikertelen", - "unable_to_remove_offline_files": "Offline fájlok törlése sikertelen", "unable_to_remove_partner": "Partner eltávolítása sikertelen", "unable_to_remove_reaction": "Reakció eltávolítása sikertelen", "unable_to_remove_user": "", @@ -686,9 +705,10 @@ "unable_to_scan_library": "Könyvtár ellenőrzése sikertelen", "unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen", "unable_to_set_profile_picture": "Profilkép beállítása sikertelen", - "unable_to_submit_job": "Nem sikerült a profilt elmenteni", + "unable_to_submit_job": "Nem sikerült a feladatot elindítani", "unable_to_trash_asset": "Nem sikerült a fájl lomtárba mozgatása", "unable_to_unlink_account": "Nem sikerült a fiók lekapcsolása", + "unable_to_unlink_motion_video": "Nem lehet a motion videót leválasztani", "unable_to_update_album_cover": "Albumborító beállítása sikertelen", "unable_to_update_album_info": "Album információ frissítése sikertelen", "unable_to_update_library": "Nem sikerült a képtár módosítása", @@ -708,7 +728,8 @@ "expire_after": "Lejárati idő", "expired": "Lejárt", "expires_date": "Lejár {date}", - "explore": "Felfedezés", + "explore": "Böngészés", + "explorer": "Böngésző", "export": "Exportálás", "export_as_json": "Exportálás JSON formátumban", "extension": "Kiterjesztés", @@ -722,6 +743,8 @@ "feature": "", "feature_photo_updated": "Címlapkép frissítve", "featurecollection": "", + "features": "Jellemzők", + "features_setting_description": "Az alkalmazás lehetőségeinek kezelése", "file_name": "Fájlnév", "file_name_or_extension": "Fájlnév vagy kiterjesztés", "filename": "Fájlnév", @@ -730,6 +753,8 @@ "filter_people": "Személyek szűrése", "find_them_fast": "Kereséssel gyorsan megtalálhatóak név alapján", "fix_incorrect_match": "Hibás találat korrigálása", + "folders": "Mappák", + "folders_feature_description": "A fájlrendszerben lévő fényképek és videók mappanézetben való böngészése", "force_re-scan_library_files": "Az összes Képtár fájl újbóli átfésülésének indítása", "forward": "Előre", "general": "Általános", @@ -750,7 +775,7 @@ "hide_password": "Jelszó elrejtése", "hide_person": "Személy elrejtése", "hide_unnamed_people": "Megnevezetlen emberek elrejtése", - "host": "", + "host": "Kiszolgáló", "hour": "Óra", "image": "Kép", "image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma {date}", @@ -801,6 +826,7 @@ "library_options": "Képtár beállítások", "light": "Világos", "like_deleted": "Tetszik törölve", + "link_motion_video": "Motion videó hozzárendelése", "link_options": "Link beállítások", "link_to_oauth": "Csatlakoztatás OAuth-hoz", "linked_oauth_account": "Csatlakoztatott OAuth felhasználó", @@ -872,8 +898,8 @@ "no_assets_message": "KATTINTSON AZ ELSŐ FÉNYKÉPE FELTÖLTÉSÉHEZ", "no_duplicates_found": "Duplikátumok nem találhatók.", "no_exif_info_available": "Exif információ nem elérhető", - "no_explore_results_message": "Töltsön fel több fényképet, hogy felfedezze a gyűjteményét.", - "no_favorites_message": "Jelöljön meg kedvenceket, hogy gyorsan megtalálhassa legjobb fényképeit és videóit", + "no_explore_results_message": "Töltsön fel több fényképet, hogy böngészhesse a gyűjteményét.", + "no_favorites_message": "Hozzáadás a kedvencekhez, hogy hamarabb megtalálhassa a legjobb fényképeit és videóit", "no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez", "no_name": "Nincs Név", "no_places": "Nincsenek helyek", @@ -936,6 +962,7 @@ "pending": "Folyamatban lévő", "people": "Személyek", "people_edits_count": "{count, plural, other {# személy}} szerkesztve", + "people_feature_description": "Személyek szerint csoportosított fényképek és videók böngészése", "people_sidebar_description": "Jelenítsen meg linket a Személyek fülhöz oldalt", "perform_library_tasks": "", "permanent_deletion_warning": "Figyelmeztetés végleges törlésről", @@ -1006,7 +1033,9 @@ "purchase_settings_server_activated": "A szerver termékkulcsot az admin menedzseli", "range": "", "rating": "Értékelés csillagokkal", - "rating_description": "Exif értékelés megjelenítése az infópanelben", + "rating_clear": "Értékelés törlése", + "rating_count": "{count, plural, one {# csillag} other {# csillagok}}", + "rating_description": "Exif értékelés megjelenítése az infópanelen", "raw": "", "reaction_options": "Reakció lehetőségek", "read_changelog": "Változtatások olvasása", @@ -1030,15 +1059,16 @@ "remove_assets_shared_link_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?", "remove_assets_title": "Elemek eltávolítása?", "remove_custom_date_range": "Szabadon megadott időintervallum eltávolítása", + "remove_deleted_assets": "Offline Fájlok Eltávolítása", "remove_from_album": "Eltávolítás az albumból", "remove_from_favorites": "Eltávolítás a kedvencekből", "remove_from_shared_link": "Eltávolítás a megosztott linkből", - "remove_offline_files": "Offline Fájlok Eltávolítása", "remove_user": "Felhasználó eltávolítása", "removed_api_key": "API Kulcs eltávolítva: {name}", "removed_from_archive": "Archívumból eltávolítva", "removed_from_favorites": "Kedvencekből eltávolítva", "removed_from_favorites_count": "A kedvencekből el lett távolítva {count, plural, other {# elem}}", + "removed_tagged_assets": "Címke eltávolítva az {count, plural, one {# elemről} other {# elemekről}}", "rename": "Átnevezés", "repair": "Javítás", "repair_no_results_message": "Nem megfigyelt és hiányzó fájlok itt jelennek meg", @@ -1071,7 +1101,8 @@ "scan_all_libraries": "Minden könyvtár átnézése", "scan_all_library_files": "Minden könyvtárbeli elem újraellenőrzése", "scan_new_library_files": "Ellenőrzés új könyvtárbeli elemekért", - "scan_settings": "Felfedezési beállítások", + "scan_settings": "Szkennelési beállítások", + "scanning_for_album": "Album szkennelése...", "search": "Keresés", "search_albums": "Albumok keresése", "search_by_context": "Keresés kontextus alapján", @@ -1084,13 +1115,16 @@ "search_for_existing_person": "Már meglévő személy keresése", "search_no_people": "Nincs személy", "search_no_people_named": "Nincs személy \"{name}\" néven", + "search_options": "Keresési lehetőségek", "search_people": "Személyek keresése", "search_places": "Helyek keresése", + "search_settings": "Keresési beállítások", "search_state": "Régió keresése...", + "search_tags": "Címkék keresése...", "search_timezone": "Időzóna keresése...", "search_type": "Típus keresése", "search_your_photos": "Fotók keresése", - "searching_locales": "", + "searching_locales": "Helyszín keresése...", "second": "Másodperc", "see_all_people": "Minden személy megtekintése", "select_album_cover": "Albumborító kiválasztása", @@ -1104,7 +1138,7 @@ "select_library_owner": "Könyvtártulajdonos kijelölése", "select_new_face": "Új arc kiválasztása", "select_photos": "Fotók választása", - "select_trash_all": "Minden szemétbe helyezése", + "select_trash_all": "Minden lomtárba helyezése", "selected": "Kijelölt", "selected_count": "{count, plural, other {# kiválasztva}}", "send_message": "Üzenet küldése", @@ -1124,7 +1158,7 @@ "settings_saved": "Beállítások mentve", "share": "Megosztás", "shared": "Megosztva", - "shared_by": "Megosztva általa:", + "shared_by": "Megosztotta", "shared_by_user": "Megosztva {user} által", "shared_by_you": "Megosztva Ön által", "shared_from_partner": "Fényképek {partner}-tól/től", @@ -1155,10 +1189,14 @@ "show_supporter_badge": "Támogató jelvény", "show_supporter_badge_description": "Támogató jelvény megjelenítése", "shuffle": "Keverés", + "sidebar": "Oldalsáv", + "sidebar_display_description": "Nézetre mutató link megjelenítése az oldalsávban", "sign_out": "Kilépés", "sign_up": "Feliratkozás", "size": "Méret", "skip_to_content": "Ugrás a tartalomhoz", + "skip_to_folders": "Ugrás a mappákra", + "skip_to_tags": "Ugrás a címkékhez", "slideshow": "Diavetítés", "slideshow_settings": "Diavetítés beállításai", "sort_albums_by": "Albumok rendezése...", @@ -1191,6 +1229,14 @@ "sunrise_on_the_beach": "Napkelte a tengerparton", "swap_merge_direction": "Egyesítés irányának megfordítása", "sync": "Szinkronizálás", + "tag": "Címke", + "tag_assets": "Elemek címkézése", + "tag_created": "Létrehozott címke: {tag}", + "tag_feature_description": "Címkék szerinti fényképek és videók böngészése", + "tag_not_found_question": "Nem találja a címkét? Hozzon létre egyet itt", + "tag_updated": "Frissített címke: {tag}", + "tagged_assets": "Címkézett {count, plural, one {# elem} other {# elemek}}", + "tags": "Címkék", "template": "Minta", "theme": "Téma", "theme_selection": "Témaválasztás", @@ -1202,17 +1248,18 @@ "to_change_password": "Jelszó megváltoztatása", "to_favorite": "Kedvenc", "to_login": "Bejelentkezés", - "to_trash": "Szemétbe helyezés", + "to_parent": "Egy szinttel feljebb", + "to_trash": "Lomtárba helyezés", "toggle_settings": "Beállítások változtatása", - "toggle_theme": "Témaváltás", + "toggle_theme": "Sötét téma váltása", "toggle_visibility": "Láthatóság változtatása", "total_usage": "Összesen használatban", "trash": "Lomtár", "trash_all": "Mindet lomtárba", - "trash_count": "{count, number} elem szemétbe helyezése", - "trash_delete_asset": "Elem szemétbe helyezése / törlése", - "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videok.", - "trashed_items_will_be_permanently_deleted_after": "A szemeteskosárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.", + "trash_count": "{count, number} elem lomtárba helyezése", + "trash_delete_asset": "Lomtárba helyezés/törlés", + "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videók.", + "trashed_items_will_be_permanently_deleted_after": "A lomtárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.", "type": "Típus", "unarchive": "Archívumból kivétel", "unarchived": "Archívumból kivett", @@ -1223,9 +1270,11 @@ "unknown_album": "Ismeretlen Album", "unknown_year": "Ismeretlen év", "unlimited": "Korlátlan", + "unlink_motion_video": "Mozgókép leválasztása", "unlink_oauth": "OAuth leválasztása", "unlinked_oauth_account": "Leválasztott OAuth felhasználó", "unnamed_album": "Névtelen Album", + "unnamed_album_delete_confirmation": "Biztosan törölni szeretné ezt az albumot?", "unnamed_share": "Névtelen Megosztás", "unsaved_change": "Mentés nélküli változtatás", "unselect_all": "Összes kiválasztás törlése", @@ -1237,7 +1286,7 @@ "up_next": "Következik", "updated_password": "Jelszó megváltoztatva", "upload": "Feltöltés", - "upload_concurrency": "", + "upload_concurrency": "Párhuzamos feltöltés", "upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", "upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}", "upload_skipped_duplicates": "{count, plural, other {# megegyező elem}} kihagyva", @@ -1246,7 +1295,7 @@ "upload_status_uploaded": "Feltöltve", "upload_success": "Feltöltés sikeres, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", "url": "URL", - "usage": "Felhasználás", + "usage": "Használat", "use_custom_date_range": "Szabadon megadott időintervallum használata", "user": "Felhasználó", "user_id": "Felhasználó azonosítója", @@ -1261,6 +1310,8 @@ "validate": "Ellenőrzés", "variables": "Változók", "version": "Verzió", + "version_announcement_closing": "Barátod, Alex", + "version_announcement_message": "Szia barátom, van egy új verziója az alkalmazásnak. Kérjük, szánj időt a verzióinformáció megtekintésére, és győződj meg róla, hogy a docker-compose.yml és a .env beállítások naprakészek, hogy elkerüld a hibás konfigurációt, különösen, ha WatchTower-t vagy valami más automatikus frissítési megoldást használsz.", "video": "Videó", "video_hover_setting": "Bélyegkép felett lebegésnél videó indítás", "video_hover_setting_description": "Ha az egér a bélyegkép felett időzik, a bélyegkép videó lejátszása induljon el. A lejátszás az indítás ikon feletti időzéssel akkor is elindul, ha ez az opció ki van kapcsolva.", @@ -1270,6 +1321,7 @@ "view_album": "Album megtekintése", "view_all": "Összes mutatása", "view_all_users": "Minden felhasználó megtekintése", + "view_in_timeline": "Megtekintés az idővonalon", "view_links": "Linkek megtekintése", "view_next_asset": "Következő elem megtekintése", "view_previous_asset": "Előző elem megtekintése", diff --git a/web/src/lib/i18n/hy.json b/web/src/lib/i18n/hy.json index f5273c4ac9b5a..57651d0063518 100644 --- a/web/src/lib/i18n/hy.json +++ b/web/src/lib/i18n/hy.json @@ -172,7 +172,7 @@ "paths_validated_successfully": "", "quota_size_gib": "", "refreshing_all_libraries": "", - "removing_offline_files": "", + "removing_deleted_files": "", "repair_all": "", "repair_matched_items": "", "repaired_items": "", @@ -486,8 +486,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "", @@ -720,10 +720,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "rename": "", "repair": "", diff --git a/web/src/lib/i18n/id.json b/web/src/lib/i18n/id.json index 368d12855039c..02d5a1923e781 100644 --- a/web/src/lib/i18n/id.json +++ b/web/src/lib/i18n/id.json @@ -41,6 +41,7 @@ "confirm_email_below": "Untuk mengonfirmasi, ketik \"{email}\" di bawah", "confirm_reprocess_all_faces": "Apakah Anda yakin ingin memproses semua wajah? Ini juga akan menghapus nama orang.", "confirm_user_password_reset": "Apakah Anda yakin ingin mengatur ulang kata sandi {user}?", + "create_job": "Buat tugas", "disable_login": "Nonaktifkan log masuk", "duplicate_detection_job_description": "Jalankan pembelajaran mesin pada aset untuk mendeteksi gambar yang serupa. Bergantung pada Pencarian Pintar", "exclusion_pattern_description": "Pola pengecualian memungkinkan Anda mengabaikan berkas dan folder ketika memindai pustaka Anda. Ini berguna jika Anda memiliki folder yang berisi berkas yang tidak ingin diimpor, seperti berkas RAW.", @@ -68,6 +69,7 @@ "image_thumbnail_resolution": "Resolusi gambar kecil", "image_thumbnail_resolution_description": "Digunakan ketika menampilkan kelompok foto (lini masa utama, tampilan album, dll.). Resolusi yang lebih tinggi dapat menjaga lebih banyak detail tetapi memerlukan waktu lama untuk mengode, memiliki ukuran berkas yang lebih besar, dan dapat mengurangi respons aplikasi.", "job_concurrency": "Konkurensi {job}", + "job_created": "Tugas telah dibuat", "job_not_concurrency_safe": "Tugas ini tidak aman untuk konkurensi.", "job_settings": "Pengaturan Tugas", "job_settings_description": "Kelola konkurensi tugas", @@ -137,7 +139,11 @@ "map_settings_description": "Kelola pengaturan peta", "map_style_description": "URL ke tema peta style.json", "metadata_extraction_job": "Ekstrak metadata", - "metadata_extraction_job_description": "Ekstrak informasi metadata dari setiap aset, seperti GPS dan resolusi", + "metadata_extraction_job_description": "Ekstrak informasi metadata dari setiap aset, seperti GPS, wajah dan resolusi", + "metadata_faces_import_setting": "Aktifkan impor wajah", + "metadata_faces_import_setting_description": "Impor wajah dari data gambar EXIF dan berkas sidecar", + "metadata_settings": "Pengaturan Metadata", + "metadata_settings_description": "Kelola pengaturan metadata", "migration_job": "Migrasi", "migration_job_description": "Migrasikan gambar kecil untuk aset dan wajah ke struktur folder terkini", "no_paths_added": "Tidak ada jalur yang ditambahkan", @@ -146,7 +152,7 @@ "note_cannot_be_changed_later": "CATATAN: Ini tidak akan dapat diubah lagi!", "note_unlimited_quota": "Catatan: Masukkan 0 untuk kuota tidak terbatas", "notification_email_from_address": "Dari alamat", - "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich \"", + "notification_email_from_address_description": "Alamat surel pengirim, misalnya: \"Server Foto Immich \"", "notification_email_host_description": "Hos server surel (mis. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Abaikan eror sertifikat", "notification_email_ignore_certificate_errors_description": "Abaikan eror validasi sertifikat TLS (tidak disarankan)", @@ -192,11 +198,12 @@ "password_settings": "Log Masuk Kata Sandi", "password_settings_description": "Kelola pengaturan log masuk kata sandi", "paths_validated_successfully": "Semua jalur berhasil divalidasi", + "person_cleanup_job": "Pembersihan data pribadi", "quota_size_gib": "Ukuran Kuota (GiB)", "refreshing_all_libraries": "Menyegarkan semua pustaka", "registration": "Pendaftaran Admin", "registration_description": "Karena Anda merupakan pengguna pertama dalam sistem, Anda akan ditetapkan sebagai Admin dan bertanggung jawab atas tugas administratif dan pengguna tambahan akan dibuat oleh Anda.", - "removing_offline_files": "Menghapus Berkas Luring", + "removing_deleted_files": "Menghapus Berkas Luring", "repair_all": "Perbaiki Semua", "repair_matched_items": "{count, plural, one {# item} other {# item}} dicocokkan", "repaired_items": "{count, plural, one {# item} other {# item}} diperbaiki", @@ -205,6 +212,7 @@ "reset_settings_to_recent_saved": "Atur ulang pengaturan ke pengaturan tersimpan terkini", "scanning_library_for_changed_files": "Memindai pustaka untuk berkas yang telah diubah", "scanning_library_for_new_files": "Memindai pustaka untuk berkas baru", + "search_jobs": "Mencari tugas...", "send_welcome_email": "Kirim surel selamat datang", "server_external_domain_settings": "Domain eksternal", "server_external_domain_settings_description": "Domain untuk tautan terbagi publik, termasuk http(s)://", @@ -232,6 +240,7 @@ "storage_template_settings_description": "Kelola struktur folder dan nama berkas dari aset yang diunggah", "storage_template_user_label": "{label} adalah Label Penyimpanan pengguna", "system_settings": "Pengaturan Sistem", + "tag_cleanup_job": "Pembersihan tag", "theme_custom_css_settings": "CSS Kustom", "theme_custom_css_settings_description": "CSS memungkinkan desain Immich untuk diubah.", "theme_settings": "Pengaturan Tema", @@ -305,6 +314,7 @@ "trash_settings_description": "Kelola pengaturan sampah", "untracked_files": "Berkas yang Belum Dilacak", "untracked_files_description": "Berkas ini tidak dilacak oleh aplikasi. Mereka dapat diakibatkan oleh pemindahan gagal, pengunggahan terganggu, atau tertinggal karena oleh kutu", + "user_cleanup_job": "Pembersihan data pengguna", "user_delete_delay": "Akun dan aset {user} akan dijadwalkan untuk penghapusan permanen dalam {delay, plural, one {# hari} other {# hari}}.", "user_delete_delay_settings": "Jeda penghapusan", "user_delete_delay_settings_description": "Jumlah hari setelah penghapusan untuk menghapus akun dan aset pengguna secara permanen. Tugas penghapusan pengguna berjalan pada tengah malam untuk memeriksa pengguna yang siap untuk dihapus. Perubahan pengaturan ini akan dievaluasi pada eksekusi berikutnya.", @@ -384,6 +394,7 @@ "asset_offline": "Aset luring", "asset_offline_description": "Aset ini sedang luring. Immich tidak dapat mengakses lokasi berkasnya. Pastikan aset tersebut tersedia lalu pindai ulang pustaka.", "asset_skipped": "Dilewati", + "asset_skipped_in_trash": "Dalam sampah", "asset_uploaded": "Sudah diunggah", "asset_uploading": "Mengunggah...", "assets": "Aset", @@ -642,6 +653,7 @@ "unable_to_get_comments_number": "Tidak bisa mendapatkan jumlah komentar", "unable_to_get_shared_link": "Gagal mendapatkan tautan berbagi", "unable_to_hide_person": "Tidak dapat menyembunyikan orang", + "unable_to_link_motion_video": "Tidak dapat menautkan video gerak", "unable_to_link_oauth_account": "Tidak dapat menautkan akun OAuth", "unable_to_load_album": "Tidak dapat memuat album", "unable_to_load_asset_activity": "Tidak dapat memuat aktivitas aset", @@ -657,8 +669,8 @@ "unable_to_remove_album_users": "Tidak dapat mengeluarkan pengguna dari album", "unable_to_remove_api_key": "Tidak dapat menghapus Kunci API", "unable_to_remove_assets_from_shared_link": "Tidak dapat menghapus aset dari tautan terbagi", + "unable_to_remove_deleted_assets": "Tidak dapat menghapus berkas luring", "unable_to_remove_library": "Tidak dapat menghapus pustaka", - "unable_to_remove_offline_files": "Tidak dapat menghapus berkas luring", "unable_to_remove_partner": "Tidak dapat menghapus partner", "unable_to_remove_reaction": "Tidak dapat menghapus reaksi", "unable_to_repair_items": "Tidak dapat memperbaiki item", @@ -680,6 +692,7 @@ "unable_to_submit_job": "Tidak dapat mengirim tugas", "unable_to_trash_asset": "Tidak dapat membuang aset", "unable_to_unlink_account": "Tidak dapat memutuskan akun", + "unable_to_unlink_motion_video": "Tidak dapat membatalkan tautan video gerak", "unable_to_update_album_cover": "Tidak dapat memperbarui kover album", "unable_to_update_album_info": "Tidak dapat memperbarui info album", "unable_to_update_library": "Tidak dapat memperbarui pustaka", @@ -816,6 +829,7 @@ "license_trial_info_4": "Pertimbangkan membeli lisensi untuk mendukung keberlanjutan pengembangan layanan", "light": "Terang", "like_deleted": "Suka dihapus", + "link_motion_video": "Tautan video gerak", "link_options": "Opsi tautan", "link_to_oauth": "Tautkan ke OAuth", "linked_oauth_account": "Akun OAuth tertaut", @@ -1045,10 +1059,10 @@ "remove_assets_shared_link_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset} other {# aset}} dari tautan terbagi ini?", "remove_assets_title": "Hapus aset?", "remove_custom_date_range": "Hapus jangka tanggal khusus", + "remove_deleted_assets": "Hapus Berkas Luring", "remove_from_album": "Hapus dari album", "remove_from_favorites": "Hapus dari favorit", "remove_from_shared_link": "Hapus dari tautan terbagi", - "remove_offline_files": "Hapus Berkas Luring", "remove_user": "Keluarkan pengguna", "removed_api_key": "Kunci API Dihapus: {name}", "removed_from_archive": "Dihapus dari arsip", @@ -1100,8 +1114,10 @@ "search_for_existing_person": "Cari orang yang sudah ada", "search_no_people": "Tidak ada orang", "search_no_people_named": "Tidak ada orang bernama \"{name}\"", + "search_options": "Pilihan pencarian", "search_people": "Cari orang", "search_places": "Cari tempat", + "search_settings": "Pengaturan pencarian", "search_state": "Cari negara bagian...", "search_tags": "Cari tag...", "search_timezone": "Cari zona waktu...", @@ -1178,6 +1194,8 @@ "sign_up": "Daftar", "size": "Ukuran", "skip_to_content": "Lewati ke konten", + "skip_to_folders": "Lewati ke berkas", + "skip_to_tags": "Lewati ke tag", "slideshow": "Salindia", "slideshow_settings": "Pengaturan salindia", "sort_albums_by": "Urutkan album berdasarkan...", @@ -1229,6 +1247,7 @@ "to_change_password": "Ubah kata sandi", "to_favorite": "Favorit", "to_login": "Log masuk", + "to_parent": "Ke induk", "to_root": "Untuk melakukan root", "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", @@ -1250,6 +1269,7 @@ "unknown": "Tidak diketahui", "unknown_year": "Tahun Tidak Diketahui", "unlimited": "Tidak terbatas", + "unlink_motion_video": "Membatalkan tautan video gerak", "unlink_oauth": "Putuskan OAuth", "unlinked_oauth_account": "Akun OAuth terputus", "unnamed_album": "Album Tanpa Nama", diff --git a/web/src/lib/i18n/it.json b/web/src/lib/i18n/it.json index 773f3bc67ca9f..fc1be8a07c206 100644 --- a/web/src/lib/i18n/it.json +++ b/web/src/lib/i18n/it.json @@ -139,7 +139,11 @@ "map_settings_description": "Gestisci impostazioni mappa", "map_style_description": "URL per un tema della mappa style.json", "metadata_extraction_job": "Estrazione Metadata", - "metadata_extraction_job_description": "Estrai informazioni dai metadati di ciascun asset, ad esempio coordinate GPS e risoluzione", + "metadata_extraction_job_description": "Estrai informazioni dai metadati di ciascun asset, ad esempio coordinate GPS, volti e risoluzione", + "metadata_faces_import_setting": "Abilita l'importazione dei volti", + "metadata_faces_import_setting_description": "Importa i volti dai dati EXIF dell'immagine e dai file sidecar", + "metadata_settings": "Impostazioni Metadati", + "metadata_settings_description": "Gestisci le impostazioni dei metadati", "migration_job": "Migrazione", "migration_job_description": "Migra le anteprime per gli asset e volti alla struttura di cartelle più recente", "no_paths_added": "Nessun percorso aggiunto", @@ -148,7 +152,7 @@ "note_cannot_be_changed_later": "NOTA: Non potrà essere modificato in futuro!", "note_unlimited_quota": "Nota: Inserisci 0 per una quota illimitata", "notification_email_from_address": "Indirizzo mittente", - "notification_email_from_address_description": "Indirizzo email mittente, ad esempio: \"Server Foto Immich \"", + "notification_email_from_address_description": "Indirizzo email mittente, ad esempio: \"Server Foto Immich \"", "notification_email_host_description": "Host del server email (es. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignora errori di certificato", "notification_email_ignore_certificate_errors_description": "Ignora errori di validazione del certificato TLS (sconsigliato)", @@ -198,7 +202,7 @@ "refreshing_all_libraries": "Aggiorna tutte le librerie", "registration": "Registrazione amministratore", "registration_description": "Poiché sei il primo utente del sistema, sarai assegnato come Amministratore e sarai responsabile dei task amministrativi, e utenti aggiuntivi saranno creati da te.", - "removing_offline_files": "Cancella File Offline", + "removing_deleted_files": "Cancella File Offline", "repair_all": "Ripara Tutto", "repair_matched_items": "{count, plural, one {Rilevato # elemento} other {Rilevati # elementi}}", "repaired_items": "{count, plural, one {Riparato # elemento} other {Riparati # elementi}}", @@ -387,6 +391,7 @@ "asset_offline": "Risorsa offline", "asset_offline_description": "Il media è offline. Immich non è in grado di accedere al percorso del file. Assicurarsi che il media sia disponibile e riscansionare la libreria.", "asset_skipped": "Saltato", + "asset_skipped_in_trash": "In cestino", "asset_uploaded": "Caricato", "asset_uploading": "Caricamento...", "assets": "Risorse", @@ -656,6 +661,7 @@ "unable_to_get_comments_number": "Impossibile ottenere il numero di commenti", "unable_to_get_shared_link": "Impossibile ottenere il link condiviso", "unable_to_hide_person": "Impossibile nascondere persona", + "unable_to_link_motion_video": "Impossibile collegare video in movimento", "unable_to_link_oauth_account": "Impossibile collegare l'account OAuth", "unable_to_load_album": "Impossibile caricare l'album", "unable_to_load_asset_activity": "Impossibile caricare l'attività dell'asset", @@ -672,8 +678,8 @@ "unable_to_remove_api_key": "Impossibile rimuovere la chiave API", "unable_to_remove_assets_from_shared_link": "Errore durante la rimozione degli assets da un link condiviso", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Impossibile rimuovere i file offline", "unable_to_remove_library": "Impossibile rimuovere libreria", - "unable_to_remove_offline_files": "Impossibile rimuovere i file offline", "unable_to_remove_partner": "Impossibile rimuovere compagno", "unable_to_remove_reaction": "Impossibile rimuovere reazione", "unable_to_remove_user": "", @@ -696,6 +702,7 @@ "unable_to_submit_job": "Impossibile eseguire l'attività", "unable_to_trash_asset": "Impossibile cestinare l'asset", "unable_to_unlink_account": "Impossibile scollegare l'account", + "unable_to_unlink_motion_video": "Impossibile scollegare video in movimento", "unable_to_update_album_cover": "Errore durante l'aggiornamento della copertina dell'album", "unable_to_update_album_info": "Impossibile aggiornare le informazioni sull'album", "unable_to_update_library": "Impossibile aggiornare la libreria", @@ -841,6 +848,7 @@ "license_trial_info_4": "Per favore considera sborsare soldi per una licenza e per sopportare il continuo sviluppo del servizio", "light": "Chiaro", "like_deleted": "Mi piace rimosso", + "link_motion_video": "Collega video in movimento", "link_options": "Impostazioni Collegamento", "link_to_oauth": "Collegamento a OAuth", "linked_oauth_account": "Account OAuth collegato", @@ -1048,6 +1056,7 @@ "range": "", "rating": "Valutazione a stelle", "rating_clear": "Crea valutazione", + "rating_count": "{count, plural, one {# stella} other {# stelle}}", "rating_description": "Visualizza la valutazione EXIF nel pannello informazioni", "raw": "", "reaction_options": "Impostazioni Reazioni", @@ -1072,15 +1081,16 @@ "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?", "remove_assets_title": "Rimuovere asset?", "remove_custom_date_range": "Rimuovi intervallo data personalizzato", + "remove_deleted_assets": "Rimuovi file offline", "remove_from_album": "Rimuovere dall'album", "remove_from_favorites": "Rimuovi dai preferiti", "remove_from_shared_link": "Rimuovi dal link condiviso", - "remove_offline_files": "Rimuovi file offline", "remove_user": "Rimuovi utente", "removed_api_key": "Rimossa chiave API: {name}", "removed_from_archive": "Rimosso dall'archivio", "removed_from_favorites": "Rimosso dai preferiti", "removed_from_favorites_count": "{count, plural, one {Rimosso } other {Rimossi #}} dai preferiti", + "removed_tagged_assets": "Rimossa etichetta {count, plural, one {# dall'asset} other {# dagli asset}}", "rename": "Rinomina", "repair": "Ripara", "repair_no_results_message": "I file mancanti e non tracciati saranno mostrati qui", @@ -1127,6 +1137,7 @@ "search_for_existing_person": "Cerca per persona esistente", "search_no_people": "Nessuna persona", "search_no_people_named": "Nessuna persona chiamate \"{name}\"", + "search_options": "Opzioni Ricerca", "search_people": "Cerca persone", "search_places": "Cerca luoghi", "search_state": "Cerca stato...", @@ -1205,6 +1216,8 @@ "sign_up": "Registrati", "size": "Dimensione", "skip_to_content": "Salta al contenuto", + "skip_to_folders": "Salta alle cartelle", + "skip_to_tags": "Salta alle etichette", "slideshow": "Presentazione", "slideshow_settings": "Impostazioni presentazione", "sort_albums_by": "Ordina album per...", @@ -1243,6 +1256,7 @@ "tag_feature_description": "Navigazione foto e video raggruppati per argomenti tag logici", "tag_not_found_question": "Non riesci a trovare una tag? Creane una qui", "tag_updated": "Tag {tag} aggiornata", + "tagged_assets": "{count, plural, one {# asset etichettato} other {# asset etichettati}}", "tags": "Tag", "template": "Modello", "theme": "Tema", @@ -1255,6 +1269,7 @@ "to_change_password": "Modifica password", "to_favorite": "Preferito", "to_login": "Login", + "to_parent": "Sali di un livello", "to_root": "Alla radice", "to_trash": "Cancella", "toggle_settings": "Attiva/disattiva impostazioni", @@ -1277,6 +1292,7 @@ "unknown_album": "Album sconosciuto", "unknown_year": "Anno sconosciuto", "unlimited": "Illimitato", + "unlink_motion_video": "Scollega video in movimento", "unlink_oauth": "Scollega OAuth", "unlinked_oauth_account": "Scollega account OAuth", "unnamed_album": "Album senza nome", diff --git a/web/src/lib/i18n/ja.json b/web/src/lib/i18n/ja.json index 017d52fb30857..c220828e54f80 100644 --- a/web/src/lib/i18n/ja.json +++ b/web/src/lib/i18n/ja.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "注意: 後から変更できません!", "note_unlimited_quota": "注意: 無制限にする場合は0を入力してください", "notification_email_from_address": "送信メールアドレス", - "notification_email_from_address_description": "送信メールアドレスを設定します(例: \"Immich Photo Server \" )", + "notification_email_from_address_description": "送信メールアドレスを設定します(例: \"Immich Photo Server \" )", "notification_email_host_description": "送信メールサーバーを設定します(例:smtp.immich.app)", "notification_email_ignore_certificate_errors": "証明書エラーを無視", "notification_email_ignore_certificate_errors_description": "TLS証明書の検証エラーを無視します(非推奨)", @@ -198,7 +198,7 @@ "refreshing_all_libraries": "すべてのライブラリを更新", "registration": "管理者登録", "registration_description": "あなたはシステムの最初のユーザーであるため、管理者として割り当てられ、管理タスクを担当し、追加のユーザーはあなたによって作成されます。", - "removing_offline_files": "オフライン ファイルを削除します", + "removing_deleted_files": "オフライン ファイルを削除します", "repair_all": "すべてを修復", "repair_matched_items": "一致: {count, plural, one {#件} other {#件}}", "repaired_items": "修復済み: {count, plural, one {#件} other {#件}}", @@ -671,8 +671,8 @@ "unable_to_remove_api_key": "API キーを削除できません", "unable_to_remove_assets_from_shared_link": "共有リンクからアセットを削除できません", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "オフラインのファイルを削除できません", "unable_to_remove_library": "ライブラリを削除できません", - "unable_to_remove_offline_files": "オフラインのファイルを削除できません", "unable_to_remove_partner": "パートナーを削除できません", "unable_to_remove_reaction": "リアクションを削除できません", "unable_to_remove_user": "", @@ -1046,10 +1046,10 @@ "remove_assets_shared_link_confirmation": "本当にこの共有リンクから{count, plural, one {#個} other {#個}}のアセットを削除しますか?", "remove_assets_title": "アセットを削除しますか?", "remove_custom_date_range": "カスタム日付範囲を削除", + "remove_deleted_assets": "オフラインのファイルを削除", "remove_from_album": "アルバムから削除", "remove_from_favorites": "お気に入りから削除", "remove_from_shared_link": "共有リンクから削除", - "remove_offline_files": "オフラインのファイルを削除", "remove_user": "ユーザーを削除", "removed_api_key": "削除されたAPI キー: {name}", "removed_from_archive": "アーカイブから削除されました", diff --git a/web/src/lib/i18n/kmr.json b/web/src/lib/i18n/kmr.json index 9e0be0afbda60..9d8bd29808e88 100644 --- a/web/src/lib/i18n/kmr.json +++ b/web/src/lib/i18n/kmr.json @@ -177,7 +177,7 @@ "paths_validated_successfully": "", "quota_size_gib": "", "refreshing_all_libraries": "", - "removing_offline_files": "", + "removing_deleted_files": "", "repair_all": "", "repair_matched_items": "", "repaired_items": "", @@ -493,8 +493,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "", @@ -727,10 +727,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "rename": "", "repair": "", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index cd3db1310267c..39b79eea7222a 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -139,7 +139,11 @@ "map_settings_description": "지도 설정 관리", "map_style_description": "지도 테마 style.json URL", "metadata_extraction_job": "메타데이터 추출", - "metadata_extraction_job_description": "각 항목에서 GPS, 해상도 등의 메타데이터 정보 추출", + "metadata_extraction_job_description": "각 항목에서 GPS, 인물 및 해상도 등의 메타데이터 정보 추출", + "metadata_faces_import_setting": "얼굴 가져오기 활성화", + "metadata_faces_import_setting_description": "사이드카 파일의 이미지 EXIF 데이터에서 얼굴 가져오기", + "metadata_settings": "메타데이터 설정", + "metadata_settings_description": "메타데이터 설정 관리", "migration_job": "마이그레이션", "migration_job_description": "각 항목의 섬네일 및 인물의 얼굴을 최신 폴더 구조로 마이그레이션", "no_paths_added": "추가된 경로 없음", @@ -148,7 +152,7 @@ "note_cannot_be_changed_later": "주의: 추후 변경할 수 없습니다!", "note_unlimited_quota": "참고: 할당량을 설정하지 않으려면 0을 입력하세요.", "notification_email_from_address": "보낸 사람 이메일", - "notification_email_from_address_description": "보낸 사람의 이메일 주소, 예: \"Immich Photo Server \"", + "notification_email_from_address_description": "보낸 사람의 이메일 주소, 예: \"Immich Photo Server \"", "notification_email_host_description": "이메일 서버의 호스트 (예: smtp.immich.app)", "notification_email_ignore_certificate_errors": "인증서 오류 무시", "notification_email_ignore_certificate_errors_description": "TLS 인증서 유효성 검사 오류 무시 (권장되지 않음)", @@ -198,7 +202,7 @@ "refreshing_all_libraries": "모든 라이브러리 다시 스캔 중...", "registration": "관리자 가입", "registration_description": "첫 번째 사용자이기 때문에 관리자로 지정되었습니다. 관리 작업 및 사용자 생성이 가능합니다.", - "removing_offline_files": "누락된 파일을 제거하는 중...", + "removing_deleted_files": "누락된 파일을 제거하는 중...", "repair_all": "모두 수리", "repair_matched_items": "동일한 항목 {count, plural, one {#개} other {#개}}를 확인했습니다.", "repaired_items": "항목 {count, plural, one {#개} other {#개}}를 수리했습니다.", @@ -672,8 +676,8 @@ "unable_to_remove_api_key": "API 키를 삭제할 수 없습니다.", "unable_to_remove_assets_from_shared_link": "공유 링크에서 항목을 제거할 수 없습니다.", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "누락된 파일을 제거할 수 없습니다.", "unable_to_remove_library": "라이브러리를 제거할 수 없습니다.", - "unable_to_remove_offline_files": "누락된 파일을 제거할 수 없습니다.", "unable_to_remove_partner": "파트너를 제거할 수 없습니다.", "unable_to_remove_reaction": "반응을 제거할 수 없습니다.", "unable_to_remove_user": "", @@ -1058,10 +1062,10 @@ "remove_assets_shared_link_confirmation": "공유 링크에서 항목 {count, plural, one {#개} other {#개}}를 제거하시겠습니까?", "remove_assets_title": "항목을 제거하시겠습니까?", "remove_custom_date_range": "맞춤 기간 제거", + "remove_deleted_assets": "누락된 파일 제거", "remove_from_album": "앨범에서 제거", "remove_from_favorites": "즐겨찾기에서 제거", "remove_from_shared_link": "공유 링크에서 제거", - "remove_offline_files": "누락된 파일 제거", "remove_user": "사용자 삭제", "removed_api_key": "API 키 삭제: {name}", "removed_from_archive": "보관함에서 제거되었습니다.", @@ -1114,6 +1118,7 @@ "search_for_existing_person": "존재하는 인물 검색", "search_no_people": "인물이 없습니다.", "search_no_people_named": "\"{name}\" 인물을 찾을 수 없음", + "search_options": "검색 옵션", "search_people": "인물 검색", "search_places": "장소 검색", "search_state": "지역 검색...", @@ -1243,6 +1248,7 @@ "to_change_password": "비밀번호 변경", "to_favorite": "즐겨찾기", "to_login": "로그인", + "to_parent": "상위 항목으로", "to_root": "루트", "to_trash": "삭제", "toggle_settings": "설정 변경", diff --git a/web/src/lib/i18n/lt.json b/web/src/lib/i18n/lt.json index 18f4ee7c7f4bf..4d4db478c9226 100644 --- a/web/src/lib/i18n/lt.json +++ b/web/src/lib/i18n/lt.json @@ -835,10 +835,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "Pašalinti", + "remove_deleted_assets": "", "remove_from_album": "Pašalinti iš albumo", "remove_from_favorites": "Pašalinti iš mėgstamiausių", "remove_from_shared_link": "", - "remove_offline_files": "", "remove_user": "Pašalinti vartotoją", "removed_api_key": "Pašalintas API Raktas: {name}", "rename": "Pervadinti", diff --git a/web/src/lib/i18n/lv.json b/web/src/lib/i18n/lv.json index bf17ccb8135a4..da5e38ee98d22 100644 --- a/web/src/lib/i18n/lv.json +++ b/web/src/lib/i18n/lv.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Pievienot koplietotam albumam", "added_to_archive": "Pievienots arhīvam", "added_to_favorites": "Pievienots izlasei", - "added_to_favorites_count": "Pievienots {count} izlasei", + "added_to_favorites_count": "Pievienots {count, number} izlasei", "admin": { "add_exclusion_pattern_description": "Pievienojiet izlaišanas shēmas. Aizstājējzīmju izmantoša *, **, un ? tiek atbalstīta. Lai ignorētu visus failus jebkurā direktorijā ar nosaukumu “RAW”, izmantojiet “**/RAW/**”. Lai ignorētu visus failus, kas beidzas ar “. tif”, izmantojiet “**/*. tif”. Lai ignorētu absolūto ceļu, izmantojiet “/path/to/ignore/**”.", "authentication_settings": "Autentifikācijas iestatījumi", @@ -44,6 +44,9 @@ "disable_login": "Atspējot pieteikšanos", "disabled": "", "duplicate_detection_job_description": "Palaidiet mašīnmācīšanos uz līdzekļiem, lai noteiktu līdzīgus attēlus. Paļaujas uz Viedo Meklēšanu", + "external_library_created_at": "Ārēja bibliotēka (izveidota {date})", + "external_library_management": "Ārējo bibliotēku pārvaldība", + "face_detection": "Seju noteikšana", "image_format_description": "", "image_prefer_embedded_preview": "", "image_prefer_embedded_preview_setting_description": "", @@ -82,7 +85,7 @@ "machine_learning_enabled_description": "", "machine_learning_facial_recognition": "", "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", + "machine_learning_facial_recognition_model": "Seju atpazīšanas modelis", "machine_learning_facial_recognition_model_description": "", "machine_learning_facial_recognition_setting_description": "", "machine_learning_max_detection_distance": "", @@ -102,11 +105,13 @@ "manage_log_settings": "", "map_dark_style": "", "map_enable_description": "", + "map_gps_settings": "Kartes un GPS iestatījumi", + "map_gps_settings_description": "Pārvaldīt karšu un GPS (apgrieztās ģeokodēšanas) iestatījumus", "map_light_style": "", "map_reverse_geocoding": "", "map_reverse_geocoding_enable_description": "", "map_reverse_geocoding_settings": "", - "map_settings": "", + "map_settings": "Karte", "map_settings_description": "", "map_style_description": "", "metadata_extraction_job_description": "", @@ -124,7 +129,7 @@ "notification_email_test_email_sent": "", "notification_email_username_description": "", "notification_enable_email_notifications": "", - "notification_settings": "", + "notification_settings": "Paziņojumu iestatījumi", "notification_settings_description": "", "oauth_auto_launch": "", "oauth_auto_launch_description": "", @@ -151,10 +156,12 @@ "password_enable_description": "", "password_settings": "", "password_settings_description": "", + "quota_size_gib": "Kvotas izmērs (GiB)", + "require_password_change_on_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", "server_external_domain_settings": "", "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", + "server_settings": "Servera iestatījumi", + "server_settings_description": "Pārvaldīt servera iestatījumus", "server_welcome_message": "", "server_welcome_message_description": "", "sidecar_job_description": "", @@ -234,49 +241,60 @@ "trash_settings_description": "", "user_delete_delay_settings": "", "user_delete_delay_settings_description": "", + "user_management": "Lietotāju pārvaldība", "user_settings": "", "user_settings_description": "", - "version_check_enabled_description": "", + "version_check_enabled_description": "Ieslēgt versijas pārbaudi", + "version_check_implications": "Versiju pārbaudes funkcija ir atkarīga no periodiskas saziņas ar github.com", "version_check_settings": "", "version_check_settings_description": "", "video_conversion_job_description": "" }, - "admin_email": "", - "admin_password": "", - "administration": "", + "admin_email": "Administratora e-pasts", + "admin_password": "Administratora parole", + "administration": "Administrēšana", "advanced": "Papildu", - "album_added": "", + "album_added": "Albums pievienots", "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", + "album_cover_updated": "Albuma attēls atjaunināts", + "album_info_updated": "Albuma informācija atjaunināta", + "album_leave": "Pamest albumu?", + "album_name": "Albuma nosaukums", "album_options": "", - "album_updated": "", + "album_remove_user": "Noņemt lietotāju?", + "album_updated": "Albums atjaunināts", "album_updated_setting_description": "", - "albums": "", + "albums": "Albumi", "all": "Viss", - "all_people": "", + "all_albums": "Visi albumi", + "all_people": "Visi cilvēki", + "all_videos": "Visi video", "allow_dark_mode": "", "allow_edits": "", - "api_key": "", - "api_keys": "", + "api_key": "API atslēga", + "api_keys": "API atslēgas", "app_settings": "", "appears_in": "", "archive": "Arhīvs", "archive_or_unarchive_photo": "", + "archive_size": "Arhīva izmērs", "archived": "", + "are_these_the_same_person": "Vai šī ir tā pati persona?", "asset_offline": "", + "asset_uploading": "Augšupielādē...", "assets": "aktīvi", "authorized_devices": "", "back": "Atpakaļ", "backward": "", + "birthdate_saved": "Dzimšanas datums veiksmīgi saglabāts", + "birthdate_set_description": "Dzimšanas datums tiek izmantots, lai aprēķinātu šīs personas vecumu fotogrāfijas uzņemšanas brīdī.", "blurred_background": "", "camera": "", "camera_brand": "", "camera_model": "", "cancel": "Atcelt", "cancel_search": "", - "cannot_merge_people": "", + "cannot_merge_people": "Nevar apvienot cilvēkus", "cannot_update_the_description": "", "cant_apply_changes": "", "cant_get_faces": "", @@ -286,24 +304,25 @@ "change_expiration_time": "Izmainīt derīguma termiņu", "change_location": "", "change_name": "", - "change_name_successfully": "", - "change_password": "Nomainīt Paroli", + "change_name_successfully": "Vārds veiksmīgi nomainīts", + "change_password": "Nomainīt paroli", "change_your_password": "", "changed_visibility_successfully": "", "check_logs": "", + "choose_matching_people_to_merge": "Izvēlies atbilstošus cilvēkus apvienošanai", "city": "Pilsēta", "clear": "Notīrīt", "clear_all": "", "clear_message": "", "clear_value": "", - "close": "", + "close": "Aizvērt", "collapse_all": "", "color_theme": "", "comment_options": "", "comments_are_disabled": "", "confirm": "Apstiprināt", "confirm_admin_password": "", - "confirm_password": "Apstiprināt Paroli", + "confirm_password": "Apstiprināt paroli", "contain": "", "context": "", "continue": "", @@ -324,8 +343,8 @@ "create_link": "Izveidot saiti", "create_link_to_share": "Izveidot kopīgošanas saiti", "create_new_person": "", - "create_new_user": "", - "create_user": "", + "create_new_user": "Izveidot jaunu lietotāju", + "create_user": "Izveidot lietotāju", "created": "", "current_device": "", "custom_locale": "", @@ -334,6 +353,7 @@ "date_after": "", "date_and_time": "Datums un Laiks", "date_before": "", + "date_of_birth_saved": "Dzimšanas datums veiksmīgi saglabāts", "date_range": "Datumu diapazons", "day": "", "default_locale": "", @@ -344,7 +364,7 @@ "delete_library": "", "delete_link": "", "delete_shared_link": "Dzēst Kopīgošanas saiti", - "delete_user": "", + "delete_user": "Dzēst lietotāju", "deleted_shared_link": "", "description": "Apraksts", "details": "INFORMĀCIJA", @@ -360,6 +380,7 @@ "done": "Gatavs", "download": "Lejupielādēt", "downloading": "", + "duplicates": "Dublikāti", "duration": "", "durations": { "days": "", @@ -382,9 +403,11 @@ "edit_name": "Rediģēt vārdu", "edit_people": "", "edit_title": "", - "edit_user": "", + "edit_user": "Labot lietotāju", "edited": "", "editor": "", + "editor_close_without_save_prompt": "Izmaiņas netiks saglabātas", + "editor_close_without_save_title": "Aizvērt redaktoru?", "email": "E-pasts", "empty": "", "empty_album": "", @@ -395,6 +418,8 @@ "error": "", "error_loading_image": "", "errors": { + "cant_search_people": "Neizdevās veikt peronu meklēšanu", + "failed_to_create_album": "Neizdevās izveidot albumu", "unable_to_add_album_users": "", "unable_to_add_comment": "", "unable_to_add_partners": "", @@ -405,14 +430,14 @@ "unable_to_check_items": "", "unable_to_create_admin_account": "", "unable_to_create_library": "", - "unable_to_create_user": "", + "unable_to_create_user": "Neizdevās izveidot lietotāju", "unable_to_delete_album": "", "unable_to_delete_asset": "", - "unable_to_delete_user": "", + "unable_to_delete_user": "Neizdevās dzēst lietotāju", "unable_to_empty_trash": "", "unable_to_enter_fullscreen": "", "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", + "unable_to_hide_person": "Neizdevās paslēpt personu", "unable_to_load_album": "", "unable_to_load_asset_activity": "", "unable_to_load_items": "", @@ -432,6 +457,7 @@ "unable_to_restore_trash": "", "unable_to_restore_user": "", "unable_to_save_album": "", + "unable_to_save_date_of_birth": "Neizdevās saglabāt dzimšanas datumu", "unable_to_save_name": "", "unable_to_save_profile": "", "unable_to_save_settings": "", @@ -450,11 +476,11 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", + "exit_slideshow": "Iziet no slīdrādes", "expand_all": "", "expire_after": "Derīguma termiņš beidzas pēc", "expired": "Derīguma termiņš beidzās", - "explore": "", + "explore": "Izpētīt", "extension": "", "external_libraries": "", "failed_to_get_people": "", @@ -471,6 +497,7 @@ "filetype": "", "filter_people": "", "fix_incorrect_match": "", + "folders": "Mapes", "force_re-scan_library_files": "", "forward": "", "general": "", @@ -480,52 +507,59 @@ "go_to_search": "", "go_to_share_page": "", "group_albums_by": "", - "has_quota": "", + "has_quota": "Ir kvota", "hide_gallery": "", + "hide_named_person": "Paslēpt personu {name}", "hide_password": "", - "hide_person": "", + "hide_person": "Paslēpt personu", "host": "", "hour": "", "image": "Attēls", "img": "", - "immich_logo": "", - "import_path": "", - "in_archive": "", - "include_archived": "Iekļaut Arhivētos", - "include_shared_albums": "", + "immich_logo": "Immich logo", + "import_from_json": "Importēt no JSON", + "import_path": "Importa ceļš", + "in_albums": "{count, plural, one {# albumā} other {# albumos}}", + "in_archive": "Arhīvā", + "include_archived": "Iekļaut arhivētos", + "include_shared_albums": "Iekļaut koplietotos albumus", "include_shared_partner_assets": "", "individual_share": "", - "info": "", + "info": "Informācija", "interval": { - "day_at_onepm": "", + "day_at_onepm": "Katru dienu 13.00", "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "night_at_midnight": "Katru dienu pusnaktī", + "night_at_twoam": "Katru dienu 2.00 naktī" }, - "invite_people": "", + "invite_people": "Ielūgt cilvēkus", "invite_to_album": "Uzaicināt albumā", "job_settings_description": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", + "jobs": "Darbi", + "keep": "Paturēt", + "keep_all": "Paturēt visus", + "keyboard_shortcuts": "Tastatūras saīsnes", + "language": "Valoda", + "language_setting_description": "Izvēlieties vēlamo valodu", + "last_seen": "Pēdējo reizi redzēts", + "latest_version": "Jaunākā versija", + "latitude": "Ģeogrāfiskais platums", + "leave": "Paturēt", "let_others_respond": "Ļaut citiem atbildēt", - "level": "", + "level": "Līmenis", "library": "Bibliotēka", "library_options": "", "light": "", "link_options": "", "link_to_oauth": "", "linked_oauth_account": "", - "list": "", - "loading": "", + "list": "Saraksts", + "loading": "Ielādē", "loading_search_results_failed": "", "log_out": "Izrakstīties", "log_out_all_devices": "", "login_has_been_disabled": "", + "longitude": "Ģeogrāfiskais garums", "look": "", "loop_videos": "", "loop_videos_description": "Iespējot, lai automātiski videoklips tiktu cikliski palaists detaļu skatītājā.", @@ -537,48 +571,57 @@ "manage_your_api_keys": "", "manage_your_devices": "", "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", + "map": "Karte", + "map_marker_for_images": "Kartes marķieris attēliem, kas uzņemti {city}, {country}", + "map_marker_with_image": "Kartes marķieris ar attēlu", "map_settings": "Kartes Iestatījumi", - "media_type": "", - "memories": "", + "matches": "Atbilstības", + "media_type": "Multivides veids", + "memories": "Atmiņas", "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", + "memory": "Atmiņa", + "menu": "Izvēlne", + "merge": "Apvienot", + "merge_people": "Cilvēku apvienošana", + "merge_people_limit": "Vienlaikus var apvienot ne vairāk kā 5 sejas", + "merge_people_prompt": "Vai vēlies apvienot šos cilvēkus? Šī darbība ir neatgriezeniska.", + "merge_people_successfully": "Cilvēki veiksmīgi apvienoti", + "minimize": "Minimizēt", + "minute": "Minūte", + "missing": "Trūkstošie", "model": "Modelis", "month": "Mēnesis", - "more": "", + "more": "Vairāk", "moved_to_trash": "", - "my_albums": "", + "my_albums": "Mani albumi", "name": "Vārds", - "name_or_nickname": "", + "name_or_nickname": "Vārds vai iesauka", "never": "nekad", - "new_api_key": "", - "new_password": "Jauna Parole", - "new_person": "", - "new_user_created": "", + "new_album": "Jauns albums", + "new_api_key": "Jauna API atslēga", + "new_password": "Jaunā parole", + "new_person": "Jauna persona", + "new_user_created": "Izveidots jauns lietotājs", + "new_version_available": "PIEEJAMA JAUNA VERSIJA", "newest_first": "", "next": "Nākošais", - "next_memory": "", - "no": "", + "next_memory": "Nākamā atmiņa", + "no": "Nē", "no_albums_message": "", "no_archived_assets_message": "", - "no_assets_message": "", - "no_exif_info_available": "", + "no_assets_message": "NOKLIKŠĶINIET, LAI AUGŠUPIELĀDĒTU SAVU PIRMO FOTOATTĒLU", + "no_duplicates_found": "Dublikāti netika atrasti.", + "no_exif_info_available": "Nav pieejama exif informācija", "no_explore_results_message": "", "no_favorites_message": "", "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", + "no_name": "Nav nosaukuma", + "no_places": "Nav atrašanās vietu", + "no_results": "Nav rezultātu", + "no_results_description": "Izmēģiniet sinonīmu vai vispārīgāku atslēgvārdu", "no_shared_albums_message": "", - "not_in_any_album": "", - "notes": "", + "not_in_any_album": "Nav nevienā albumā", + "notes": "Piezīmes", "notification_toggle_setting_description": "", "notifications": "Paziņojumi", "notifications_setting_description": "", @@ -587,8 +630,9 @@ "ok": "", "oldest_first": "", "online": "", - "only_favorites": "", + "only_favorites": "Tikai izlase", "only_refreshes_modified_files": "", + "open_in_openstreetmap": "Atvērt OpenStreetMap", "open_the_search_filters": "", "options": "Iestatījumi", "organize_your_library": "", @@ -650,22 +694,25 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "Noņemt no albuma", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", "require_password": "", + "require_user_to_change_password_on_first_login": "Pieprasīt lietotājam mainīt paroli pēc pirmās pieteikšanās", "reset": "", "reset_password": "", "reset_people_visibility": "", "reset_settings_to_default": "", + "resolve_duplicates": "Atrisināt dublēšanās gadījumus", + "resolved_all_duplicates": "Visi dublikāti ir atrisināti", "restore": "Atjaunot", "restore_user": "", "retry_upload": "", - "review_duplicates": "", + "review_duplicates": "Pārskatīt dublikātus", "role": "", "save": "Saglabāt", "saved_profile": "", @@ -683,7 +730,9 @@ "search_city": "", "search_country": "", "search_for_existing_person": "", - "search_people": "", + "search_no_people": "Nav cilvēku", + "search_no_people_named": "Nav cilvēku ar vārdu \"{name}\"", + "search_people": "Meklēt cilvēkus", "search_places": "", "search_state": "", "search_timezone": "", @@ -691,8 +740,9 @@ "search_your_photos": "Meklēt Jūsu fotoattēlus", "searching_locales": "", "second": "", - "select_album_cover": "", + "select_album_cover": "Izvēlieties albuma vāciņu", "select_all": "", + "select_all_duplicates": "Atlasīt visus dublikātus", "select_avatar_color": "", "select_face": "", "select_featured_photo": "", @@ -702,11 +752,12 @@ "selected": "", "send_message": "", "server": "", - "server_stats": "", + "server_online": "Serveris tiešsaistē", + "server_stats": "Servera statistika", "set": "", "set_as_album_cover": "", "set_as_profile_picture": "", - "set_date_of_birth": "", + "set_date_of_birth": "Iestatīt dzimšanas datumu", "set_profile_picture": "", "set_slideshow_to_fullscreen": "", "settings": "Iestatījumi", @@ -715,7 +766,7 @@ "shared": "Kopīgots", "shared_by": "", "shared_by_you": "", - "shared_links": "Kopīgotas Saites", + "shared_links": "Kopīgotās saites", "sharing": "Kopīgošana", "sharing_sidebar_description": "", "show_album_options": "", @@ -735,8 +786,8 @@ "sign_up": "", "size": "", "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", + "slideshow": "Slīdrāde", + "slideshow_settings": "Slīdrādes iestatījumi", "sort_albums_by": "", "stack": "Steks", "stack_selected_photos": "", @@ -746,7 +797,7 @@ "status": "", "stop_motion_photo": "", "stop_photo_sharing": "Beigt kopīgot jūsu fotogrāfijas?", - "storage": "", + "storage": "Uzglabāšanas vieta", "storage_label": "", "submit": "", "suggestions": "Ieteikumi", @@ -757,23 +808,25 @@ "theme": "Dizains", "theme_selection": "", "theme_selection_description": "", + "they_will_be_merged_together": "Tās tiks apvienotas", "time_based_memories": "", "timezone": "Laika zona", "toggle_settings": "", "toggle_theme": "", "toggle_visibility": "", - "total_usage": "", + "total_usage": "Kopējais lietojums", "trash": "Atkritne", "trash_all": "", "trash_no_results_message": "", "type": "", "unarchive": "Atarhivēt", "unarchived": "", - "unfavorite": "Noņemt no Izlases", - "unhide_person": "", + "unfavorite": "Noņemt no izlases", + "unhide_person": "Atcelt personas slēpšanu", "unknown": "", "unknown_album": "", "unknown_year": "", + "unlimited": "Neierobežots", "unlink_oauth": "", "unlinked_oauth_account": "", "unselect_all": "", @@ -782,16 +835,17 @@ "updated_password": "", "upload": "Augšupielādēt", "upload_concurrency": "", + "upload_status_duplicates": "Dublikāti", "upload_status_errors": "Kļūdas", "upload_status_uploaded": "Augšupielādēts", "url": "", - "usage": "", + "usage": "Lietojums", "user": "Lietotājs", "user_id": "Lietotāja ID", - "user_usage_detail": "", + "user_usage_detail": "Informācija par lietotāju lietojumu", "username": "", "users": "Lietotāji", - "utilities": "", + "utilities": "Rīki", "validate": "", "variables": "", "version": "Versija", @@ -804,10 +858,11 @@ "view_next_asset": "", "view_previous_asset": "", "viewer": "", - "waiting": "", + "waiting": "Gaida", "week": "", "welcome_to_immich": "", "year": "", + "years_ago": "Pirms {years, plural, one {# gada} other {# gadiem}}", "yes": "Jā", "zoom_image": "Pietuvināt attēlu" } diff --git a/web/src/lib/i18n/mn.json b/web/src/lib/i18n/mn.json index 1bd96a43fd323..13658138c23df 100644 --- a/web/src/lib/i18n/mn.json +++ b/web/src/lib/i18n/mn.json @@ -660,10 +660,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "Дуртай зурагнуудаас хасах", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_from_favorites": "Дуртай зурагнуудаас хасагдсан", "removed_from_favorites_count": "Дуртай зурагнуудаас {count, plural, other {Removed #}} хасагдлаа", "repair": "", diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index df56d27a237bf..ea508d1b7ef2a 100644 --- a/web/src/lib/i18n/nb_NO.json +++ b/web/src/lib/i18n/nb_NO.json @@ -138,6 +138,8 @@ "map_style_description": "URL til et style.json-karttema", "metadata_extraction_job": "Hent metadata", "metadata_extraction_job_description": "Hent metadatainformasjon fra hver fil, for eksempel GPS-posisjon og oppløsning", + "metadata_settings": "Metadatainnstillinger", + "metadata_settings_description": "Administrer metadatainnstillinger", "migration_job": "Migrering", "migration_job_description": "Migrer miniatyrbilder for filer og ansikter til den nyeste mappestrukturen", "no_paths_added": "Ingen filbaner lagt til", @@ -146,7 +148,7 @@ "note_cannot_be_changed_later": "MERK: Dette kan ikke endres senere!", "note_unlimited_quota": "Merk: Skriv inn 0 for ubegrenset kvote", "notification_email_from_address": "Fra adresse", - "notification_email_from_address_description": "Avsenderens e-postadresse, for eksempel: \"Immich Photo Server \"", + "notification_email_from_address_description": "Avsenderens e-postadresse, for eksempel: \"Immich Photo Server \"", "notification_email_host_description": "Verten til e-posts serveren (f.eks. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorer sertifikatfeil", "notification_email_ignore_certificate_errors_description": "Ignorer valideringsfeil for TLS-sertifikat (ikke anbefalt)", @@ -196,7 +198,7 @@ "refreshing_all_libraries": "Oppdaterer alle biblioteker", "registration": "Administrator registrering", "registration_description": "Siden du er den første brukeren på systemet, vil du bli utnevnt til administrator og ha ansvar for administrative oppgaver. Du vil også opprette eventuelle nye brukere.", - "removing_offline_files": "Fjerner frakoblede filer", + "removing_deleted_files": "Fjerner frakoblede filer", "repair_all": "Reparer alle", "repair_matched_items": "Samsvarte med {count, plural, one {# element} other {# elementer}}", "repaired_items": "Reparerte {count, plural, one {# item} other {# items}}", @@ -384,6 +386,7 @@ "asset_offline": "Fil utilgjengelig", "asset_offline_description": "Dette elementet er offline. Immich kan ikke aksessere dets lokasjon. Vennlist påse at elementet er tilgijengelig og skann så biblioteket på nytt.", "asset_skipped": "Hoppet over", + "asset_skipped_in_trash": "I søppelbøtten", "asset_uploaded": "Lastet opp", "asset_uploading": "Laster opp...", "assets": "Filer", @@ -589,8 +592,8 @@ "unable_to_remove_album_users": "Kan ikke fjerne brukere fra album", "unable_to_remove_api_key": "Kan ikke fjerne API-nøkkel", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Kan ikke fjerne offlinefiler", "unable_to_remove_library": "Kan ikke fjerne bibliotek", - "unable_to_remove_offline_files": "Kan ikke fjerne offlinefiler", "unable_to_remove_partner": "Kan ikke fjerne partner", "unable_to_remove_reaction": "Kan ikke fjerne reaksjon", "unable_to_remove_user": "", @@ -841,10 +844,10 @@ "refreshed": "Oppdatert", "refreshes_every_file": "Oppdaterer alle filer", "remove": "Fjern", + "remove_deleted_assets": "Fjern fra frakoblede filer", "remove_from_album": "Fjern fra album", "remove_from_favorites": "Fjern fra favoritter", "remove_from_shared_link": "Fjern fra delt lenke", - "remove_offline_files": "Fjern fra frakoblede filer", "removed_api_key": "Fjernet API-nøkkel: {name}", "rename": "Gi nytt navn", "repair": "Reparer", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index d6b3373152c63..84cacc765527a 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -41,6 +41,7 @@ "confirm_email_below": "Typ hieronder \"{email}\" ter bevestiging", "confirm_reprocess_all_faces": "Weet je zeker dat je alle gezichten opnieuw wilt verwerken? Hiermee worden ook alle mensen gewist.", "confirm_user_password_reset": "Weet u zeker dat je het wachtwoord van {user} wilt resetten?", + "create_job": "Taak maken", "crontab_guru": "Crontab Guru", "disable_login": "Inloggen uitschakelen", "disabled": "Uitgeschakeld", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Thumbnail resolutie", "image_thumbnail_resolution_description": "Gebruikt wanneer groepen foto's bekeken worden (hoofdtijdslijn, album, enzo). Hogere resoluties kunnen meer detail behouden maar duren langer om te verwerken, hebben hogere bestandsgrootte, en kunnen de applicatie langzamer maken.", "job_concurrency": "{job} gelijktijdigheid", + "job_created": "Taak aangemaakt", "job_not_concurrency_safe": "Deze taak kan niet gelijktijdig worden uitgevoerd.", "job_settings": "Achtergrondtaak-instellingen", "job_settings_description": "Beheer gelijktijdige taken", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "LET OP: Dit kan later niet meer worden gewijzigd!", "note_unlimited_quota": "Opmerking: voer 0 in voor onbeperkt", "notification_email_from_address": "Adres afzender", - "notification_email_from_address_description": "E-mailadres van de afzender, bijvoorbeeld: \"Immich Foto Server \"", + "notification_email_from_address_description": "E-mailadres van de afzender, bijvoorbeeld: \"Immich Foto Server \"", "notification_email_host_description": "Host van de e-mailserver (bijv. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Negeer certificaatfouten", "notification_email_ignore_certificate_errors_description": "Negeer TLS certificaat validatiefouten (niet aanbevolen)", @@ -202,7 +204,7 @@ "refreshing_all_libraries": "Alle bibliotheken vernieuwen", "registration": "Beheerder registratie", "registration_description": "Omdat je de eerste gebruiker in het systeem bent, word je toegewezen als beheerder en ben je verantwoordelijk voor administratieve taken. Extra gebruikers kunnen door jou worden aangemaakt.", - "removing_offline_files": "Offline bestanden verwijderen", + "removing_deleted_files": "Offline bestanden verwijderen", "repair_all": "Repareer alle", "repair_matched_items": "Overeenkomend {count, plural, one {# item} other {# items}}", "repaired_items": "Gerepareerd {count, plural, one {# item} other {# items}}", @@ -211,6 +213,7 @@ "reset_settings_to_recent_saved": "Instellingen zijn gereset naar de recent opgeslagen instellingen", "scanning_library_for_changed_files": "Bibliotheek scannen op gewijzigde bestanden", "scanning_library_for_new_files": "Bibliotheek scannen op nieuwe bestanden", + "search_jobs": "Taak zoeken...", "send_welcome_email": "Stuur een welkomstmail", "server_external_domain_settings": "Extern domein", "server_external_domain_settings_description": "Domein voor openbaar gedeelde links, inclusief http(s)://", @@ -391,6 +394,7 @@ "asset_offline": "Asset offline", "asset_offline_description": "Deze asset is offline. Immich kan de bestandslocatie niet openen. Controleer of de asset beschikbaar is en scan de bibliotheek opnieuw.", "asset_skipped": "Overgeslagen", + "asset_skipped_in_trash": "In prullenbak", "asset_uploaded": "Geüpload", "asset_uploading": "Uploaden...", "assets": "Assets", @@ -660,6 +664,7 @@ "unable_to_get_comments_number": "Kan het aantal opmerkingen niet ophalen", "unable_to_get_shared_link": "Kan gedeelde link niet ophalen", "unable_to_hide_person": "Kan persoon niet verbergen", + "unable_to_link_motion_video": "Kan bewegende video niet verbinden", "unable_to_link_oauth_account": "Kan OAuth account niet koppelen", "unable_to_load_album": "Kan album niet laden", "unable_to_load_asset_activity": "Kan asset activiteit niet laden", @@ -676,8 +681,8 @@ "unable_to_remove_api_key": "Kan API sleutel niet verwijderen", "unable_to_remove_assets_from_shared_link": "Kan assets niet verwijderen uit gedeelde link", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Kan offline bestanden niet verwijderen", "unable_to_remove_library": "Kan bibliotheek niet verwijderen", - "unable_to_remove_offline_files": "Kan offline bestanden niet verwijderen", "unable_to_remove_partner": "Kan partner niet verwijderen", "unable_to_remove_reaction": "Kan reactie niet verwijderen", "unable_to_remove_user": "", @@ -700,6 +705,7 @@ "unable_to_submit_job": "Kan taak niet uitvoeren", "unable_to_trash_asset": "Kan asset niet naar prullenbak verplaatsen", "unable_to_unlink_account": "Kan account niet ontkoppelen", + "unable_to_unlink_motion_video": "Kan bewegende video niet los maken", "unable_to_update_album_cover": "Kan album cover niet bijwerken", "unable_to_update_album_info": "Kan albumgegevens niet bijwerken", "unable_to_update_library": "Kan bibliotheek niet bijwerken", @@ -845,6 +851,7 @@ "license_trial_info_4": "Overweeg een licentie te kopen om de verdere ontwikkeling van de service te ondersteunen", "light": "Licht", "like_deleted": "Like verwijderd", + "link_motion_video": "verbind bewegende video", "link_options": "Opties voor link", "link_to_oauth": "Koppel OAuth", "linked_oauth_account": "Gekoppeld OAuth account", @@ -1079,10 +1086,10 @@ "remove_assets_shared_link_confirmation": "Weet je zeker dat je {count, plural, one {# asset} other {# assets}} uit deze gedeelde link wilt verwijderen?", "remove_assets_title": "Assets verwijderen?", "remove_custom_date_range": "Aangepast datumbereik verwijderen", + "remove_deleted_assets": "Verwijder offline bestanden", "remove_from_album": "Verwijder uit album", "remove_from_favorites": "Verwijderen uit favorieten", "remove_from_shared_link": "Verwijderen uit gedeelde link", - "remove_offline_files": "Verwijder offline bestanden", "remove_user": "Gebruiker verwijderen", "removed_api_key": "API sleutel verwijderd: {name}", "removed_from_archive": "Verwijderd uit archief", @@ -1135,8 +1142,10 @@ "search_for_existing_person": "Zoek naar bestaande persoon", "search_no_people": "Geen mensen", "search_no_people_named": "Geen mensen genaamd \"{name}\"", + "search_options": "Zoekopties", "search_people": "Zoek mensen", "search_places": "Zoek plaatsen", + "search_settings": "Zoek instellingen", "search_state": "Zoek staat...", "search_tags": "Tags zoeken...", "search_timezone": "Zoek tijdzone...", @@ -1289,6 +1298,7 @@ "unknown_album": "Onbekend album", "unknown_year": "Onbekend jaar", "unlimited": "Onbeperkt", + "unlink_motion_video": "Maak bewegende video los", "unlink_oauth": "Ontkoppel OAuth", "unlinked_oauth_account": "OAuth account ontkoppeld", "unnamed_album": "Naamloos album", diff --git a/web/src/lib/i18n/pl.json b/web/src/lib/i18n/pl.json index b9bb3f27e82ac..5f187e2263e18 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -4,7 +4,7 @@ "account_settings": "Ustawienia Konta", "acknowledge": "Rozumiem", "action": "Akcja", - "actions": "Akcje", + "actions": "Akcje/i", "active": "Aktywne", "activity": "Aktywność", "activity_changed": "Aktywność jest {enabled, select, true {włączona} other {wyłączona}}", @@ -15,7 +15,7 @@ "add_a_title": "Dodaj tytuł", "add_exclusion_pattern": "Dodaj wzór wykluczający", "add_import_path": "Dodaj ścieżkę importu", - "add_location": "Dodaj lokacje", + "add_location": "Dodaj lokalizację", "add_more_users": "Dodaj więcej użytkowników", "add_partner": "Dodaj partnera", "add_path": "Dodaj ścieżkę", @@ -139,7 +139,11 @@ "map_settings_description": "Zarządzaj ustawieniami mapy", "map_style_description": "URL do pliku style.json z motywem mapy", "metadata_extraction_job": "Wyodrębnij metadane", - "metadata_extraction_job_description": "Wyodrębnij informacje o metadanych z każdego zasobu, takie jak GPS i rozdzielczość", + "metadata_extraction_job_description": "Wyodrębnij informacje o metadanych z każdego zasobu, takie jak GPS, twarze i rozdzielczość", + "metadata_faces_import_setting": "Włącz import twarzy", + "metadata_faces_import_setting_description": "Zaimportuj twarze z danych EXIF obrazu i plików towarzyszących", + "metadata_settings": "Ustawienia Metadanych", + "metadata_settings_description": "Zarządzaj ustawieniami metadanych", "migration_job": "Migracja", "migration_job_description": "Przenieś miniatury zasobów i twarzy do najnowszej struktury folderów", "no_paths_added": "Nie dodano ścieżki", @@ -148,7 +152,7 @@ "note_cannot_be_changed_later": "UWAŻAJ: Nie można tego później zmienić!", "note_unlimited_quota": "Wpisz by wyłączyć limit", "notification_email_from_address": "Z adresu", - "notification_email_from_address_description": "Adres e-mail nadawcy, na przykład: „Immich Photo Server ”", + "notification_email_from_address_description": "Adres e-mail nadawcy, na przykład: „Immich Photo Server ”", "notification_email_host_description": "Host serwera e-mail (np. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignoruj niepoprawny certyfikat", "notification_email_ignore_certificate_errors_description": "Ignoruj błąd walidacji certyfikatu TLS (nie zalecane)", @@ -198,7 +202,7 @@ "refreshing_all_libraries": "Wszystkie biblioteki zostaną odświeżone", "registration": "Rejestracja Administratora", "registration_description": "Jesteś pierwszym użytkownikiem aplikacji, więc twoje konto jest administratorem. Możesz zarządzać platformą, w tym dodawać nowych użytkowników.", - "removing_offline_files": "Niedostępne pliki zostaną usunięte", + "removing_deleted_files": "Niedostępne pliki zostaną usunięte", "repair_all": "Napraw Wszystko", "repair_matched_items": "Powiązano {count, plural, one {# element} few {# elementy} other {# elementów}}", "repaired_items": "Naprawiono {count, plural, one {# element} few {# elementy} other {# elementów}}", @@ -509,7 +513,7 @@ "delete_link": "Usuń link", "delete_shared_link": "Usuń udostępniony link", "delete_tag": "Usuń etykietę", - "delete_tag_confirmation_prompt": "Czy jesteś pewny(a), że chcesz usunąć etykietę {tagName}?", + "delete_tag_confirmation_prompt": "Czy na pewno chcesz usunąć etykietę {tagName}?", "delete_user": "Usuń użytkownika", "deleted_shared_link": "Pomyślnie usunięto udostępniony link", "description": "Opis", @@ -672,8 +676,8 @@ "unable_to_remove_api_key": "Usunięcie Klucza API nie powiodło się", "unable_to_remove_assets_from_shared_link": "Nie można usunąć zasobów z udostępnionego linku", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Usunięcie niedostępnych plików nie powiodło się", "unable_to_remove_library": "Usunięcie biblioteki nie powiodło się", - "unable_to_remove_offline_files": "Usunięcie niedostępnych plików nie powiodło się", "unable_to_remove_partner": "Nie można usunąć partnerów", "unable_to_remove_reaction": "Usunięcie reakcji nie powiodło się", "unable_to_remove_user": "", @@ -1048,10 +1052,10 @@ "remove_assets_shared_link_confirmation": "Czy na pewno chcesz usunąć {count, plural, one {# zasób} other {# zasoby}} z tego udostępnionego linku?", "remove_assets_title": "Usunąć zasoby?", "remove_custom_date_range": "Usuń niestandardowy zakres dat", + "remove_deleted_assets": "Usuń Niedostępne Pliki", "remove_from_album": "Usuń z albumu", "remove_from_favorites": "Usuń z ulubionych", "remove_from_shared_link": "Usuń z udostępnionego linku", - "remove_offline_files": "Usuń Niedostępne Pliki", "remove_user": "Usuń użytkownika", "removed_api_key": "Usunięto Klucz API: {name}", "removed_from_archive": "Usunięto z archiwum", @@ -1176,10 +1180,14 @@ "show_supporter_badge": "Odznaka wspierającego", "show_supporter_badge_description": "Pokaż odznakę wspierającego", "shuffle": "Losuj", + "sidebar": "Panel boczny", + "sidebar_display_description": "Wyświetl link do widoku w pasku bocznym", "sign_out": "Wyloguj się", "sign_up": "Zarejestruj się", "size": "Rozmiar", "skip_to_content": "Przejdź do treści", + "skip_to_folders": "Przejdź do folderów", + "skip_to_tags": "Przejdź do tagów", "slideshow": "Pokaz slajdów", "slideshow_settings": "Ustawienia pokazu slajdów", "sort_albums_by": "Sortuj albumy według...", @@ -1213,11 +1221,12 @@ "swap_merge_direction": "Zmień kierunek złączenia", "sync": "Synchronizuj", "tag": "Etykieta", - "tag_assets": "Zasoby etykiet", - "tag_created": "Utworzono etykietę: {tag}", - "tag_feature_description": "Przeglądanie zdjęć i filmów pogrupowanych według tematów z logiki etykiet", + "tag_assets": "Ustaw etykiety zasobów", + "tag_created": "Stworzono etykietę: {tag}", + "tag_feature_description": "Przeglądanie zdjęć i filmów pogrupowanych według logicznych etykiet wskazujących temat", "tag_not_found_question": "Nie możesz znaleźć etykiety? Utwórz ją tutaj", "tag_updated": "Uaktualniono etykietę: {tag}", + "tagged_assets": "Przypisano etykietę {count, plural, one {# zasobowi} other {# zasobom}}", "tags": "Etykiety", "template": "Szablon", "theme": "Motyw", diff --git a/web/src/lib/i18n/pt.json b/web/src/lib/i18n/pt.json index 943cde377dca9..ef6626be01505 100644 --- a/web/src/lib/i18n/pt.json +++ b/web/src/lib/i18n/pt.json @@ -1,11 +1,11 @@ { "about": "Sobre", "account": "Conta", - "account_settings": "Configurações da Conta", + "account_settings": "Definições da Conta", "acknowledge": "Confirmar", "action": "Ação", "actions": "Ações", - "active": "Ativo", + "active": "Em execução", "activity": "Atividade", "activity_changed": "A actividade está {enabled, select, true {ativada} other {desativada}}", "add": "Adicionar", @@ -22,337 +22,347 @@ "add_photos": "Adicionar fotos", "add_to": "Adicionar a...", "add_to_album": "Adicionar ao álbum", - "add_to_shared_album": "Adicionar ao álbum compartilhado", + "add_to_shared_album": "Adicionar ao álbum partilhado", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", "admin": { - "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", - "authentication_settings": "Configurações de Autenticação", - "authentication_settings_description": "Gerenciar senhas, OAuth, e outras configurações de autenticação", - "authentication_settings_disable_all": "Tem a certeza que deseja desativar todos os métodos de entrada? Entrar será completamente desativado.", + "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os ficheiros em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os ficheiros que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", + "authentication_settings": "Definições de Autenticação", + "authentication_settings_description": "Gerir palavras-passe, OAuth, e outras definições de autenticação", + "authentication_settings_disable_all": "Tem a certeza que deseja desativar todos os métodos de início de sessão? O início de sessão será completamente desativado.", "authentication_settings_reenable": "Para reativar, use um Comando de servidor.", "background_task_job": "Tarefas em segundo plano", "check_all": "Selecionar Tudo", "cleared_jobs": "Eliminadas as tarefas de: {job}", - "config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração", - "confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?", - "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará {count, plural, one {# arquivo incluído} other {todos os # arquivos incluídos}} do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.", - "confirm_email_below": "Para confirmar, digite o {email} abaixo", - "confirm_reprocess_all_faces": "Tem certeza de que deseja reprocessar todos as faces? Isso também limpará as pessoas nomeadas.", - "confirm_user_password_reset": "Tem certeza de que deseja redefinir a senha de {user}?", + "config_set_by_file": "A configuração está atualmente definida por um ficheiro de configuração", + "confirm_delete_library": "Tem a certeza de que deseja eliminar a biblioteca {library} ?", + "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará {count, plural, one {# ficheiro incluído} other {todos os # ficheiros incluídos}} do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.", + "confirm_email_below": "Para confirmar, escreva \"{email}\" abaixo", + "confirm_reprocess_all_faces": "Tem a certeza de que deseja reprocessar todos os rostos? Isto também limpará os nomes de pessoas.", + "confirm_user_password_reset": "Tem a certeza de que deseja redefinir a palavra-passe de {user}?", + "create_job": "Criar tarefa", "crontab_guru": "Guru do Crontab", - "disable_login": "Desabilitar login", + "disable_login": "Desativar inicio de sessão", "disabled": "", - "duplicate_detection_job_description": "Execute o aprendizado de máquina em arquivos para detectar imagens semelhantes. Depende da pesquisa inteligente", - "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar arquivos e pastas ao escanear sua biblioteca. Isso é útil se você tiver pastas que contenham arquivos que não deseja importar, como arquivos RAW.", + "duplicate_detection_job_description": "Executa a aprendizagem de máquina em ficheiros para detetar imagens semelhantes. Depende da Pesquisa Inteligente", + "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar ficheiros e pastas ao analisar a sua biblioteca. Isto é útil se tiver pastas que contenham ficheiros que não deseja importar, como ficheiros RAW.", "external_library_created_at": "Biblioteca externa (criada em {date})", - "external_library_management": "Gerenciamento de bibliotecas externas", - "face_detection": "Detecção de faces", - "face_detection_description": "Deteta rostos em arquivos com aprendizagem automática. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os arquivos. \"Ausente\" enfileira arquivos que ainda não foram processados. Os rostos detetados serão enfileirados para reconhecimento facial após a conclusão da deteção de rostos, agrupando-os em pessoas novas ou já existentes.", - "facial_recognition_job_description": "Agrupa rostos detectados em pessoas. Esta etapa é executada após a conclusão da deteção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira rostos que ainda não têm uma pessoa atribuída.", + "external_library_management": "Gestão de bibliotecas externas", + "face_detection": "Deteção de Rostos", + "face_detection_description": "Deteta rostos em ficheiros utilizando aprendizagem automática. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os ficheiros. \"Ausente\" coloca em fila ficheiros que ainda não foram processados. Os rostos detetados serão colocados em fila para Reconhecimento Facial após a conclusão da Deteção de Rostos, agrupando-os em pessoas novas ou já existentes.", + "facial_recognition_job_description": "Agrupa rostos detetadas em pessoas. Esta etapa é executada após a conclusão da Deteção de Rostos. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" coloca em fila rostos que ainda não têm uma pessoa atribuída.", "failed_job_command": "Comando {command} falhou para a tarefa: {job}", - "force_delete_user_warning": "AVISO: Isso removerá imediatamente o utilizador e todos os arquivos. Isso não pode ser desfeito e os ficheiros não poderão ser recuperados.", - "forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca", - "image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.", - "image_prefer_embedded_preview": "Prefira visualização incorporada", - "image_prefer_embedded_preview_setting_description": "Use visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isso pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmera e a imagem pode ter mais artefatos de compactação.", + "force_delete_user_warning": "AVISO: Isto removerá imediatamente o utilizador e todos os ficheiros. Isso não pode ser revertido e os ficheiros não poderão ser recuperados.", + "forcing_refresh_library_files": "A forçar a atualização de todos os ficheiros da biblioteca", + "image_format_description": "WebP produz ficheiros mais pequenos do que JPEG, mas é mais lento para codificar.", + "image_prefer_embedded_preview": "Preferir visualização incorporada", + "image_prefer_embedded_preview_setting_description": "Utilizar visualizações incorporadas em fotos RAW como entrada para processamento de imagem, quando disponível. Isto pode produzir cores mais precisas para algumas imagens, mas a qualidade da visualização depende da câmara e a imagem pode ter mais artefatos de compressão.", "image_prefer_wide_gamut": "Prefira ampla gama", - "image_prefer_wide_gamut_setting_description": "Use o Display P3 para miniaturas. Isso preserva melhor a vibração das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.", + "image_prefer_wide_gamut_setting_description": "Utilizar Display P3 para miniaturas. Isso preserva melhor a vibrância das imagens com espaços de cores amplos, mas as imagens podem aparecer de maneira diferente em dispositivos antigos com uma versão antiga do navegador. As imagens sRGB são mantidas como sRGB para evitar mudanças de cores.", "image_preview_format": "Formato de visualização", "image_preview_resolution": "Resolução de visualização", - "image_preview_resolution_description": "Usado ao visualizar uma única foto e para aprendizado de máquina. Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.", + "image_preview_resolution_description": "Usado ao visualizar uma única foto e para aprendizagem de máquina. Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.", "image_quality": "Qualidade", - "image_quality_description": "Qualidade de imagem de 1 a 100. Quanto maior, melhor para a qualidade, mas produz arquivos maiores. Esta opção afeta as imagens de visualização e miniatura.", - "image_settings": "Configurações de imagem", - "image_settings_description": "Gerenciar a qualidade e resolução das imagens geradas", + "image_quality_description": "Qualidade de imagem de 1 a 100. Quanto maior, melhor para a qualidade, mas produz ficheiros maiores. Esta definição afeta as imagens de visualização e miniatura.", + "image_settings": "Definições de imagem", + "image_settings_description": "Gerir a qualidade e resolução das imagens geradas", "image_thumbnail_format": "Formato de miniatura", "image_thumbnail_resolution": "Resolução de miniatura", - "image_thumbnail_resolution_description": "Usado ao visualizar grupos de fotos (linha do tempo principal, visualização de álbum, etc.). Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.", - "job_concurrency": "{job} simultâneo", - "job_not_concurrency_safe": "Este trabalho não é compatível com simultaneidade.", - "job_settings": "Configurações de trabalho", - "job_settings_description": "Gerenciar simultaneidade dos trabalhos", - "job_status": "Status do trabalho", + "image_thumbnail_resolution_description": "Utilizado ao visualizar grupos de fotos (linha do tempo principal, visualização de álbum, etc.). Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.", + "job_concurrency": "{job} em simultâneo", + "job_created": "Tarefa criada", + "job_not_concurrency_safe": "Esta tarefa não pode ser executada em simultâneo.", + "job_settings": "Definições de Tarefas", + "job_settings_description": "Gerir tarefas em simultâneo", + "job_status": "Estado das Tarefas", "jobs_delayed": "{jobCount, plural, one {# adiado} other {# adiados}}", "jobs_failed": "{jobCount, plural, one {# falhou} other {# falharam}}", - "library_created": "Criado biblioteca: {library}", + "library_created": "Criada biblioteca: {library}", "library_cron_expression": "Expressão Cron", "library_cron_expression_description": "Defina o intervalo de procura utilizando o formato cron. Para mais informações consulte Guru Crontab", "library_cron_expression_presets": "Predefinições de expressão Cron", - "library_deleted": "Biblioteca excluída", - "library_import_path_description": "Especifique uma pasta para importar. Esta pasta, incluindo subpastas, será escaneada em busca de imagens e vídeos.", - "library_scanning": "Escanear periódicamente", - "library_scanning_description": "Configurar o escaneamento periódico da biblioteca", - "library_scanning_enable_description": "Habilitar escaneamento periódico da biblioteca", + "library_deleted": "Biblioteca eliminada", + "library_import_path_description": "Especifique uma pasta para importar. Esta pasta, incluindo sub-pastas, será analisada por imagens e vídeos.", + "library_scanning": "Análise periódica", + "library_scanning_description": "Configurar a análise periódica da biblioteca", + "library_scanning_enable_description": "Ativar análise periódica da biblioteca", "library_settings": "Biblioteca Externa", - "library_settings_description": "Gerenciar configurações de biblioteca externa", - "library_tasks_description": "Execute tarefas de biblioteca", - "library_watching_enable_description": "Observe bibliotecas externas para alterações de arquivos", - "library_watching_settings": "Observação de biblioteca (EXPERIMENTAL)", - "library_watching_settings_description": "Observe automaticamente os arquivos alterados", - "logging_enable_description": "Habilitar registro", - "logging_level_description": "Quando ativado, qual nível de log usar.", - "logging_settings": "Registros", + "library_settings_description": "Gerir definições de biblioteca externa", + "library_tasks_description": "Executa tarefas de biblioteca", + "library_watching_enable_description": "Analisar bibliotecas externas por alterações de ficheiros", + "library_watching_settings": "Análise de biblioteca (EXPERIMENTAL)", + "library_watching_settings_description": "Analise automaticamente por ficheiros alterados", + "logging_enable_description": "Ativar registo", + "logging_level_description": "Quando ativado, qual o nível de log a usar.", + "logging_settings": "Registo", "machine_learning_clip_model": "Modelo CLIP", - "machine_learning_clip_model_description": "O nome do modelo CLIP definido aqui. Note que é necessário voltar a executar a \"Pesquisa Inteligente\" para todas as imagens depois de alterar um modelo.", - "machine_learning_duplicate_detection": "Detecção de duplicidade", - "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade", - "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.", - "machine_learning_duplicate_detection_setting_description": "Use embeddings CLIP para encontrar prováveis duplicidades", - "machine_learning_enabled": "Habilitar o aprendizado da máquina", - "machine_learning_enabled_description": "Se desativado, todos os recursos de ML serão desativados, independentemente das configurações abaixo.", + "machine_learning_clip_model_description": "O nome do modelo CLIP definido aqui. Tome nota de que é necessário voltar a executar a tarefa de \"Pesquisa Inteligente\" para todas as imagens depois de alterar o modelo.", + "machine_learning_duplicate_detection": "Deteção de Itens Duplicados", + "machine_learning_duplicate_detection_enabled": "Ativar deteção de itens duplicados", + "machine_learning_duplicate_detection_enabled_description": "Se desativado, ficheiros exatamente idênticos serão desduplicados na mesma.", + "machine_learning_duplicate_detection_setting_description": "Utilizar embeddings CLIP para encontrar itens provavelmente duplicados", + "machine_learning_enabled": "Ativar a aprendizagem de máquina", + "machine_learning_enabled_description": "Se desativado, todos as funcionalidades de ML serão desativados, independentemente das definições abaixo.", "machine_learning_facial_recognition": "Reconhecimento Facial", - "machine_learning_facial_recognition_description": "Deteta, reconhece e agrupa rostos em imagens", + "machine_learning_facial_recognition_description": "Detetar, reconhecer e agrupar rostos em imagens", "machine_learning_facial_recognition_model": "Modelo de reconhecimento facial", - "machine_learning_facial_recognition_model_description": "Os modelos estão listados em ordem decrescente de tamanho. Modelos maiores são mais lentos e utilizam mais memória, mas produzem melhores resultados. Observe que ao alterar um modelo, você deve executar novamente o trabalho de Detecção de faces para todas as imagens.", + "machine_learning_facial_recognition_model_description": "Os modelos estão ordenados por ordem decrescente de tamanho. Modelos maiores são mais lentos e utilizam mais memória, mas produzem melhores resultados. Tome conta de que ao alterar um modelo, deve executar novamente a tarefa de \"Deteção de Rostos\" para todas as imagens.", "machine_learning_facial_recognition_setting": "Ativar reconhecimento facial", - "machine_learning_facial_recognition_setting_description": "Se desativado, as imagens não serão codificadas para reconhecimento facial e não preencherão a seção Pessoas na página Explorar.", - "machine_learning_max_detection_distance": "Distância máxima de detecção", - "machine_learning_max_detection_distance_description": "Distância máxima entre duas imagens para considerá-las duplicadas, variando de 0,001 a 0,1. Valores mais altos detectarão mais duplicidades, mas poderão resultar em falsos positivos.", + "machine_learning_facial_recognition_setting_description": "Se desativado, as imagens não serão codificadas para reconhecimento facial e não preencherão a secção Pessoas na página Explorar.", + "machine_learning_max_detection_distance": "Distância máxima de deteção", + "machine_learning_max_detection_distance_description": "Distância máxima entre duas imagens para considerá-las duplicadas, variando de 0,001 a 0,1. Valores mais altos detetarão mais duplicidades, mas poderão resultar em falsos positivos.", "machine_learning_max_recognition_distance": "Distância máxima de reconhecimento", - "machine_learning_max_recognition_distance_description": "Distância máxima entre duas faces para ser considerada a mesma pessoa, variando de 0 a 2. Valores menores evitam rotular duas faces como a mesma pessoa, enquanto valores maiores evitam rotular a mesma face como duas pessoas diferentes. Observe que é mais fácil mesclar duas pessoas do que dividir uma pessoa em duas, portanto tenha preferência por valores mais baixos quando possível.", - "machine_learning_min_detection_score": "Pontuação mínima de detecção", - "machine_learning_min_detection_score_description": "Pontuação mínima de confiança para uma face ser detectada, de 0 a 1. Valores mais baixos detectam mais rostos, mas poderão resultar em falsos positivos.", - "machine_learning_min_recognized_faces": "Mínimo de faces reconhecidas", - "machine_learning_min_recognized_faces_description": "O número mínimo de faces reconhecidas para uma pessoa ser criada na lista. Aumentar isso torna o Reconhecimento Facial mais preciso, ao custo de aumentar a chance de um rosto não ser atribuído a uma pessoa.", - "machine_learning_settings": "Configurações de aprendizado de máquina (Machine Learning)", - "machine_learning_settings_description": "Gerenciar recursos e configurações de aprendizado de máquina", - "machine_learning_smart_search": "Busca inteligente", - "machine_learning_smart_search_description": "Pesquise imagens semanticamente usando embeddings CLIP", - "machine_learning_smart_search_enabled": "Habilite a pesquisa inteligente", - "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para pesquisa inteligente.", - "machine_learning_url_description": "URL do servidor de aprendizado de máquina", - "manage_concurrency": "Gerenciar simultaneidade", - "manage_log_settings": "Gerenciar configurações de registro", + "machine_learning_max_recognition_distance_description": "Distância máxima entre dois rostos para serem considerados a mesma pessoa, variando de 0 a 2. Valores menores evitam rotular dois rostos como a mesma pessoa, enquanto valores maiores evitam rotular o mesmo rosto como duas pessoas diferentes. Tenha em conta de que é mais fácil unir duas pessoas do que dividir uma pessoa em duas, portanto tenha preferência por valores mais baixos quando possível.", + "machine_learning_min_detection_score": "Pontuação mínima de deteção", + "machine_learning_min_detection_score_description": "Pontuação mínima de confiança para um rosto ser detetado, de 0 a 1. Valores mais baixos detetam mais rostos, mas poderão resultar em falsos positivos.", + "machine_learning_min_recognized_faces": "Mínimo de rostos reconhecidos", + "machine_learning_min_recognized_faces_description": "O número mínimo de faces reconhecidas para uma pessoa ser criada na lista. Aumentar isto torna o Reconhecimento Facial mais preciso, no entanto aumenta a probabilidade de um rosto não ser atribuído a uma pessoa.", + "machine_learning_settings": "Definições de aprendizagem de máquina (Machine Learning)", + "machine_learning_settings_description": "Gerir funcionalidades e definições de aprendizagem de máquina", + "machine_learning_smart_search": "Pesquisa Inteligente", + "machine_learning_smart_search_description": "Pesquise imagens semanticamente utilizando embeddings CLIP", + "machine_learning_smart_search_enabled": "Ativar a Pesquisa Inteligente", + "machine_learning_smart_search_enabled_description": "Se desativado, as imagens não serão codificadas para Pesquisa Inteligente.", + "machine_learning_url_description": "URL do servidor de aprendizagem de máquina", + "manage_concurrency": "Gerir simultaneidade", + "manage_log_settings": "Gerir definições de registo", "map_dark_style": "Tema Escuro", - "map_enable_description": "Ativar recursos do mapa", + "map_enable_description": "Ativar funcionalidades de mapa", "map_gps_settings": "Mapas e Definições de GPS", - "map_gps_settings_description": "Configurações de mapas e GPS (Geocoding inverso)", - "map_implications": "A funcionalidade do mapa necessita um servico externo (tiles.immich.cloud)", + "map_gps_settings_description": "Gerir Definições de Mapas e GPS (Geocodificação Reversa)", + "map_implications": "A funcionalidades do mapa necessita um serviço externo (tiles.immich.cloud)", "map_light_style": "Tema Claro", - "map_manage_reverse_geocoding_settings": "Gerir definições de Geocoding inverso", - "map_reverse_geocoding": "Geocodificação reversa", - "map_reverse_geocoding_enable_description": "Ativar geocodificação reversa", - "map_reverse_geocoding_settings": "Configurações de geocodificação reversa", + "map_manage_reverse_geocoding_settings": "Gerir definições de Geocodificação Reversa", + "map_reverse_geocoding": "Geocodificação Reversa", + "map_reverse_geocoding_enable_description": "Ativar Geocodificação Reversa", + "map_reverse_geocoding_settings": "Definições de Geocodificação Reversa", "map_settings": "Mapa", - "map_settings_description": "Gerenciar configurações do mapa", + "map_settings_description": "Gerir definições do mapa", "map_style_description": "URL para um tema de mapa style.json", "metadata_extraction_job": "Extrair metadados", - "metadata_extraction_job_description": "Extraia informações de metadados de cada ativo, como GPS e resolução", + "metadata_extraction_job_description": "Extrai informações de metadados de cada ficheiro, como GPS, rostos e resolução", + "metadata_faces_import_setting": "Ativar a importação facial", + "metadata_faces_import_setting_description": "Importar rostos a partir dos dados EXIF da imagem e ficheiros anexos", + "metadata_settings": "Definições de metadados", + "metadata_settings_description": "Gerir definições de metadados", "migration_job": "Migração", - "migration_job_description": "Migre miniaturas de arquivos e rostos para a estrutura de pastas mais recente", + "migration_job_description": "Migra miniaturas de ficheiros e rostos para a estrutura de pastas mais recente", "no_paths_added": "Nenhum caminho adicionado", "no_pattern_added": "Nenhum padrão adicionado", - "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", + "note_apply_storage_label_previous_assets": "Observação: Para aplicar o Rótulo de Armazenamento a ficheiros carregados anteriormente, execute o", "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", - "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", + "note_unlimited_quota": "Observação: insira 0 para quota ilimitada", "notification_email_from_address": "A partir do endereço", - "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", + "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Servidor de Fotos Immich \"", "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar erros de certificado", "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)", - "notification_email_password_description": "Senha a ser usada ao autenticar no servidor de e-mail", + "notification_email_password_description": "Palavra-passe a ser usada ao autenticar no servidor de e-mail", "notification_email_port_description": "Porta do servidor de e-mail (por exemplo, 25, 465 ou 587)", - "notification_email_sent_test_email_button": "Envie e-mail de teste e salve", - "notification_email_setting_description": "Configurações para envio de notificações por e-mail", + "notification_email_sent_test_email_button": "Enviar e-mail de teste e gravar", + "notification_email_setting_description": "Definições para envio de notificações por e-mail", "notification_email_test_email": "Enviar e-mail de teste", - "notification_email_test_email_failed": "Falha ao enviar e-mail de teste. Verifique seus valores", - "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.", + "notification_email_test_email_failed": "Falha ao enviar e-mail de teste, verifique os valores", + "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique a sua caixa de entrada.", "notification_email_username_description": "Nome de utilizador a ser usado ao autenticar com o servidor de e-mail", - "notification_enable_email_notifications": "Habilitar notificações por e-mail", - "notification_settings": "Configurações de notificação", - "notification_settings_description": "Gerenciar configurações de notificação, incluindo e-mail", - "oauth_auto_launch": "Inicialização automática", - "oauth_auto_launch_description": "Inicie o fluxo de login do OAuth automaticamente ao navegar até a página de login", - "oauth_auto_register": "Registro automático", - "oauth_auto_register_description": "Registre automaticamente novos utilizadores após fazer login com OAuth", - "oauth_button_text": "Botão de texto", + "notification_enable_email_notifications": "Ativar notificações por e-mail", + "notification_settings": "Definições de notificações", + "notification_settings_description": "Gerir definições de notificações, incluindo e-mail", + "oauth_auto_launch": "Arranque automático", + "oauth_auto_launch_description": "Iniciar o fluxo de login do OAuth automaticamente ao navegar até a página de inicio de sessão", + "oauth_auto_register": "Registo automático", + "oauth_auto_register_description": "Registar automaticamente novos utilizadores após iniciarem sessão com o OAuth", + "oauth_button_text": "Texto do botão", "oauth_client_id": "ID do Cliente", "oauth_client_secret": "Segredo do cliente", - "oauth_enable_description": "Faça login com OAuth", + "oauth_enable_description": "Iniciar sessão com o OAuth", "oauth_issuer_url": "URL do emissor", "oauth_mobile_redirect_uri": "URI de redirecionamento móvel", "oauth_mobile_redirect_uri_override": "Substituição de URI de redirecionamento móvel", - "oauth_mobile_redirect_uri_override_description": "Ative quando 'app.immich:/' for um URI de redirecionamento inválido.", + "oauth_mobile_redirect_uri_override_description": "Ative quando o provedor do OAuth não permite um URI móvel, como '{callback}'", "oauth_profile_signing_algorithm": "Algoritmo de assinatura de perfis", "oauth_profile_signing_algorithm_description": "Algoritmo utilizado para assinar o perfil de utilizador.", "oauth_scope": "Escopo", "oauth_settings": "OAuth", - "oauth_settings_description": "Gerenciar configurações de login do OAuth", + "oauth_settings_description": "Gerir definições de inicio de sessão do OAuth", "oauth_settings_more_details": "Para mais informações sobre esta funcionalidade, veja a documentação.", "oauth_signing_algorithm": "Algoritmo de assinatura", - "oauth_storage_label_claim": "Reivindicação de rótulo de armazenamento", - "oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do utilizador para o valor desta declaração.", - "oauth_storage_quota_claim": "Reivindicação de cota de armazenamento", - "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do utilizador para o valor desta declaração.", - "oauth_storage_quota_default": "Cota de armazenamento padrão (GiB)", - "oauth_storage_quota_default_description": "Cota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para cota ilimitada).", - "offline_paths": "Caminhos off-line", - "offline_paths_description": "Esses resultados podem ser devidos à exclusão manual de arquivos que não fazem parte de uma biblioteca externa.", - "password_enable_description": "Login com e-mail e senha", - "password_settings": "Senha de acesso", - "password_settings_description": "Gerenciar configurações de login e senha", + "oauth_storage_label_claim": "Reivindicação de Rótulo de Armazenamento", + "oauth_storage_label_claim_description": "Definir automaticamente o Rótulo de Armazenamento do utilizador para o valor desta declaração.", + "oauth_storage_quota_claim": "Reivindicação de quota de armazenamento", + "oauth_storage_quota_claim_description": "Definir automaticamente a quota de armazenamento do utilizador para o valor desta declaração.", + "oauth_storage_quota_default": "Quota de armazenamento padrão (GiB)", + "oauth_storage_quota_default_description": "Quota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para quota ilimitada).", + "offline_paths": "Caminhos Offline", + "offline_paths_description": "Estes resultados podem ser devidos à eliminação manual de ficheiros que não fazem parte de uma biblioteca externa.", + "password_enable_description": "Iniciar sessão com e-mail e palavra-passe", + "password_settings": "Palavra-passe de acesso", + "password_settings_description": "Gerir definições de inicio de sessão e palavra-passe", "paths_validated_successfully": "Todos os caminhos validados com sucesso", - "quota_size_gib": "Tamanho da cota (GiB)", - "refreshing_all_libraries": "Atualizando todas as bibliotecas", - "registration": "Registo de Admin", + "person_cleanup_job": "Limpeza de pessoas", + "quota_size_gib": "Tamanho da quota (GiB)", + "refreshing_all_libraries": "A atualizar todas as bibliotecas", + "registration": "Registo de Administrador", "registration_description": "Como é o primeiro utilizador no sistema, será marcado como administrador, e será responsável pelas tarefas administrativas, sendo que utilizadores adicionais serão criados por si.", - "removing_offline_files": "Removendo arquivos offline", + "removing_deleted_files": "Removendo arquivos offline", "repair_all": "Reparar tudo", - "repair_matched_items": "Encontrado {count, plural, one {# item} other {# itens}}", - "repaired_items": "Reparado {count, plural, one {# item} other {# itens}}", - "require_password_change_on_login": "Exigir que o utilizador altere a senha no primeiro início de sessão", - "reset_settings_to_default": "Redefinir as configurações para o padrão", - "reset_settings_to_recent_saved": "Redefinir as configurações para as configurações salvas recentemente", - "scanning_library_for_changed_files": "Escaneando a biblioteca em busca de arquivos alterados", - "scanning_library_for_new_files": "Escaneando a biblioteca em busca de novos arquivos", + "repair_matched_items": "Encontrado(s) {count, plural, one {# item} other {# itens}}", + "repaired_items": "Reparado(s) {count, plural, one {# item} other {# itens}}", + "require_password_change_on_login": "Exigir que o utilizador altere a palavra-passe no primeiro início de sessão", + "reset_settings_to_default": "Redefinir as definições para o padrão", + "reset_settings_to_recent_saved": "Redefinir as definições para as guardadas mais recentemente", + "scanning_library_for_changed_files": "A analisar a biblioteca por ficheiros alterados", + "scanning_library_for_new_files": "A analisar a biblioteca por ficheiros novos", + "search_jobs": "Pesquisar tarefas...", "send_welcome_email": "Enviar e-mail de boas-vindas", "server_external_domain_settings": "Domínio externo", - "server_external_domain_settings_description": "Domínio para links públicos compartilhados, incluindo http(s)://", - "server_settings": "Configurações do servidor", - "server_settings_description": "Gerenciar configurações do servidor", + "server_external_domain_settings_description": "Domínio para links públicos partilhados, incluindo http(s)://", + "server_settings": "Definições do Servidor", + "server_settings_description": "Gerir definições do servidor", "server_welcome_message": "Mensagem de boas-vindas", - "server_welcome_message_description": "Uma mensagem exibida na página de login.", + "server_welcome_message_description": "Uma mensagem que é exibida na página de inicio de sessão.", "sidecar_job": "Metadados secundários", - "sidecar_job_description": "Descubra ou sincronize metadados secundários do sistema de arquivos", + "sidecar_job_description": "Descobrir ou sincronizar metadados secundários a partir do sistema de ficheiros", "slideshow_duration_description": "Tempo em segundos para exibir cada imagem", - "smart_search_job_description": "Execute a aprendizagem automática em arquivos para oferecer suporte à pesquisa inteligente", - "storage_template_date_time_description": "O registro de data e hora da criação é usado para fornecer essas informações", + "smart_search_job_description": "Execute a aprendizagem automática em ficheiros para oferecer apoio à Pesquisa Inteligente", + "storage_template_date_time_description": "O registo de data e hora de criação do ficheiro é usado para fornecer essas informações", "storage_template_date_time_sample": "Exemplo de tempo {date}", - "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento", + "storage_template_enable_description": "Ativar mecanismo de modelo de armazenamento", "storage_template_hash_verification_enabled": "Verificação de hash ativada", - "storage_template_hash_verification_enabled_description": "Ativa a verificação de hash, não desative esta opção a menos que tenha certeza das implicações", + "storage_template_hash_verification_enabled_description": "Ativa a verificação de hash, não desative esta opção a menos que tenha a certeza das implicações", "storage_template_migration": "Migração de modelo de armazenamento", - "storage_template_migration_description": "Aplicar o {template} atual para arquivos previamente carregados", - "storage_template_migration_info": "As mudanças do modelo apenas se aplicarão a novos arquivos. Para aplicar o modelo retroativamente para os arquivos carregados anteriormente, execute o {job}.", - "storage_template_migration_job": "Trabalho de migração do modelo de armazenamento", - "storage_template_more_details": "Para mais informações sobre esta funcionalidade, dirija-se a Modelo de Armazenamento e as suas implicações", - "storage_template_onboarding_description": "Quando ativada, esta funcionalidade irá organizar os ficheiros automaticamente baseando-se num modelo definido pelo utilizador. Devido a problemas de estabilidade esta funcionalidade está desativada por defeito. Para mais informações, por favor leia a documentação.", + "storage_template_migration_description": "Aplica o {template} atual para ficheiros previamente carregados", + "storage_template_migration_info": "As mudanças do modelo apenas se aplicarão a novos ficheiros. Para aplicar o modelo retroativamente para os ficheiros carregados anteriormente, execute o {job}.", + "storage_template_migration_job": "Tarefa de Migração do Modelo de Armazenamento", + "storage_template_more_details": "Para mais informações sobre esta funcionalidade, dirija-se a Modelo de Armazenamento e às suas implicações", + "storage_template_onboarding_description": "Quando ativada, esta funcionalidade irá organizar os ficheiros automaticamente baseando-se num modelo definido pelo utilizador. Devido a problemas de estabilidade esta funcionalidade está desativada por padrão. Para mais informações, por favor leia a documentação.", "storage_template_path_length": "Limite aproximado do tamanho do caminho: {length, number}{limit, number}", - "storage_template_settings": "Modelo de armazenamento", - "storage_template_settings_description": "Gerenciar a estrutura de pastas e o nome do arquivo dos ativos carregados", + "storage_template_settings": "Modelo de Armazenamento", + "storage_template_settings_description": "Gerir a estrutura de pastas e o nome do ficheiro carregado", "storage_template_user_label": "{label} é o Rótulo do Armazenamento do utilizador", - "system_settings": "Configurações de Sistema", - "theme_custom_css_settings": "CSS customizado", - "theme_custom_css_settings_description": "Folhas de estilo em cascata permitem que o design do Immich seja personalizado.", - "theme_settings": "Configurações de tema", - "theme_settings_description": "Gerencie a personalização da interface web do Immich", - "these_files_matched_by_checksum": "Esses arquivos são correspondidos por seus checksum", + "system_settings": "Definições de Sistema", + "tag_cleanup_job": "Limpeza de etiquetas", + "theme_custom_css_settings": "CSS Personalizado", + "theme_custom_css_settings_description": "Folhas de estilo em cascata (CSS) permitem que o design do Immich seja personalizado.", + "theme_settings": "Definições de Tema", + "theme_settings_description": "Gerir a personalização da interface web do Immich", + "these_files_matched_by_checksum": "Estes ficheiros são correspondidos pelas suas somas de verificação", "thumbnail_generation_job": "Gerar miniaturas", - "thumbnail_generation_job_description": "Gere miniaturas grandes, pequenas e desfocadas para cada ativo, bem como miniaturas para cada pessoa", + "thumbnail_generation_job_description": "Gera miniaturas grandes, pequenas e desfocadas para cada ficheiro, bem como miniaturas para cada pessoa", "transcode_policy_description": "", "transcoding_acceleration_api": "API de aceleração", - "transcoding_acceleration_api_description": "A API que irá interagir com o seu dispositivo para acelerar a transcodificação. Esta configuração é a 'melhor opção': ela retornará à transcodificação de software em caso de falha. O VP9 pode não funcionar dependendo do seu hardware.", + "transcoding_acceleration_api_description": "A API que irá interagir com o seu dispositivo para acelerar a transcodificação. Esta definição é a 'melhor opção': ela voltará à transcodificação de software em caso de falha. O VP9 pode não funcionar dependendo do seu hardware.", "transcoding_acceleration_nvenc": "NVENC (requer GPU NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (requer CPU Intel de 7ª geração ou posterior)", "transcoding_acceleration_rkmpp": "RKMPP (apenas em SOCs Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "Codecs de áudio aceitos", - "transcoding_accepted_audio_codecs_description": "Selecione quais codecs de áudio não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", + "transcoding_accepted_audio_codecs": "Codecs de áudio aceites", + "transcoding_accepted_audio_codecs_description": "Selecione os codecs de áudio que não precisam de ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", "transcoding_accepted_containers": "Contentores aceites", - "transcoding_accepted_containers_description": "Selecione os formatos de contentores que não precisam de ser remuxed para MP4. Apenas usados para algumas políticas de transcodificação.", + "transcoding_accepted_containers_description": "Selecione os formatos de contentores que não precisam de ser remisturados para MP4. Usado apenas para algumas políticas de transcodificação.", "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos", - "transcoding_accepted_video_codecs_description": "Selecione quais codecs de vídeo não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", - "transcoding_advanced_options_description": "Opções que a maioria dos utilizadores não deveria precisar alterar", + "transcoding_accepted_video_codecs_description": "Selecione quais os codecs de vídeo que não precisam de ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", + "transcoding_advanced_options_description": "Opções que a maioria dos utilizadores não deverá precisar de alterar", "transcoding_audio_codec": "Codec de áudio", - "transcoding_audio_codec_description": "Opus é a opção de mais alta qualidade, mas tem menor compatibilidade com dispositivos ou softwares antigos.", - "transcoding_bitrate_description": "Vídeos com taxa de bits superior à máxima ou que não estão em um formato aceito", - "transcoding_codecs_learn_more": "Para aprender mais sobre as terminologias utilizadas aqui, consulte a documentação do FFmpeg para o codec H.264, codec HEVC e codec VP9.", + "transcoding_audio_codec_description": "Opus é a opção de mais alta qualidade, mas tem menor compatibilidade com dispositivos ou software antigos.", + "transcoding_bitrate_description": "Vídeos com taxa de bits superior à máxima ou que não estão num formato aceite", + "transcoding_codecs_learn_more": "Para saber mais sobre as terminologias utilizadas aqui, consulte a documentação do FFmpeg para o codec H.264, codec HEVC e codec VP9.", "transcoding_constant_quality_mode": "Modo de qualidade constante", "transcoding_constant_quality_mode_description": "ICQ é melhor que CQP, mas alguns dispositivos de aceleração de hardware não suportam este modo. Definir esta opção dará preferência ao modo especificado ao usar codificação baseada em qualidade. Ignorado pelo NVENC porque não suporta ICQ.", "transcoding_constant_rate_factor": "Fator de taxa constante (-crf)", - "transcoding_constant_rate_factor_description": "Nível de qualidade do vídeo. Os valores típicos são 23 para H.264, 28 para HEVC, 31 para VP9 e 35 para AV1. Menor é melhor, mas produz arquivos maiores.", - "transcoding_disabled_description": "Não transcodifique nenhum vídeo, pois pode interromper a reprodução em alguns clientes", + "transcoding_constant_rate_factor_description": "Nível de qualidade do vídeo. Os valores típicos são 23 para H.264, 28 para HEVC, 31 para VP9 e 35 para AV1. Menor é melhor, mas produz ficheiros maiores.", + "transcoding_disabled_description": "Não transcodificar nenhum vídeo, no entanto pode causar erros de reprodução em alguns clientes", "transcoding_hardware_acceleration": "Aceleração de hardware", "transcoding_hardware_acceleration_description": "Experimental; muito mais rápido, mas terá qualidade inferior com a mesma taxa de bits", "transcoding_hardware_decoding": "Decodificação de hardware", "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC, QSV e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.", "transcoding_hevc_codec": "Codec HEVC", "transcoding_max_b_frames": "Máximo de quadros B", - "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.", + "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compressão, mas tornam a codificação mais lenta. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.", "transcoding_max_bitrate": "Taxa de bits máxima", - "transcoding_max_bitrate_description": "Definir uma taxa de bits máxima pode tornar os tamanhos dos arquivos mais previsíveis com um custo menor de qualidade. Em 720p, os valores típicos são 2.600k para VP9 ou HEVC, ou 4.500k para H.264. Desativado se definido como 0.", + "transcoding_max_bitrate_description": "Definir uma taxa de bits máxima pode tornar os tamanhos dos ficheiros mais previsíveis com um custo menor de qualidade. Em 720p, os valores típicos são 2.600k para VP9 ou HEVC, ou 4.500k para H.264. Desativado se definido como 0.", "transcoding_max_keyframe_interval": "Intervalo máximo de quadro-chave", - "transcoding_max_keyframe_interval_description": "Define a distância máxima do quadro entre os quadros-chave. Valores mais baixos pioram a eficiência da compressão, mas melhoram os tempos de busca e podem melhorar a qualidade em cenas com movimento rápido. 0 define esse valor automaticamente.", - "transcoding_optimal_description": "Vídeos com resolução superior à desejada ou em formato não aceito", + "transcoding_max_keyframe_interval_description": "Define a distância máxima do quadro entre os quadros-chave. Valores mais baixos pioram a eficiência da compressão, mas melhoram os tempos de procura e podem melhorar a qualidade em cenas com movimento rápido. 0 define esse valor automaticamente.", + "transcoding_optimal_description": "Vídeos com resolução superior à desejada ou num formato não aceite", "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.", - "transcoding_preset_preset": "Predefinido (-preset)", - "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de \"mais rápidas\".", + "transcoding_preset_preset": "Predefinição (-preset)", + "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem ficheiros menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de \"mais rápido\".", "transcoding_reference_frames": "Quadros de referência", - "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao compactar um determinado quadro. Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. 0 define esse valor automaticamente.", - "transcoding_required_description": "Somente vídeos que não estejam em um formato aceito", - "transcoding_settings": "Configurações de transcodificação de vídeo", - "transcoding_settings_description": "Gerencie as informações de resolução e codificação dos arquivos de vídeo", + "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao comprimir um determinado quadro. Valores mais altos melhoram a eficiência da compressão, mas tornam a codificação mais lenta. 0 define esse valor automaticamente.", + "transcoding_required_description": "Apenas vídeos que não estejam num formato aceite", + "transcoding_settings": "Definições de transcodificação de vídeo", + "transcoding_settings_description": "Gerir as informações de resolução e codificação dos ficheiros de vídeo", "transcoding_target_resolution": "Resolução desejada", - "transcoding_target_resolution_description": "Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de arquivo maiores e podem reduzir a capacidade de resposta do aplicativo.", + "transcoding_target_resolution_description": "Resoluções mais altas podem preservar mais detalhes, mas demoram mais para codificar, têm tamanhos de ficheiro maiores e podem reduzir a capacidade de resposta da aplicação.", "transcoding_temporal_aq": "QA temporal", "transcoding_temporal_aq_description": "Aplica-se apenas ao NVENC. Aumenta a qualidade de cenas com alto detalhe e pouco movimento. Pode não ser compatível com dispositivos mais antigos.", "transcoding_threads": "Threads", - "transcoding_threads_description": "Valores mais altos levam a uma codificação mais rápida, mas deixam menos espaço para o servidor processar outras tarefas enquanto estiver ativo. Este valor não deve ser superior ao número de núcleos da CPU. Maximiza a utilização se definido como 0.", + "transcoding_threads_description": "Valores mais altos levam a uma codificação mais rápida, mas deixam menos espaço para o servidor processar outras tarefas enquanto estiver ativo. Este valor não deve ser superior ao número de núcleos do CPU. Maximiza a utilização se definido como 0.", "transcoding_tone_mapping": "Mapeamento de tons", "transcoding_tone_mapping_description": "Tenta preservar a aparência dos vídeos HDR quando convertidos para SDR. Cada algoritmo faz compensações diferentes em termos de cor, detalhes e brilho. Hable preserva os detalhes, Mobius preserva as cores e Reinhard preserva o brilho.", "transcoding_tone_mapping_npl": "NPL de mapeamento de tons", - "transcoding_tone_mapping_npl_description": "As cores serão ajustadas para parecerem normais para uma exibição com esse brilho. Contra-intuitivamente, valores mais baixos aumentam o brilho do vídeo e vice-versa, uma vez que compensam o brilho da tela. 0 define esse valor automaticamente.", + "transcoding_tone_mapping_npl_description": "As cores serão ajustadas para parecerem normais para uma exibição com esse brilho. Contra-intuitivamente, valores mais baixos aumentam o brilho do vídeo e vice-versa, uma vez que compensam o brilho do ecrã. 0 define esse valor automaticamente.", "transcoding_transcode_policy": "Política de transcodificação", - "transcoding_transcode_policy_description": "Política para quando um vídeo deve ser transcodificado. Os vídeos HDR sempre serão transcodificados (exceto se a transcodificação estiver desativada).", - "transcoding_two_pass_encoding": "Codificação de duas passagens", - "transcoding_two_pass_encoding_setting_description": "Transcodifique em duas passagens para produzir vídeos melhor codificados. Quando a taxa de bits máxima está habilitada (necessária para funcionar com H.264 e HEVC), este modo usa um intervalo de taxa de bits baseado na taxa de bits máxima e ignora o CRF. Para VP9, o CRF pode ser usado se a taxa de bits máxima estiver desabilitada.", + "transcoding_transcode_policy_description": "Política para quando um vídeo deve ser transcodificado. Os vídeos HDR serão sempre transcodificados (exceto se a transcodificação estiver desativada).", + "transcoding_two_pass_encoding": "Codificação em duas passagens", + "transcoding_two_pass_encoding_setting_description": "Transcodificar em duas passagens para produzir vídeos melhor codificados. Quando a taxa de bits máxima está ativada (necessário para funcionar com H.264 e HEVC), este modo usa um intervalo de taxa de bits baseado na taxa de bits máxima e ignora o CRF. Para VP9, o CRF pode ser usado se a taxa de bits máxima estiver desativada.", "transcoding_video_codec": "Codec de vídeo", - "transcoding_video_codec_description": "O VP9 tem alta eficiência e compatibilidade com a web, mas leva mais tempo para transcodificar. HEVC tem desempenho semelhante, mas tem menor compatibilidade com a web. H.264 é amplamente compatível e rápido de transcodificar, mas produz arquivos muito maiores. AV1 é o codec mais eficiente, mas não possui suporte em dispositivos mais antigos.", - "trash_enabled_description": "Ativar recursos da Lixeira", + "transcoding_video_codec_description": "O VP9 tem alta eficiência e compatibilidade com a web, mas leva mais tempo para transcodificar. HEVC tem desempenho semelhante, mas tem menor compatibilidade com a web. H.264 é amplamente compatível e rápido de transcodificar, mas produz ficheiros muito maiores. AV1 é o codec mais eficiente, mas não possui suporte em dispositivos mais antigos.", + "trash_enabled_description": "Ativar funcionalidade da Reciclagem", "trash_number_of_days": "Número de dias", - "trash_number_of_days_description": "Número de dias para manter os arquivos na lixeira antes de eliminar permanentemente", - "trash_settings": "Configurações da Lixeira", - "trash_settings_description": "Gerenciar configurações da lixeira", - "untracked_files": "Arquivos não rastreados", - "untracked_files_description": "Esses arquivos não são rastreados pelo aplicativo. Eles podem ser o resultado de movimentos malsucedidos, carregamentos interrompidos ou deixados para trás devido a um bug", - "user_delete_delay": "A conta e os arquivos de {user} serão agendados para eliminação permanente em {delay, plural, one {# dia} other {# dias}}.", - "user_delete_delay_settings": "Excluir atraso", - "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um utilizador. O trabalho de exclusão de utilizadores é executado à meia-noite para verificar utilizadores que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", - "user_delete_immediately": "A conta e os arquivos de {user} serão enfileirados para exclusão permanente imediatamente.", - "user_delete_immediately_checkbox": "Adicionar utilizador e arquivos à fila para eliminação imediata", - "user_management": "Gerenciamento de utilizadores", - "user_password_has_been_reset": "A senha do utilizador foi redefinida:", - "user_password_reset_description": "Forneça a senha temporária ao utilizador e informe que ele precisará alterar a senha no próximo início de sessão.", + "trash_number_of_days_description": "Número de dias para manter os ficheiros na reciclagem antes de os eliminar permanentemente", + "trash_settings": "Definições da Reciclagem", + "trash_settings_description": "Gerir definições da reciclagem", + "untracked_files": "Ficheiros não monitorizados", + "untracked_files_description": "Estes ficheiros não são monitorizados pela aplicação. Eles podem ser o resultado de transferências mal-sucedidas, carregamentos interrompidos ou deixados para trás devido a um problema", + "user_cleanup_job": "Limpeza de utilizadores", + "user_delete_delay": "A conta e os ficheiros de {user} serão agendados para eliminação permanente dentro de {delay, plural, one {# dia} other {# dias}}.", + "user_delete_delay_settings": "Atraso de eliminação", + "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os ficheiros de um utilizador. A tarefa de eliminação de utilizadores é executada à meia-noite para verificar utilizadores que estão prontos para eliminação. As alterações a esta definição serão avaliadas na próxima execução.", + "user_delete_immediately": "A conta e os ficheiros de {user} serão colocados em fila para eliminação permanente de imediato.", + "user_delete_immediately_checkbox": "Adicionar utilizador e ficheiros à fila para eliminação imediata", + "user_management": "Gestão de utilizadores", + "user_password_has_been_reset": "A palavra-passe do utilizador foi redefinida:", + "user_password_reset_description": "Por favor forneça a palavra-passe temporária ao utilizador e informe-o(a) de que será necessário alterá-la próximo início de sessão.", "user_restore_description": "A conta de {user} será restaurada.", - "user_restore_scheduled_removal": "Restaurar usuário - planejar remoção em {date, date, long}", - "user_settings": "Configurações do Utilizador", - "user_settings_description": "Gerenciar configurações do utilizador", + "user_restore_scheduled_removal": "Restaurar utilizador - remoção agendada em {date, date, long}", + "user_settings": "Definições do Utilizador", + "user_settings_description": "Gerir definições do utilizador", "user_successfully_removed": "O utilizador {email} foi removido com sucesso.", "version_check_enabled_description": "Ativa verificação de novas versões", - "version_check_implications": "A funcionalidade de verificação da versão necessita comunicação periodica com github.com", + "version_check_implications": "A funcionalidade de verificação da versão necessita de comunicação periódica com o github.com", "version_check_settings": "Verificação de versão", "version_check_settings_description": "Ativar/desativar a notificação de nova versão", "video_conversion_job": "Transcodificar vídeos", - "video_conversion_job_description": "Transcodifique vídeos para maior compatibilidade com navegadores e dispositivos" + "video_conversion_job_description": "Transcodificar vídeos para maior compatibilidade com navegadores e dispositivos" }, "admin_email": "E-mail do administrador", - "admin_password": "Senha do administrador", + "admin_password": "Palavra-passe do administrador", "administration": "Administração", "advanced": "Avançado", "age_months": "Idade {months, plural, one {# mês} other {# meses}}", "age_year_months": "Idade 1 ano, {months, plural, one {# mês} other {# meses}}", - "age_years": "Idade {years, plural, one{# ano} other {# anos}}", + "age_years": "{years, plural, one{# ano} other {# anos}}", "album_added": "Álbum adicionado", - "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado", + "album_added_notification_setting_description": "Receber uma notificação por e-mail quando for adicionado a um álbum partilhado", "album_cover_updated": "Capa do álbum atualizada", - "album_delete_confirmation": "Tem a certeza que quer apagar o álbum {album}? Se o álbum for partilhado, os outros utilizadores não poderão aceder-lhe novamente.", - "album_delete_confirmation_description": "Se este álbum for partilhado, os outros utilizadores deixam de poder aceder.", + "album_delete_confirmation": "Tem a certeza de que quer eliminar o álbum {album}?", + "album_delete_confirmation_description": "Se este álbum for partilhado, os outros utilizadores deixam de o poder aceder.", "album_info_updated": "Informações do álbum atualizadas", "album_leave": "Sair do álbum?", - "album_leave_confirmation": "Tem a certeza que quer sair de {album}?", + "album_leave_confirmation": "Tem a certeza de que quer sair de {album}?", "album_name": "Nome do álbum", "album_options": "Opções de álbum", "album_remove_user": "Remover utilizador?", - "album_remove_user_confirmation": "Tem a certeza que quer remover {user}?", - "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores para o partilhar.", + "album_remove_user_confirmation": "Tem a certeza de que quer remover {user}?", + "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores com quem o partilhar.", "album_updated": "Álbum atualizado", - "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos arquivos", - "album_user_left": "Saída {album}", + "album_updated_setting_description": "Receber uma notificação por e-mail quando um álbum partilhado tiver novos ficheiros", + "album_user_left": "Saíu do {album}", "album_user_removed": "Utilizador {user} removido", - "album_with_link_access": "Permite acesso a fotos e pessoas deste album por qualquer pessoa com o link.", + "album_with_link_access": "Permite o acesso a fotos e pessoas deste álbum por qualquer pessoa com o link.", "albums": "Álbuns", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}", "all": "Todos", @@ -361,68 +371,69 @@ "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edições", - "allow_public_user_to_download": "Permit acesso de download ao user publico", - "allow_public_user_to_upload": "Permite acesso de upload ao user publico", + "allow_public_user_to_download": "Permitir que utilizadores públicos façam transferências", + "allow_public_user_to_upload": "Permitir que utilizadores públicos façam carregamentos", "anti_clockwise": "Sentido anti-horário", "api_key": "Chave de API", "api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.", - "api_key_empty": "O nome da API Key não pode ser vazio", + "api_key_empty": "O nome da chave a API não pode estar vazio", "api_keys": "Chaves de API", - "app_settings": "Configurações do Aplicativo", + "app_settings": "Definições da Aplicação", "appears_in": "Aparece em", "archive": "Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", "archive_size": "Tamanho do arquivo", - "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)", + "archive_size_description": "Configure o tamanho do arquivo para transferências (em GiB)", "archived": "Arquivado", "archived_count": "{count, plural, other {Arquivado #}}", - "are_these_the_same_person": "São a mesma pessoa?", - "are_you_sure_to_do_this": "Tem a certeza que quer fazer isto?", + "are_these_the_same_person": "Estas pessoas são a mesma pessoa?", + "are_you_sure_to_do_this": "Tem a certeza de que quer fazer isto?", "asset_added_to_album": "Adicionado ao álbum", "asset_adding_to_album": "A adicionar ao álbum...", - "asset_description_updated": "A descrição do arquivo foi atualizada", - "asset_filename_is_offline": "O arquivo {filename} está offline", - "asset_has_unassigned_faces": "O arquivo tem rostos sem atribuição", - "asset_hashing": "Hashing...", - "asset_offline": "Ativo off-line", - "asset_offline_description": "Este arquivo está offline. Immich não consegue acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e, em seguida, escaneie a biblioteca novamente.", + "asset_description_updated": "A descrição do ficheiro foi atualizada", + "asset_filename_is_offline": "O ficheiro {filename} não está disponível", + "asset_has_unassigned_faces": "O ficheiro tem rostos não atribuídas", + "asset_hashing": "A criar hash...", + "asset_offline": "Ficheiro indisponível", + "asset_offline_description": "Este ficheiro está indisponível. O Immich não consegue aceder ao local do local. Certifique-se de que o ficheiro está disponível e, em seguida, analise a biblioteca novamente.", "asset_skipped": "Ignorado", + "asset_skipped_in_trash": "Na reciclagem", "asset_uploaded": "Enviado", - "asset_uploading": "Em upload...", - "assets": "Arquivos", - "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}", - "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum", - "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} a {hasName, select, true {{name}} other {novo álbum}}", - "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}", + "asset_uploading": "A enviar...", + "assets": "Ficheiros", + "assets_added_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}}", + "assets_added_to_album_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} ao álbum", + "assets_added_to_name_count": "{count, plural, one {# ficheiro adicionado} other {# ficheiros adicionados}} a {hasName, select, true {{name}} other {novo álbum}}", + "assets_count": "{count, plural, one {# ficheiro} other {# ficheiros}}", "assets_moved_to_trash": "{count, plural, one {# ativo enviado} other {# ativos enviados}} para a lixeira", - "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira", - "assets_permanently_deleted_count": "{count, plural, one {# arquivo} other {# arquivos}} excluídos permanentemente", - "assets_removed_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}}", - "assets_restore_confirmation": "Tem a certeza que quer recuperar todos os artigos apagados? Não é possivel voltar atrás nesta acção!", - "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", - "assets_trashed_count": "{count, plural, one {# arquivo enviado} other {# arquivos enviados}} para a lixeira", - "assets_were_part_of_album_count": "{count, plural, one {Arquivo já era} other {Os arquivos já eram}} parte do álbum", + "assets_moved_to_trash_count": "{count, plural, one {# ficheiro movido} other {# aficheiros movidos}} para a reciclagem", + "assets_permanently_deleted_count": "{count, plural, one {# ficheiro} other {# ficheiros}} eliminados permanentemente", + "assets_removed_count": "{count, plural, one {# ficheiro eliminado} other {# ficheiros eliminados}}", + "assets_restore_confirmation": "Tem a certeza de que quer recuperar todos os ficheiros apagados? Não é possível anular esta ação!", + "assets_restored_count": "{count, plural, one {# ficheiro restaurado} other {# ficheiros restaurados}}", + "assets_trashed_count": "{count, plural, one {# ficheiro enviado} other {# ficheiros enviados}} para a reciclagem", + "assets_were_part_of_album_count": "{count, plural, one {O ficheiro já fazia} other {Os ficheiros já faziam}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Voltar", "back_close_deselect": "Voltar, fechar ou desmarcar", "backward": "Para trás", "birthdate_saved": "Data de nascimento guardada com sucesso", - "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.", + "birthdate_set_description": "A data de nascimento é utilizada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.", "blurred_background": "Fundo desfocado", - "build": "Construir", - "build_image": "Construir Imagem", - "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja excluir {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e exclui permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!", - "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", - "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", + "build": "Versão de compilação", + "build_image": "Imagem de compilação", + "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja eliminar {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Esta ação mantém o maior ficheiro de cada grupo e elimina permanentemente todos os outros duplicados. Não é possível anular esta ação!", + "bulk_keep_duplicates_confirmation": "Tem a certeza de que deseja manter {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto resolverá todos os grupos duplicados sem eliminar nada.", + "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a recicalgem {count, plural, one {# ficheiro duplicado} other {# ficheiros duplicados}}? Isto manterá o maior ficheiro de cada grupo e irá mover para a reciclagem todos os outros duplicados.", "buy": "Comprar Immich", - "camera": "Câmera", - "camera_brand": "Marca da câmera", - "camera_model": "Modelo da câmera", + "camera": "Câmara", + "camera_brand": "Marca da câmara", + "camera_model": "Modelo da câmara", "cancel": "Cancelar", "cancel_search": "Cancelar pesquisa", - "cannot_merge_people": "Não é possível mesclar pessoas", - "cannot_undo_this_action": "Não pode voltar atrás nesta ação!", - "cannot_update_the_description": "Não é possível atualizar a descrição", + "cannot_merge_people": "Não foi possível unir pessoas", + "cannot_undo_this_action": "Não é possível anular esta ação!", + "cannot_update_the_description": "Não foi possível atualizar a descrição", "cant_apply_changes": "Não é possível aplicar alterações", "cant_get_faces": "Não foi possível obter faces", "cant_search_people": "Não foi possível pesquisar pessoas", @@ -432,13 +443,13 @@ "change_location": "Alterar localização", "change_name": "Alterar nome", "change_name_successfully": "Nome alterado com sucesso", - "change_password": "Mudar a senha", - "change_password_description": "Esta é a primeira vez que você está entrando no sistema ou uma solicitação foi feita para alterar sua senha. Insira a nova senha abaixo.", - "change_your_password": "Alterar sua senha", + "change_password": "Alterar a palavra-passe", + "change_password_description": "Esta é a primeira vez que está a entrar no sistema ou um pedido foi feito para alterar a sua palavra-passe. Insira a nova palavra-passe abaixo.", + "change_your_password": "Alterar a sua palavra-passe", "changed_visibility_successfully": "Visibilidade alterada com sucesso", "check_all": "Verificar tudo", - "check_logs": "Verificar registros", - "choose_matching_people_to_merge": "Escolha pessoas correspondentes para mesclar", + "check_logs": "Verificar registos", + "choose_matching_people_to_merge": "Escolha pessoas correspondentes para unir", "city": "Cidade", "clear": "Limpar", "clear_all": "Limpar tudo", @@ -449,26 +460,27 @@ "close": "Fechar", "collapse": "Colapsar", "collapse_all": "Colapsar tudo", - "color_theme": "Tema de cores", + "color": "Cor", + "color_theme": "Esquema de cores", "comment_deleted": "Comentário eliminado", "comment_options": "Opções de comentário", "comments_and_likes": "Comentários e gostos", "comments_are_disabled": "Comentários estão desativados", "confirm": "Confirmar", - "confirm_admin_password": "Confirmar senha de administrador", - "confirm_delete_shared_link": "Tem certeza de que deseja excluir este link compartilhado?", - "confirm_password": "Confirme a senha", - "contain": "Caber", + "confirm_admin_password": "Confirmar palavra-passe de administrador", + "confirm_delete_shared_link": "Tem certeza de que deseja eliminar este link partilhado?", + "confirm_password": "Confirmar a palavra-passe", + "contain": "Ajustar", "context": "Contexto", "continue": "Continuar", "copied_image_to_clipboard": "Imagem copiada para a área de transferência.", "copied_to_clipboard": "Copiado para a área de transferência!", "copy_error": "Copiar erro", - "copy_file_path": "Copiar caminho do arquivo", + "copy_file_path": "Copiar caminho do ficheiro", "copy_image": "Copiar Imagem", "copy_link": "Copiar link", "copy_link_to_clipboard": "Copiar link para a área de transferência", - "copy_password": "Copiar senha", + "copy_password": "Copiar palavra-passe", "copy_to_clipboard": "Copiar para a área de transferência", "country": "País", "cover": "Preencher", @@ -478,15 +490,17 @@ "create_library": "Criar biblioteca", "create_link": "Criar link", "create_link_to_share": "Criar link para partilhar", - "create_link_to_share_description": "Permiter a visualização desta imagem(s) a qualquer pessoa com este link", + "create_link_to_share_description": "Permitir a visualização desta(s) imagem(s) a qualquer pessoa com o link", "create_new_person": "Criar nova pessoa", - "create_new_person_hint": "Associe os arquivos para uma nova pessoa", + "create_new_person_hint": "Associe os ficheiros a uma nova pessoa", "create_new_user": "Criar novo utilizador", + "create_tag": "Criar etiqueta", + "create_tag_description": "Criar uma nova etiqueta. Para etiquetas compostas, introduza o caminho completo, incluindo as barras.", "create_user": "Criar utilizador", "created": "Criado", "current_device": "Dispositivo atual", - "custom_locale": "Localização Customizada", - "custom_locale_description": "Formatar datas e números baseados na linguagem e região", + "custom_locale": "Localização Personalizada", + "custom_locale_description": "Formatar datas e números baseados na língua e na região", "dark": "Escuro", "date_after": "Data após", "date_and_time": "Data e Hora", @@ -494,19 +508,21 @@ "date_of_birth_saved": "Data de nascimento guardada com sucesso", "date_range": "Intervalo de datas", "day": "Dia", - "deduplicate_all": "Limpar todas Duplicidades", + "deduplicate_all": "Limpar todos os itens duplicados", "default_locale": "Localização Padrão", "default_locale_description": "Formatar datas e números baseados na linguagem do seu navegador", - "delete": "Excluir", - "delete_album": "Excluir álbum", - "delete_api_key_prompt": "Tem certeza de que deseja excluir esta chave de API?", - "delete_duplicates_confirmation": "Tem certeza de que deseja excluir permanentemente estas duplicidades?", - "delete_key": "Excluir chave", - "delete_library": "Excluir biblioteca", - "delete_link": "Excluir link", - "delete_shared_link": "Excluir link de compartilhamento", - "delete_user": "Excluir utilizador", - "deleted_shared_link": "Link de compartilhamento excluído", + "delete": "Eliminar", + "delete_album": "Eliminar álbum", + "delete_api_key_prompt": "Tem certeza de que deseja eliminar esta chave de API?", + "delete_duplicates_confirmation": "Tem certeza de que deseja eliminar permanentemente estes itens duplicados?", + "delete_key": "Eliminar chave", + "delete_library": "Eliminar biblioteca", + "delete_link": "Eliminar link", + "delete_shared_link": "Eliminar link de partilha", + "delete_tag": "Eliminar etiqueta", + "delete_tag_confirmation_prompt": "Tem a certeza de que pretende eliminar a etiqueta {tagName} ?", + "delete_user": "Eliminar utilizador", + "deleted_shared_link": "Link de partilha eliminado", "description": "Descrição", "details": "Detalhes", "direction": "Direção", @@ -518,19 +534,19 @@ "display_options": "Opções de exibição", "display_order": "Ordem de exibição", "display_original_photos": "Exibir fotos originais", - "display_original_photos_setting_description": "Prefira exibir a foto original ao visualizar um ativo em vez de miniaturas quando o ativo original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.", + "display_original_photos_setting_description": "Preferir a exibição da foto original ao visualizar um ficheiro em vez de miniaturas quando o ficheiro original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.", "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", "download": "Transferir", "download_include_embedded_motion_videos": "Vídeos incorporados", - "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um arquivo separado", + "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um ficheiro separado", "download_settings": "Transferir", - "download_settings_description": "Gerenciar configurações relacionadas a transferir ativos", - "downloading": "Baixando", - "downloading_asset_filename": "A transferir o arquivo {filename}", - "drop_files_to_upload": "Coloque os ficheiros em qualquer lugar para fazer o upload", - "duplicates": "Duplicados", - "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados", + "download_settings_description": "Gerir definições relacionadas com a transferência de ficheiros", + "downloading": "A transferir", + "downloading_asset_filename": "A transferir o ficheiro {filename}", + "drop_files_to_upload": "Solte os ficheiros em qualquer lugar para os enviar", + "duplicates": "Itens duplicados", + "duplicates_description": "Marque cada grupo indicando quais ficheiros, se algum, são duplicados", "duration": "Duração", "durations": { "days": "", @@ -541,11 +557,11 @@ }, "edit": "Editar", "edit_album": "Editar álbum", - "edit_avatar": "Editar foto de perfil", + "edit_avatar": "Editar imagem de perfil", "edit_date": "Editar data", "edit_date_and_time": "Editar data e hora", "edit_exclusion_pattern": "Editar o padrão de exclusão", - "edit_faces": "Editar faces", + "edit_faces": "Editar rostos", "edit_import_path": "Editar caminho de importação", "edit_import_paths": "Editar caminhos de importação", "edit_key": "Editar chave", @@ -553,150 +569,153 @@ "edit_location": "Editar Localização", "edit_name": "Editar nome", "edit_people": "Editar pessoas", + "edit_tag": "Editar etiqueta", "edit_title": "Editar Título", "edit_user": "Editar utilizador", "edited": "Editado", - "editor": "Editar", - "editor_close_without_save_prompt": "As alterações não serão salvas", + "editor": "Editor", + "editor_close_without_save_prompt": "As alterações não serão guardadas", "editor_close_without_save_title": "Fechar editor?", - "editor_crop_tool_h2_aspect_ratios": "Proporções de aspecto", + "editor_crop_tool_h2_aspect_ratios": "Relação de aspeto", "editor_crop_tool_h2_rotation": "Rotação", "email": "E-mail", "empty": "", "empty_album": "", - "empty_trash": "Esvaziar lixo", - "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a lixeira? Isso removerá todos os arquivos da lixeira do Immich permanentemente.\nVocê não pode desfazer esta ação!", + "empty_trash": "Esvaziar reciclagem", + "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a reciclagem? Isto removerá todos os ficheiros da reciclagem do Immich permanentemente.\nNão é possível anular esta ação!", "enable": "Ativar", "enabled": "Ativado", "end_date": "Data final", "error": "Erro", - "error_loading_image": "Erro ao carregar a página", + "error_loading_image": "Erro ao carregar a imagem", "error_title": "Erro - Algo correu mal", "errors": { - "cannot_navigate_next_asset": "Não pode navegar para o proximo artigo", - "cannot_navigate_previous_asset": "Não pode navegar para o artigo anterior", + "cannot_navigate_next_asset": "Não foi possível navegar para o próximo ficheiro", + "cannot_navigate_previous_asset": "Não foi possível navegar para o ficheiro anterior", "cant_apply_changes": "Não foi possível aplicar as alterações", - "cant_change_activity": "Não é possível {enabled, select, true {desativar} other {ativar}} atividade", - "cant_change_asset_favorite": "Não pode alterar o favorito deste artigo", - "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", + "cant_change_activity": "Não foi possível {enabled, select, true {desativar} other {ativar}} atividade", + "cant_change_asset_favorite": "Não foi possível alterar o favorito deste ficheiro", + "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# ficheiro} other {# ficheiros}}", "cant_get_faces": "Não foi possível obter os rostos", "cant_get_number_of_comments": "Não foi possível obter o número de comentários", "cant_search_people": "Não foi possível pesquisar pessoas", "cant_search_places": "Não foi possível pesquisar locais", - "cleared_jobs": "Trabalhos eliminados para: {job}", - "error_adding_assets_to_album": "Erro ao adicionar arquivos ao álbum", - "error_adding_users_to_album": "Erro a adicionar utilizador ao album", - "error_deleting_shared_user": "Error a apagar o utilizador partilhado", - "error_downloading": "Erro a transferir {filename}", + "cleared_jobs": "Tarefas eliminadas para: {job}", + "error_adding_assets_to_album": "Erro ao adicionar ficheiros ao álbum", + "error_adding_users_to_album": "Erro ao adicionar utilizador ao álbum", + "error_deleting_shared_user": "Erro ao apagar o utilizador partilhado", + "error_downloading": "Erro ao transferir {filename}", "error_hiding_buy_button": "Erro ao esconder botão de compra", - "error_removing_assets_from_album": "Erro a eliminar artigos do album, verifique a consola para mais detalhes", - "error_selecting_all_assets": "Erro ao selecionar todos os arquivos", + "error_removing_assets_from_album": "Erro ao eliminar ficheiros do álbum, verifique a consola para mais detalhes", + "error_selecting_all_assets": "Erro ao selecionar todos os ficheiros", "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.", - "failed_job_command": "Comando {command} falhou para o trabalho: {job}", - "failed_to_create_album": "Falha ao criar álbum", - "failed_to_create_shared_link": "Falhou a criar um link partilhado", - "failed_to_edit_shared_link": "Falhou a editar o link partilhado", - "failed_to_get_people": "Falha na obtenção de pessoas", - "failed_to_load_asset": "Falha ao carregar arquivo", - "failed_to_load_assets": "Falha ao carregar arquivos", - "failed_to_load_people": "Falha ao carregar pessoas", - "failed_to_remove_product_key": "Falha ao remover chave de produto", - "failed_to_stack_assets": "Falha ao empilhar os arquivos", - "failed_to_unstack_assets": "Falha ao desempilhar arquivos", + "failed_job_command": "Comando {command} falhou para a tarefa: {job}", + "failed_to_create_album": "Não foi possível criar álbum", + "failed_to_create_shared_link": "Não foi possível criar o link partilhado", + "failed_to_edit_shared_link": "Não foi possível editar o link partilhado", + "failed_to_get_people": "Não foi possível obter pessoas", + "failed_to_load_asset": "Não foi possível ler o ficheiro", + "failed_to_load_assets": "Não foi possível ler ficheiros", + "failed_to_load_people": "Não foi possível carregar pessoas", + "failed_to_remove_product_key": "Não foi possível remover chave de produto", + "failed_to_stack_assets": "Não foi possível empilhar os ficheiros", + "failed_to_unstack_assets": "Não foi possível desempilhar ficheiros", "import_path_already_exists": "Este caminho de importação já existe.", - "incorrect_email_or_password": "Email ou password incorretos", - "paths_validation_failed": "a validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}", - "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixels transparentes. Por favor faça zoom in e/ou mova a imagem.", - "quota_higher_than_disk_size": "Você definiu uma cota maior do que o tamanho do disco", + "incorrect_email_or_password": "Email ou palavra-passe incorretos", + "paths_validation_failed": "A validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}", + "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixeis transparentes. Por favor amplie e/ou mova a imagem.", + "quota_higher_than_disk_size": "Definiu uma quota maior do que o tamanho do disco", "repair_unable_to_check_items": "Não foi possível verificar {count, select, one {um item} other {alguns itens}}", "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum", - "unable_to_add_assets_to_shared_link": "Não foi possivel adicionar os artigos ao link partilhado", + "unable_to_add_assets_to_shared_link": "Não foi possível adicionar os ficheiros ao link partilhado", "unable_to_add_comment": "Não foi possível adicionar o comentário", "unable_to_add_exclusion_pattern": "Não foi possível adicionar o padrão de exclusão", "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação", "unable_to_add_partners": "Não foi possível adicionar parceiros", - "unable_to_add_remove_archive": "Não é possível {archived, select, true {remover o arquivo de} other {adicionar o arquivo}}", - "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar arquivo aos} other {remover arquivo dos}} favoritos", - "unable_to_archive_unarchive": "Não é possível {archived, select, true {arquivar} other {desarquivar}}", + "unable_to_add_remove_archive": "Não foi possível {archived, select, true {remover o ficheiro de} other {adicionar o ficheiro}}", + "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar ficheiro aos} other {remover ficheiro dos}} favoritos", + "unable_to_archive_unarchive": "Não foi possível {archived, select, true {arquivar} other {desarquivar}}", "unable_to_change_album_user_role": "Não foi possível alterar a permissão do utilizador no álbum", "unable_to_change_date": "Não foi possível alterar a data", - "unable_to_change_favorite": "Não foi possivel mudar o favorito do artigo", + "unable_to_change_favorite": "Não foi possível mudar o favorito do ficheiro", "unable_to_change_location": "Não foi possível alterar a localização", - "unable_to_change_password": "Não foi possível alterar a senha", + "unable_to_change_password": "Não foi possível alterar a palavra-passe", "unable_to_change_visibility": "Não é possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}", "unable_to_check_item": "", "unable_to_check_items": "", - "unable_to_complete_oauth_login": "Não foi possível completar início de sessão com OAuth", - "unable_to_connect": "Não é possível conectar", + "unable_to_complete_oauth_login": "Não foi possível completar o início de sessão com OAuth", + "unable_to_connect": "Não é possível ligar", "unable_to_connect_to_server": "Não foi possível ligar ao servidor", - "unable_to_copy_to_clipboard": "Não é possível copiar para a área de transferência, certifique-se que está acessando a pagina através de https", + "unable_to_copy_to_clipboard": "Não foi possível copiar para a área de transferência, certifique-se de que está a aceder à pagina através de https", "unable_to_create_admin_account": "Não foi possível criar conta de administrador", "unable_to_create_api_key": "Não foi possível criar uma nova Chave de API", "unable_to_create_library": "Não foi possível criar a biblioteca", "unable_to_create_user": "Não foi possível criar o utilizador", - "unable_to_delete_album": "Não foi possível deletar o álbum", - "unable_to_delete_asset": "Não foi possível deletar o ativo", - "unable_to_delete_assets": "Erro ao eliminar arquivos", - "unable_to_delete_exclusion_pattern": "Não foi possível deletar o padrão de exclusão", - "unable_to_delete_import_path": "Não foi possível deletar o caminho de importação", - "unable_to_delete_shared_link": "Não foi possível deletar o link compartilhado", - "unable_to_delete_user": "Não foi possível deletar o utilizador", + "unable_to_delete_album": "Não foi possível eliminar o álbum", + "unable_to_delete_asset": "Não foi possível eliminar o ficheiro", + "unable_to_delete_assets": "Erro ao eliminar ficheiros", + "unable_to_delete_exclusion_pattern": "Não foi possível eliminar o padrão de exclusão", + "unable_to_delete_import_path": "Não foi possível eliminar o caminho de importação", + "unable_to_delete_shared_link": "Não foi possível eliminar o link compartilhado", + "unable_to_delete_user": "Não foi possível eliminar o utilizador", "unable_to_download_files": "Não foi possível transferir ficheiros", "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão", "unable_to_edit_import_path": "Não foi possível editar o caminho de importação", - "unable_to_empty_trash": "Não foi possível esvaziar a lixeira", - "unable_to_enter_fullscreen": "Não foi possível entrar em modo de tela cheia", - "unable_to_exit_fullscreen": "Não foi possível sair do modo de tela cheia", + "unable_to_empty_trash": "Não foi possível esvaziar a reciclagem", + "unable_to_enter_fullscreen": "Não foi possível entrar em modo de ecrã inteiro", + "unable_to_exit_fullscreen": "Não foi possível sair do modo de ecrã inteiro", "unable_to_get_comments_number": "Não foi possível obter número de comentários", - "unable_to_get_shared_link": "Falha ao obter link compartilhado", + "unable_to_get_shared_link": "Não foi possível obter link partilhado", "unable_to_hide_person": "Não foi possível esconder a pessoa", + "unable_to_link_motion_video": "Não foi possível relacionar o video animado", "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth", "unable_to_load_album": "Não foi possível carregar o álbum", - "unable_to_load_asset_activity": "Não foi possível carregar as atividades do ativo", - "unable_to_load_items": "Não foi possível carregar os items", - "unable_to_load_liked_status": "Não foi possível carregar os status de gostei", + "unable_to_load_asset_activity": "Não foi possível carregar a atividade do ficheiro", + "unable_to_load_items": "Não foi possível carregar os itens", + "unable_to_load_liked_status": "Não foi possível carregar o estado de gostos", "unable_to_log_out_all_devices": "Não foi possível terminar a sessão em todos os dispositivos", "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo", "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", - "unable_to_reassign_assets_existing_person": "Não é possível reatribuir arquivos para {name, select, null {uma pessoa existente} other {{name}}}", - "unable_to_reassign_assets_new_person": "Não é possível reatribuir os arquivos a uma nova pessoa", - "unable_to_refresh_user": "Não foi possível atualizar o utilizador", + "unable_to_reassign_assets_existing_person": "Não foi possível reatribuir ficheiros para {name, select, null {uma pessoa existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "Não foi possível reatribuir os ficheiros a uma nova pessoa", + "unable_to_refresh_user": "Não foi possível recarregar o utilizador", "unable_to_remove_album_users": "Não foi possível remover utilizador do álbum", - "unable_to_remove_api_key": "Não foi possível a Chave de API", - "unable_to_remove_assets_from_shared_link": "Não é possível remover os arquivos do link compartilhado", + "unable_to_remove_api_key": "Não foi possível remover a Chave de API", + "unable_to_remove_assets_from_shared_link": "Não foi possível remover os ficheiros do link partilhado", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Não foi possível remover arquivos offline", "unable_to_remove_library": "Não foi possível remover a biblioteca", - "unable_to_remove_offline_files": "Não foi possível remover arquivos offline", "unable_to_remove_partner": "Não foi possível remover parceiro", "unable_to_remove_reaction": "Não foi possível remover a reação", "unable_to_remove_user": "", "unable_to_repair_items": "Não foi possível reparar os itens", - "unable_to_reset_password": "Não foi possível resetar a senha", - "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade", - "unable_to_restore_assets": "Não foi possível restaurar arquivos", - "unable_to_restore_trash": "Não foi possível restaurar itens da lixeira", + "unable_to_reset_password": "Não foi possível redefinir a palavra-passe", + "unable_to_resolve_duplicate": "Não foi possível resolver as duplicidades", + "unable_to_restore_assets": "Não foi possível restaurar ficheiros", + "unable_to_restore_trash": "Não foi possível restaurar itens da reciclagem", "unable_to_restore_user": "Não foi possível restaurar utilizador", - "unable_to_save_album": "Não foi possível salvar o álbum", - "unable_to_save_api_key": "Não foi possível salvar a Chave de API", + "unable_to_save_album": "Não foi possível guardar o álbum", + "unable_to_save_api_key": "Não foi possível guardar a Chave de API", "unable_to_save_date_of_birth": "Não foi possível guardar a data de nascimento", - "unable_to_save_name": "Não foi possível salvar o nome", - "unable_to_save_profile": "Não foi possível salvar o perfil", - "unable_to_save_settings": "Não foi possível salvar as configurações", - "unable_to_scan_libraries": "Não foi possível escanear as bibliotecas", - "unable_to_scan_library": "Não foi possível escanear a biblioteca", - "unable_to_set_feature_photo": "Não é possível definir a foto do recurso", + "unable_to_save_name": "Não foi possível guardar o nome", + "unable_to_save_profile": "Não foi possível guardar o perfil", + "unable_to_save_settings": "Não foi possível guardar as definições", + "unable_to_scan_libraries": "Não foi possível analisar as bibliotecas", + "unable_to_scan_library": "Não foi possível analisar a biblioteca", + "unable_to_set_feature_photo": "Não foi possível definir a foto de destaque", "unable_to_set_profile_picture": "Não foi possível definir a foto de perfil", - "unable_to_submit_job": "Não foi possível enviar o trabalho", - "unable_to_trash_asset": "Não foi possível enviar o ativo para a lixeira", + "unable_to_submit_job": "Não foi possível enviar a tarefa", + "unable_to_trash_asset": "Não foi possível enviar o ficheiro para a reciclagem", "unable_to_unlink_account": "Não foi possível desvincular conta", + "unable_to_unlink_motion_video": "Não foi possível remover a relação com o video animado", "unable_to_update_album_cover": "Não foi possível atualizar a capa do álbum", "unable_to_update_album_info": "Não foi possível atualizar informações do álbum", "unable_to_update_library": "Não foi possível atualizar a biblioteca", "unable_to_update_location": "Não foi possível atualizar a localização", - "unable_to_update_settings": "Não foi possível atualizar as configurações", + "unable_to_update_settings": "Não foi possível atualizar as definições", "unable_to_update_timeline_display_status": "Não foi possível atualizar o modo de visualização da linha do tempo", - "unable_to_update_user": "Não foi possível atualizar o usuário", + "unable_to_update_user": "Não foi possível atualizar o utilizador", "unable_to_upload_file": "Não foi possível carregar o ficheiro" }, "every_day_at_onepm": "", @@ -706,7 +725,7 @@ "exif": "Exif", "exit_slideshow": "Sair da apresentação", "expand_all": "Expandir tudo", - "expire_after": "Expira depois", + "expire_after": "Expira depois de", "expired": "Expirou", "expires_date": "Expira em {date}", "explore": "Explorar", @@ -719,38 +738,41 @@ "face_unassigned": "Sem atribuição", "failed_to_get_people": "Falha ao carregar as pessoas", "favorite": "Favorito", - "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita", + "favorite_or_unfavorite_photo": "Marcar ou desmarcar a foto como favorita", "favorites": "Favoritos", "feature": "", "feature_photo_updated": "Foto principal atualizada", "featurecollection": "", - "file_name": "Nome do arquivo", - "file_name_or_extension": "Nome do arquivo ou extensão", - "filename": "Nome do arquivo", + "features": "Funcionalidades", + "features_setting_description": "Configurar as funcionalidades da aplicação", + "file_name": "Nome do ficheiro", + "file_name_or_extension": "Nome do ficheiro ou extensão", + "filename": "Nome do ficheiro", "files": "", - "filetype": "Tipo de arquivo", + "filetype": "Tipo de ficheiro", "filter_people": "Filtrar pessoas", - "find_them_fast": "Encontre pelo nome em uma pesquisa", + "find_them_fast": "Encontre-as mais rapidamente pelo nome numa pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", "folders": "Pastas", - "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca", - "forward": "Para frente", + "folders_feature_description": "A navegar na vista de pastas pelas fotos e vídeos no sistema de ficheiros", + "force_re-scan_library_files": "Forçar uma nova análise de todos os ficheiros da biblioteca", + "forward": "Para a frente", "general": "Geral", "get_help": "Obter Ajuda", - "getting_started": "Primeiros passos", - "go_back": "Voltar", + "getting_started": "Primeiros Passos", + "go_back": "Regressar", "go_to_search": "Ir para a pesquisa", "go_to_share_page": "Ir para a página de compartilhamento", "group_albums_by": "Agrupar álbuns por...", "group_no": "Sem agrupamento", "group_owner": "Agrupar por dono", "group_year": "Agrupar por ano", - "has_quota": "Há cota", + "has_quota": "Tem quota", "hi_user": "Olá {name} ({email})", "hide_all_people": "Ocultar todas as pessoas", "hide_gallery": "Ocultar galeria", "hide_named_person": "Ocultar pessoa {name}", - "hide_password": "Ocultar senha", + "hide_password": "Ocultar palavra-passe", "hide_person": "Ocultar pessoa", "hide_unnamed_people": "Ocultar pessoas sem nome", "host": "Host", @@ -767,33 +789,33 @@ "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}", "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}", "img": "", - "immich_logo": "Logo do Immich", - "immich_web_interface": "Interface web do Immich", - "import_from_json": "Importar do JSON", + "immich_logo": "Logotipo do Immich", + "immich_web_interface": "Interface Web do Immich", + "import_from_json": "Importar a partir de JSON", "import_path": "Caminho de importação", "in_albums": "Em {count, plural, one {# álbum} other {# álbuns}}", "in_archive": "Arquivado", "include_archived": "Incluir arquivados", - "include_shared_albums": "Incluir álbuns compartilhados", - "include_shared_partner_assets": "Incluir arquivos compartilhados por parceiros", - "individual_share": "Compartilhamento único", + "include_shared_albums": "Incluir álbuns partilhados", + "include_shared_partner_assets": "Incluir ficheiros partilhados por parceiros", + "individual_share": "Partilha individual", "info": "Informações", "interval": { - "day_at_onepm": "Todo dia, 1pm", + "day_at_onepm": "Todos os dias, às 13:00", "hours": "A cada {hours, plural, one {hora} other {{hours, number} horas}}", - "night_at_midnight": "Toda noite, meia noite", - "night_at_twoam": "Toda noite, 2am" + "night_at_midnight": "Todas as noites, à meia noite", + "night_at_twoam": "Todas as noites, às 02:00" }, "invite_people": "Convidar Pessoas", "invite_to_album": "Convidar para o álbum", "items_count": "{count, plural, one {item #} other {itens #}}", "job_settings_description": "", - "jobs": "Trabalhos", + "jobs": "Tarefas", "keep": "Manter", "keep_all": "Manter Todos", "keyboard_shortcuts": "Atalhos do teclado", "language": "Idioma", - "language_setting_description": "Selecione seu Idioma preferido", + "language_setting_description": "Selecione o seu Idioma preferido", "last_seen": "Visto pela ultima vez", "latest_version": "Versão mais recente", "latitude": "Latitude", @@ -803,64 +825,65 @@ "library": "Biblioteca", "library_options": "Opções da biblioteca", "light": "Claro", - "like_deleted": "Curtida removida", + "like_deleted": "Gosto removido", + "link_motion_video": "Relacionar video animado", "link_options": "Opções do Link", "link_to_oauth": "Link do OAuth", - "linked_oauth_account": "Conta OAuth Vinculada", + "linked_oauth_account": "Conta OAuth Associada", "list": "Lista", - "loading": "Carregando", - "loading_search_results_failed": "Falha ao carregar os resultados da pesquisa", + "loading": "A Carregar", + "loading_search_results_failed": "Não foi possível carregar os resultados da pesquisa", "log_out": "Sair", - "log_out_all_devices": "Sair de todos dispositivos", + "log_out_all_devices": "Terminar a sessão de todos os dispositivos", "logged_out_all_devices": "Sessão terminada em todos os dispositivos", "logged_out_device": "Sessão terminada no dispositivo", "login": "Iniciar sessão", - "login_has_been_disabled": "Login foi desativado.", - "logout_all_device_confirmation": "Tem certeza de que deseja desconectar todos os dispositivos?", - "logout_this_device_confirmation": "Tem certeza de que deseja sair deste dispositivo?", + "login_has_been_disabled": "Início de sessão foi desativado.", + "logout_all_device_confirmation": "Tem certeza de que deseja terminar a sessão em todos os dispositivos?", + "logout_this_device_confirmation": "Tem certeza de que deseja terminar a sessão deste dispositivo?", "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", - "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibição.", + "loop_videos_description": "Ativar para repetir os vídeos automaticamente durante a exibição.", "make": "Marca", "manage_shared_links": "Gerir links partilhados", - "manage_sharing_with_partners": "Gerenciar compartilhamento com parceiros", - "manage_the_app_settings": "Gerenciar configurações do app", - "manage_your_account": "Gerenciar sua conta", - "manage_your_api_keys": "Gerenciar suas Chaves de API", - "manage_your_devices": "Gerenciar seus dispositivos logados", - "manage_your_oauth_connection": "Gerenciar sua conexão OAuth", + "manage_sharing_with_partners": "Gerir partilha com parceiros", + "manage_the_app_settings": "Gerir definições da aplicação", + "manage_your_account": "Gerir a sua conta", + "manage_your_api_keys": "Gerir as suas Chaves de API", + "manage_your_devices": "Gerir os seus dispositivos com sessão iniciada", + "manage_your_oauth_connection": "Gerir a sua ligação ao OAuth", "map": "Mapa", "map_marker_for_images": "Marcador no mapa para fotos tiradas em {city}, {country}", "map_marker_with_image": "Marcador de mapa com imagem", "map_settings": "Definições do mapa", "matches": "Correspondências", - "media_type": "Tipo de mídia", + "media_type": "Tipo de média", "memories": "Memórias", - "memories_setting_description": "Gerencie o que vê em suas memórias", + "memories_setting_description": "Gerir o que vê nas suas memórias", "memory": "Memória", "memory_lane_title": "Memórias {title}", "menu": "Menu", - "merge": "Mesclar", - "merge_people": "Mesclar pessoas", - "merge_people_limit": "Só é possível mesclar até 5 faces de uma só vez", - "merge_people_prompt": "Tem certeza que deseja mesclar estas pessoas? Esta ação é irreversível.", - "merge_people_successfully": "Pessoas mescladas com sucesso", - "merged_people_count": "Mesclada {count, plural, one {1 pessoa} other {# pessoas}}", + "merge": "Unir", + "merge_people": "Unir pessoas", + "merge_people_limit": "Só é possível unir até 5 rostos de cada vez", + "merge_people_prompt": "Tem a certeza de que deseja unir estas pessoas? Esta ação é irreversível.", + "merge_people_successfully": "Pessoas unidas com sucesso", + "merged_people_count": "Unidas {count, plural, one {# pessoa} other {# pessoas}}", "minimize": "Minimizar", "minute": "Minuto", - "missing": "Faltando", + "missing": "Em falta", "model": "Modelo", "month": "Mês", "more": "Mais", - "moved_to_trash": "Enviado para a lixeira", - "my_albums": "Meus Álbuns", + "moved_to_trash": "Enviado para a reciclagem", + "my_albums": "Os meus álbuns", "name": "Nome", - "name_or_nickname": "Nome ou apelido", + "name_or_nickname": "Nome ou alcunha", "never": "Nunca", "new_album": "Novo Álbum", "new_api_key": "Nova Chave de API", - "new_password": "Nova senha", + "new_password": "Nova palavra-passe", "new_person": "Nova Pessoa", "new_user_created": "Novo utilizador criado", "new_version_available": "NOVA VERSÃO DISPONÍVEL", @@ -868,48 +891,48 @@ "next": "Avançar", "next_memory": "Próxima memória", "no": "Não", - "no_albums_message": "Crie um álbum para organizar suas fotos e vídeos", - "no_albums_with_name_yet": "Parece que você ainda não tem nenhum álbum com este nome.", - "no_albums_yet": "Parece que você ainda não tem nenhum álbum.", + "no_albums_message": "Crie um álbum para organizar as suas fotos e vídeos", + "no_albums_with_name_yet": "Parece que ainda não tem nenhum álbum com este nome.", + "no_albums_yet": "Parece que ainda não tem nenhum álbum.", "no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualização de fotos", - "no_assets_message": "CLIQUE PARA CARREGAR SUA PRIMEIRA FOTO", - "no_duplicates_found": "Nenhuma duplicidade foi encontrada.", + "no_assets_message": "FAÇA CLIQUE PARA CARREGAR A SUA PRIMEIRA FOTO", + "no_duplicates_found": "Nenhum item duplicado foi encontrado.", "no_exif_info_available": "Sem informações exif disponíveis", - "no_explore_results_message": "Carregue mais fotos para explorar sua coleção.", - "no_favorites_message": "Adicione aos favoritos para encontrar suas melhores fotos e vídeos rapidamente", - "no_libraries_message": "Crie uma biblioteca externa para ver suas fotos e vídeos", + "no_explore_results_message": "Carregue mais fotos para explorar a sua coleção.", + "no_favorites_message": "Adicione aos favoritos para encontrar as suas melhores fotos e vídeos rapidamente", + "no_libraries_message": "Crie uma biblioteca externa para ver as suas fotos e vídeos", "no_name": "Sem nome", "no_places": "Sem lugares", "no_results": "Sem resultados", - "no_results_description": "Tente um sinônimo ou uma palavra-chave mais comum", - "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede", - "not_in_any_album": "Fora de álbum", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", - "note_unlimited_quota": "Nota: Digite 0 para cota ilimitada", + "no_results_description": "Tente um sinónimo ou uma palavra-chave mais comum", + "no_shared_albums_message": "Crie um álbum para partilhar fotos e vídeos com pessoas na sua rede", + "not_in_any_album": "Não está em nenhum álbum", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o Rótulo de Armazenamento a ficheiros carregados anteriormente, execute o", + "note_unlimited_quota": "Nota: Escreva 0 para quota ilimitada", "notes": "Notas", - "notification_toggle_setting_description": "Habilitar notificações por e-mail", + "notification_toggle_setting_description": "Ativar notificações por e-mail", "notifications": "Notificações", - "notifications_setting_description": "Gerenciar notificações", + "notifications_setting_description": "Gerir notificações", "oauth": "OAuth", "offline": "Offline", "offline_paths": "Caminhos offline", - "offline_paths_description": "Estes resultados podem ser devidos a arquivos deletados manualmente e que não são parte de uma biblioteca externa.", + "offline_paths_description": "Estes resultados podem ser devidos a ficheiros eliminados manualmente e que não fazem parte de uma biblioteca externa.", "ok": "Ok", "oldest_first": "Mais antigo primeiro", "onboarding": "Integração", - "onboarding_privacy_description": "Os seguintes recursos (opcionais) dependem de serviços externos e podem ser desabilitados a qualquer momento nas configurações de administração.", - "onboarding_theme_description": "Escolha um tema de cor para sua instância. Você pode alterar isso mais tarde em suas configurações.", - "onboarding_welcome_description": "Vamos configurar sua instância com algumas configurações comuns.", + "onboarding_privacy_description": "As seguintes funcionalidades opcionais dependem de serviços externos e podem ser desativados a qualquer momento nas definições de administração.", + "onboarding_theme_description": "Escolha um tema de cor para sua instância. Pode alterar isto mais tarde nas suas definições.", + "onboarding_welcome_description": "Vamos configurar a sua instância com algumas definições comuns.", "onboarding_welcome_user": "Bem-vindo(a), {user}", "online": "Online", - "only_favorites": "Somente favoritos", - "only_refreshes_modified_files": "Somente atualize arquivos modificados", - "open_in_map_view": "Abrir na visualização do mapa", + "only_favorites": "Apenas favoritos", + "only_refreshes_modified_files": "Apenas recarrega ficheiros modificados", + "open_in_map_view": "Abrir na visualização de mapa", "open_in_openstreetmap": "Abrir no OpenStreetMap", - "open_the_search_filters": "Abre os filtros de pesquisa", + "open_the_search_filters": "Abrir os filtros de pesquisa", "options": "Opções", "or": "ou", - "organize_your_library": "Organize sua biblioteca", + "organize_your_library": "Organizar a sua biblioteca", "original": "original", "other": "Outro", "other_devices": "Outros dispositivos", @@ -917,15 +940,15 @@ "owned": "Seu", "owner": "Dono", "partner": "Parceiro", - "partner_can_access": "{partner} pode acessar", - "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Excluídos", + "partner_can_access": "{partner} pode aceder", + "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Eliminados", "partner_can_access_location": "A localização onde as fotos foram tiradas", - "partner_sharing": "Compartilhamento com Parceiro", + "partner_sharing": "Partilha com Parceiro", "partners": "Parceiros", - "password": "Senha", - "password_does_not_match": "As senhas não são iguais", - "password_required": "A senha é obrigatório", - "password_reset_success": "Senha resetada com sucesso", + "password": "Palavra-passe", + "password_does_not_match": "As palavras-passe não condizem", + "password_required": "A palavra-passe é obrigatória", + "password_reset_success": "Palavra-passe redefinida com sucesso", "past_durations": { "days": "{days, plural, one {Último dia} other {# últimos dias}}", "hours": "Últimas {hours, plural, one {horas} other {# horas}}", @@ -933,25 +956,26 @@ }, "path": "Caminho", "pattern": "Padrão", - "pause": "Interromper", - "pause_memories": "Interromper memórias", - "paused": "Interrompido", + "pause": "Pausa", + "pause_memories": "Pausar memórias", + "paused": "Em Pausa", "pending": "Pendente", "people": "Pessoas", "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}", - "people_sidebar_description": "Exibe o link Pessoas na barra lateral", + "people_feature_description": "A navegar fotos e vídeos agrupados por pessoas", + "people_sidebar_description": "Exibir o link Pessoas na barra lateral", "perform_library_tasks": "", - "permanent_deletion_warning": "Aviso para deletar permanentemente", - "permanent_deletion_warning_setting_description": "Exibe um aviso ao excluir arquivos de forma permanente", - "permanently_delete": "Deletar permanentemente", - "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {arquivo} other {arquivos}}", - "permanently_delete_assets_prompt": "Tem certeza que deseja excluir permanentemente {count, plural, one {esse arquivo?} other {estes # arquivos?}} Essa ação também removerá {count, plural, one {isto do} other {isto dos}} álbum(s).", - "permanently_deleted_asset": "Ativo deletado permanentemente", + "permanent_deletion_warning": "Aviso de eliminação permanente", + "permanent_deletion_warning_setting_description": "Exibir um aviso ao eliminar ficheiros de forma permanente", + "permanently_delete": "Eliminar permanentemente", + "permanently_delete_assets_count": "Eliminar permanentemente {count, plural, one {ficheiro} other {ficheiros}}", + "permanently_delete_assets_prompt": "Tem a certeza de que deseja eliminar permanentemente {count, plural, one {este ficheiro?} other {estes # ficheiros?}} Esta ação também removerá {count, plural, one {isto do álbum} other {isto dos álbuns}}.", + "permanently_deleted_asset": "Ficheiro eliminado permanentemente", "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente", - "permanently_deleted_assets_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}} permanentemente", + "permanently_deleted_assets_count": "{count, plural, one {# Ficheiro eliminado} other {# Ficheiros eliminados}} permanentemente", "person": "Pessoa", "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", - "photo_shared_all_users": "Parece que você compartilhou suas fotos com todos os usuários ou não tem nenhum usuário para compartilhar.", + "photo_shared_all_users": "Parece que partilhou as suas fotos com todos os utilizadores ou não tem nenhum utilizador para partilhar.", "photos": "Fotos", "photos_and_videos": "Fotos & Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", @@ -975,179 +999,183 @@ "profile_image_of_user": "Imagem de perfil de {user}", "profile_picture_set": "Foto de perfil definida.", "public_album": "Álbum público", - "public_share": "Compartilhar Publicamente", - "purchase_account_info": "Apoiador", + "public_share": "Partilhar Publicamente", + "purchase_account_info": "Apoiante", "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto", "purchase_activated_time": "Ativado em {date, date}", - "purchase_activated_title": "Sua chave foi ativada com sucesso", + "purchase_activated_title": "A sua chave foi ativada com sucesso", "purchase_button_activate": "Ativar", "purchase_button_buy": "Comprar", "purchase_button_buy_immich": "Comprar Immich", - "purchase_button_never_show_again": "Nunca mostrar novamente", + "purchase_button_never_show_again": "Não mostrar de novo", "purchase_button_reminder": "Relembrar-me daqui a 30 dias", "purchase_button_remove_key": "Remover chave", "purchase_button_select": "Selecionar", - "purchase_failed_activation": "Falha ao ativar! Verifique seu e-mail para obter a chave de produto correta!", - "purchase_individual_description_1": "Para uma pessoa", - "purchase_individual_description_2": "Status de apoiador", + "purchase_failed_activation": "Não foi possível ativar! Verifique o seu e-mail para obter a chave de produto correta!", + "purchase_individual_description_1": "Para uma pessoa individual", + "purchase_individual_description_2": "Status de apoiante", "purchase_individual_title": "Particular", "purchase_input_suggestion": "Tem uma chave de produto? Insira a chave abaixo", - "purchase_license_subtitle": "Compre Immich para apoiar o desenvolvimento contínuo do serviço", + "purchase_license_subtitle": "Compre o Immich para apoiar o desenvolvimento contínuo do serviço", "purchase_lifetime_description": "Compra vitalícia", "purchase_option_title": "OPÇÕES DE COMPRA", "purchase_panel_info_1": "O desenvolvimento do Immich requer muito tempo e esforço, e temos engenheiros a tempo inteiro a trabalhar nele para melhorá-lo quanto possível. A nossa missão é para que o software de código aberto e práticas de negócio éticas se tornem numa fonte de rendimento sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade dos utilizadores e que ofereça alternativas reais a serviços cloud explorativos.", - "purchase_panel_info_2": "Como estamos comprometidos em não adicionar acesso pago, esta compra não lhe dará nenhum recurso adicional no Immich. Contamos com usuários como você para dar suporte ao desenvolvimento contínuo do Immich.", + "purchase_panel_info_2": "Como estamos comprometidos a não adicionar acesso pago, esta compra não lhe dará acesso a nenhuma funcionalidade adicional do Immich. Contamos com utilizadores como você para dar suporte ao desenvolvimento contínuo do Immich.", "purchase_panel_title": "Apoie o projeto", "purchase_per_server": "Por servidor", "purchase_per_user": "Por utilizador", "purchase_remove_product_key": "Remover chave de produto", - "purchase_remove_product_key_prompt": "Tem certeza de que deseja remover a chave do produto?", + "purchase_remove_product_key_prompt": "Tem a certeza de que deseja remover a chave do produto?", "purchase_remove_server_product_key": "Remover chave do produto do servidor", - "purchase_remove_server_product_key_prompt": "Tem certeza de que deseja remover a chave do produto do servidor?", + "purchase_remove_server_product_key_prompt": "Tem a certeza de que deseja remover a chave do produto do servidor?", "purchase_server_description_1": "Para o servidor inteiro", - "purchase_server_description_2": "Status de apoiador", + "purchase_server_description_2": "Status de apoiante", "purchase_server_title": "Servidor", - "purchase_settings_server_activated": "A chave de produto para servidor é gerida pelo administrador", + "purchase_settings_server_activated": "A chave de produto do servidor é gerida pelo administrador", "range": "", "rating": "Classificação por estrelas", "rating_clear": "Limpar classificação", - "rating_count": "{contar, plural, um {# estrela} outro {# estrelas}}", - "rating_description": "Exibir a classificação exif no painel de informações", + "rating_count": "{count, plural, one {# estrela} other {# estrelas}}", + "rating_description": "Mostrar a classificação EXIF no painel de informações", "raw": "", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", "reassign": "Reatribuir", - "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} PARA {name, select, null {uma pessoa existente} other {{name}}}", - "reassigned_assets_to_new_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} a uma nova pessoa", - "reassing_hint": "Atribuir ativos selecionados a uma pessoa existente", - "recent": "Recente", + "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# ficheiro} other {# ficheiros}} para {name, select, null {uma pessoa existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reatribuído {count, plural, one {# ficheiro} other {# ficheiros}} a uma nova pessoa", + "reassing_hint": "Atribuir ficheiros selecionados a uma pessoa existente", + "recent": "Recentes", "recent_searches": "Pesquisas recentes", "refresh": "Atualizar", "refresh_encoded_videos": "Atualizar vídeos codificados", "refresh_metadata": "Atualizar metadados", "refresh_thumbnails": "Atualizar miniaturas", "refreshed": "Atualizado", - "refreshes_every_file": "Atualiza todos arquivos", - "refreshing_encoded_video": "Atualizando vídeo codificado", + "refreshes_every_file": "Atualiza todos os ficheiros", + "refreshing_encoded_video": "A atualizar vídeo codificado", "refreshing_metadata": "A atualizar metadados", "regenerating_thumbnails": "A atualizar miniaturas", "remove": "Remover", - "remove_assets_album_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} do álbum?", - "remove_assets_shared_link_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?", - "remove_assets_title": "Remover arquivos?", + "remove_assets_album_confirmation": "Tem a certeza de que deseja remover {count, plural, one {# ficheiro} other {# ficheiros}} do álbum?", + "remove_assets_shared_link_confirmation": "Tem certeza de que deseja remover {count, plural, one {# ficheiro} other {# ficheiros}} deste link partilhado?", + "remove_assets_title": "Remover ficheiros?", "remove_custom_date_range": "Remover intervalo de datas personalizado", + "remove_deleted_assets": "Remover arquivos offline", "remove_from_album": "Remover do álbum", "remove_from_favorites": "Remover dos favoritos", - "remove_from_shared_link": "Remover do link compartilhado", - "remove_offline_files": "Remover arquivos offline", + "remove_from_shared_link": "Remover do link partilhado", "remove_user": "Remover utilizador", - "removed_api_key": "Removido a Chave de API: {name}", + "removed_api_key": "Foi removida a Chave de API: {name}", "removed_from_archive": "Removido do arquivo", "removed_from_favorites": "Removido dos favoritos", - "removed_from_favorites_count": "{count, plural, other {Removido #}} dos favoritos", - "rename": "Renomear", + "removed_from_favorites_count": "{count, plural, other {Removidos #}} dos favoritos", + "removed_tagged_assets": "Removida a etiqueta de {count, plural, one {# ficheiro} other {# ficheiros}}", + "rename": "Mudar o nome", "repair": "Reparar", - "repair_no_results_message": "Arquivos perdidos ou não rastreados aparecem aqui", - "replace_with_upload": "Substituir", + "repair_no_results_message": "Ficheiros perdidos ou não rastreados irão aparecer aqui", + "replace_with_upload": "Substituir pelo ficheiro carregado", "repository": "Repositório", - "require_password": "Proteger com senha", - "require_user_to_change_password_on_first_login": "Obrigar utilizador a alterar a senha após primeiro início de sessão", - "reset": "Resetar", - "reset_password": "Resetar senha", - "reset_people_visibility": "Resetar pessoas ocultas", + "require_password": "Proteger com palavra-passe", + "require_user_to_change_password_on_first_login": "Obrigar utilizador a alterar a palavra-passe após o primeiro início de sessão", + "reset": "Redefinir", + "reset_password": "Redefinir palavra-passe", + "reset_people_visibility": "Redefinir pessoas ocultas", "reset_settings_to_default": "", "reset_to_default": "Repor predefinições", "resolve_duplicates": "Resolver itens duplicados", - "resolved_all_duplicates": "Todas duplicidades resolvidas", + "resolved_all_duplicates": "Todos os itens duplicados resolvidos", "restore": "Restaurar", "restore_all": "Restaurar tudo", "restore_user": "Restaurar utilizador", - "restored_asset": "Arquivo restaurado", + "restored_asset": "Ficheiro restaurado", "resume": "Continuar", "retry_upload": "Tentar carregar novamente", - "review_duplicates": "Revisar duplicidade", + "review_duplicates": "Rever itens duplicados", "role": "Função", "role_editor": "Editor", "role_viewer": "Visualizador", "save": "Guardar", - "saved_api_key": "Chave de API salva", - "saved_profile": "Perfil Salvo", - "saved_settings": "Configurações salvas", - "say_something": "Diga algo", - "scan_all_libraries": "Escanear Todas Bibliotecas", - "scan_all_library_files": "Re-escanear todos arquivos da biblioteca", - "scan_new_library_files": "Escanear novos arquivos na biblioteca", - "scan_settings": "Opções de escanear", - "scanning_for_album": "Escaneando por álbum...", + "saved_api_key": "Chave de API guardada", + "saved_profile": "Perfil guardado", + "saved_settings": "Definições guardadas", + "say_something": "Diga alguma coisa", + "scan_all_libraries": "Analisar todas as bibliotecas", + "scan_all_library_files": "Re-analisar todos os ficheiros da biblioteca", + "scan_new_library_files": "Analisar novos ficheiros na biblioteca", + "scan_settings": "Opções de análise", + "scanning_for_album": "A analisar por álbum...", "search": "Pesquisar", "search_albums": "Pesquisar álbuns", "search_by_context": "Pesquisar por contexto", "search_by_filename": "Pesquisar por nome de ficheiro ou extensão", "search_by_filename_example": "por exemplo, IMG_1234.JPG ou PNG", - "search_camera_make": "Pesquisar câmeras da marca...", - "search_camera_model": "Pesquisar câmera do modelo...", + "search_camera_make": "Pesquisar por marca da câmara...", + "search_camera_model": "Pesquisar por modelo da câmara...", "search_city": "Pesquisar cidade...", "search_country": "Pesquisar país...", - "search_for_existing_person": "Pesquisar por pessoas", - "search_no_people": "Nenhuma pessoa", + "search_for_existing_person": "Pesquisar por pessoas existentes", + "search_no_people": "Sem pessoas", "search_no_people_named": "Nenhuma pessoa chamada \"{name}\"", + "search_options": "Opções de pesquisa", "search_people": "Pesquisar pessoas", "search_places": "Pesquisar lugares", - "search_state": "Pesquisar estado...", + "search_settings": "Definições de pesquisa", + "search_state": "Pesquisar estado/distrito...", + "search_tags": "Pesquisar etiquetas...", "search_timezone": "Pesquisar fuso horário...", - "search_type": "Pesquisar tipo", + "search_type": "Tipo de pesquisa", "search_your_photos": "Pesquisar fotos", - "searching_locales": "Pesquisar Lugares....", + "searching_locales": "A pesquisar Lugares....", "second": "Segundo", "see_all_people": "Ver todas as pessoas", "select_album_cover": "Escolher capa do álbum", "select_all": "Selecionar todos", "select_all_duplicates": "Selecionar todos os itens duplicados", "select_avatar_color": "Selecionar cor do avatar", - "select_face": "Selecionar face", + "select_face": "Selecionar rosto", "select_featured_photo": "Selecionar foto principal", - "select_from_computer": "Selecionar do computador", - "select_keep_all": "Marcar manter em todos", - "select_library_owner": "Selecione o dono da biblioteca", - "select_new_face": "Selecionar nova face", + "select_from_computer": "Selecionar a partir do computador", + "select_keep_all": "Selecionar manter todos", + "select_library_owner": "Selecionar o dono da biblioteca", + "select_new_face": "Selecionar novo rosto", "select_photos": "Selecionar fotos", - "select_trash_all": "Marcar lixo em todos", + "select_trash_all": "Selecionar todos para reciclagem", "selected": "Selecionados", - "selected_count": "{count, plural, other {# selecionado}}", + "selected_count": "{count, plural, other {# selecionados}}", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", "server_offline": "Servidor Offline", "server_online": "Servidor Online", - "server_stats": "Status do servidor", + "server_stats": "Estado do servidor", "server_version": "Versão do servidor", "set": "Definir", "set_as_album_cover": "Definir como capa do álbum", "set_as_profile_picture": "Definir como foto de perfil", "set_date_of_birth": "Definir data de nascimento", "set_profile_picture": "Definir foto de perfil", - "set_slideshow_to_fullscreen": "Apresentação em tela cheia", - "settings": "Configurações", - "settings_saved": "Configurações salvas", - "share": "Compartilhar", - "shared": "Compartilhado", - "shared_by": "Compartilhado por", + "set_slideshow_to_fullscreen": "Apresentação em ecrã inteiro", + "settings": "Definições", + "settings_saved": "Definições guardadas", + "share": "Partilhar", + "shared": "Partilhado", + "shared_by": "Partilhado por", "shared_by_user": "Partilhado por {user}", - "shared_by_you": "Compartilhado por você", + "shared_by_you": "Partilhado por si", "shared_from_partner": "Fotos de {partner}", - "shared_link_options": "Opções de link compartilhado", - "shared_links": "Links compartilhados", - "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos compartilhados.}}", - "shared_with_partner": "Compartilhado com {partner}", - "sharing": "Compartilhar", - "sharing_enter_password": "Por favor, digite a senha para visualizar esta página.", - "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral", - "shift_to_permanent_delete": "Pressione ⇧ para excluir o arquivo permanentemente", + "shared_link_options": "Opções de link partilhado", + "shared_links": "Links partilhados", + "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos partilhados.}}", + "shared_with_partner": "Partilhado com {partner}", + "sharing": "Partilha", + "sharing_enter_password": "Por favor, insira a palavra-passe para ver esta página.", + "sharing_sidebar_description": "Exibe o link para Partilhar na barra lateral", + "shift_to_permanent_delete": "Pressione ⇧ para eliminar o ficheiro permanentemente", "show_album_options": "Exibir opções do álbum", "show_albums": "Mostrar álbuns", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", - "show_file_location": "Exibir local do arquivo", + "show_file_location": "Exibir localização do ficheiro", "show_gallery": "Exibir galeria", "show_hidden_people": "Exibir pessoas ocultadas", "show_in_timeline": "Exibir na linha do tempo", @@ -1155,19 +1183,23 @@ "show_keyboard_shortcuts": "Exibir atalhos do teclado", "show_metadata": "Mostrar metadados", "show_or_hide_info": "Exibir ou ocultar informações", - "show_password": "Exibir senha", + "show_password": "Mostrar palavra-passe", "show_person_options": "Exibir opções da pessoa", "show_progress_bar": "Exibir barra de progresso", "show_search_options": "Exibir opções de pesquisa", - "show_supporter_badge": "Emblema de apoiador", - "show_supporter_badge_description": "Mostrar um emblema de apoiador", + "show_supporter_badge": "Emblema de apoiante", + "show_supporter_badge_description": "Mostrar um emblema de apoiante", "shuffle": "Aleatório", - "sign_out": "Sair", - "sign_up": "Registrar", + "sidebar": "Barra lateral", + "sidebar_display_description": "Mostrar um link para a vista na barra lateral", + "sign_out": "Terminar sessão", + "sign_up": "Criar conta", "size": "Tamanho", - "skip_to_content": "Pular para o conteúdo", + "skip_to_content": "Saltar para o conteúdo", + "skip_to_folders": "Saltar para pastas", + "skip_to_tags": "Saltar para as etiquetas", "slideshow": "Apresentação", - "slideshow_settings": "Opções de apresentação", + "slideshow_settings": "Definições de apresentação", "sort_albums_by": "Ordenar álbuns por...", "sort_created": "Data de criação", "sort_items": "Número de itens", @@ -1177,49 +1209,58 @@ "sort_title": "Título", "source": "Fonte", "stack": "Empilhar", - "stack_duplicates": "Empilhar duplicados", + "stack_duplicates": "Empilhar itens duplicados", "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", - "stacked_assets_count": "Empilhado {count, plural, one {# arquivo} other {# arquivos}}", + "stacked_assets_count": "Empilhado {count, plural, one {# ficheiro} other {# ficheiros}}", "stacktrace": "Stacktrace", - "start": "Início", - "start_date": "Data inicial", + "start": "Iniciar", + "start_date": "Data de início", "state": "Estado", - "status": "Status", + "status": "Estado", "stop_motion_photo": "Parar foto em movimento", - "stop_photo_sharing": "Parar de partilhar as suas fotos?", - "stop_photo_sharing_description": "{partner} não terá mais acesso às suas fotos.", - "stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este utilizador", + "stop_photo_sharing": "Deixar de partilhar as suas fotos?", + "stop_photo_sharing_description": "{partner} deixará de ter acesso às suas fotos.", + "stop_sharing_photos_with_user": "Deixar de partilhar as fotos com este utilizador", "storage": "Espaço de armazenamento", - "storage_label": "Rótulo de armazenamento", - "storage_usage": "utilizado {used} de {available}", + "storage_label": "Rótulo de Armazenamento", + "storage_usage": "Utilizado {used} de {available}", "submit": "Enviar", "suggestions": "Sugestões", "sunrise_on_the_beach": "Nascer do sol na praia", - "swap_merge_direction": "Alternar direção da mesclagem", + "swap_merge_direction": "Alternar direção da união", "sync": "Sincronizar", + "tag": "Etiqueta", + "tag_assets": "Etiquetar ficheiros", + "tag_created": "Criada a etiqueta {tag}", + "tag_feature_description": "A mostrar fotos e videos agrupados por tópicos lógicos de etiquetas", + "tag_not_found_question": "Não consegue encontrar a etiqueta? Crie uma aqui", + "tag_updated": "Atualizada a etiqueta: {tag}", + "tagged_assets": "Etiquetado {count, plural, one {# ficheiros} other {# ficheiros}}", + "tags": "Etiquetas", "template": "Modelo", "theme": "Tema", "theme_selection": "Selecionar tema", - "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", - "they_will_be_merged_together": "Eles serão mesclados", - "time_based_memories": "Memórias baseada no tempo", + "theme_selection_description": "Definir automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", + "they_will_be_merged_together": "Eles serão unidos", + "time_based_memories": "Memórias baseadas no tempo", "timezone": "Fuso horário", "to_archive": "Arquivar", - "to_change_password": "Alterar senha", + "to_change_password": "Alterar palavra-passe", "to_favorite": "Favorito", - "to_login": "Iniciar sessão", - "to_trash": "Lixo", + "to_login": "Iniciar Sessão", + "to_parent": "Ir para o pai", + "to_trash": "Reciclagem", "toggle_settings": "Alternar configurações", - "toggle_theme": "Alternar tema", + "toggle_theme": "Ativar modo escuro", "toggle_visibility": "Alternar visibilidade", "total_usage": "Total utilizado", - "trash": "Lixeira", - "trash_all": "Todos para o lixo", - "trash_count": "Lixeira {count, number}", - "trash_delete_asset": "Excluir arquivo", - "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", - "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira são deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", + "trash": "Reciclagem", + "trash_all": "Mover todos para a reciclagem", + "trash_count": "Reciclar {count, number}", + "trash_delete_asset": "Eliminar ficheiro", + "trash_no_results_message": "Fotos e vídeos enviados para a reciclagem aparecem aqui.", + "trashed_items_will_be_permanently_deleted_after": "Os itens da reciclagem são eliminados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", "unarchive": "Desarquivar", "unarchived": "Restaurado do arquivo", @@ -1230,70 +1271,72 @@ "unknown_album": "", "unknown_year": "Ano desconhecido", "unlimited": "Ilimitado", + "unlink_motion_video": "Remover relação com video animado", "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Conta OAuth desvinculada", "unnamed_album": "Álbum sem nome", - "unnamed_album_delete_confirmation": "Tem a certeza que pretende remover este album?", - "unnamed_share": "Compartilhamento sem nome", + "unnamed_album_delete_confirmation": "Tem a certeza de que pretende eliminar este álbum?", + "unnamed_share": "Partilha sem nome", "unsaved_change": "Alteração não guardada", "unselect_all": "Limpar seleção", - "unselect_all_duplicates": "Remover seleção de todos os duplicados", + "unselect_all_duplicates": "Remover seleção de todos os itens duplicados", "unstack": "Desempilhar", - "unstacked_assets_count": "Desempilhar {count, plural, one {# arquivo} other {# arquivos}}", - "untracked_files": "Arquivos não monitorados", - "untracked_files_decription": "Estes arquivos não são monitorados pela aplicação. Podem ser resultados de falhas em uma movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema", + "unstacked_assets_count": "Desempilhados {count, plural, one {# ficheiro} other {# ficheiros}}", + "untracked_files": "Ficheiros não monitorizados", + "untracked_files_decription": "Estes ficheiros não são monitorizados pela aplicação. Podem ser resultados de falhas numa movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema", "up_next": "A seguir", - "updated_password": "Senha atualizada", + "updated_password": "Palavra-passe atualizada", "upload": "Carregar", - "upload_concurrency": "Carregar simultâneo", - "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver novos arquivos enviados.", + "upload_concurrency": "Carregamentos em simultâneo", + "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos ficheiros enviados.", "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}", - "upload_skipped_duplicates": "Ignorado {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}", + "upload_skipped_duplicates": "{count, plural, one {# Ignorado ficheiro duplicado} other {# Ignorados ficheiros duplicados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Erros", "upload_status_uploaded": "Enviado", - "upload_success": "Upload realizado com sucesso, atualize a página para ver os novos ativos de upload.", + "upload_success": "Carregamento realizado com sucesso, atualize a página para ver os novos ficheiros carregados.", "url": "URL", - "usage": "Uso", - "use_custom_date_range": "Usar um intervalo de datas personalizado", + "usage": "Utilização", + "use_custom_date_range": "Utilizar um intervalo de datas personalizado", "user": "Utilizador", "user_id": "ID do utilizador", - "user_liked": "{user} gostou {type, select, photo {dessa foto} video {deste video} asset {deste arquivo} other {disto}}", - "user_purchase_settings": "Compra", - "user_purchase_settings_description": "Gerencie sua compra", + "user_liked": "{user} gostou {type, select, photo {desta fotografia} video {deste video} asset {deste ficheiro} other {disto}}", + "user_purchase_settings": "Comprar", + "user_purchase_settings_description": "Gerir a sua compra", "user_role_set": "Definir {user} como {role}", - "user_usage_detail": "Detalhes de uso do utilizador", - "username": "Nome do utilizador", + "user_usage_detail": "Detalhes de utilização do utilizador", + "username": "Nome de utilizador", "users": "Utilizadores", - "utilities": "Utilitários", + "utilities": "Ferramentas", "validate": "Validar", "variables": "Variáveis", "version": "Versão", - "version_announcement_closing": "Seu amigo, Alex", - "version_announcement_message": "Olá amigo, há uma nova versão do aplicativo. Reserve um tempo para visitar as histórico de mudanças e garantir que suas configurações docker-compose.yml e .env estejam atualizadas para evitar qualquer configuração incorreta, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização do seu aplicativo automaticamente.", + "version_announcement_closing": "O seu amigo, Alex", + "version_announcement_message": "Olá amigo, há uma nova versão da aplicação. Reserve algum tempo para visitar o histórico de mudanças e garantir que as suas configurações do docker-compose.yml e .env estão atualizadas para evitar qualquer configuração incorreta, especialmente se usar o WatchTower ou qualquer mecanismo que lide com a atualização automática da aplicação.", "video": "Vídeo", - "video_hover_setting": "Reproduzir vídeo em miniatura quando passar por cima", - "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o mouse está sobre o item. Mesmo quando desativado, a reprodução ainda pode ser iniciada passando sobre o ícone.", + "video_hover_setting": "Reproduzir vídeo em miniatura quando passar com o cursor por cima", + "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o cursor está sobre o item. Mesmo quando está desativado, a reprodução ainda pode ser iniciada passando sobre o ícone de reproduzir.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", "view": "Ver", "view_album": "Ver Álbum", "view_all": "Ver tudo", "view_all_users": "Ver todos os utilizadores", + "view_in_timeline": "Ver na linha do tempo", "view_links": "Ver links", - "view_next_asset": "Ver próximo ativo", - "view_previous_asset": "Ver ativo anterior", - "view_stack": "Visualizar pilha", + "view_next_asset": "Ver próximo ficheiro", + "view_previous_asset": "Ver ficheiro anterior", + "view_stack": "Ver pilha", "viewer": "Visualizar", "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", - "waiting": "Aguardando", + "waiting": "Em fila", "warning": "Aviso", "week": "Semana", - "welcome": "Bem-vindo", - "welcome_to_immich": "Bem-vindo ao Immich", + "welcome": "Bem-vindo(a)", + "welcome_to_immich": "Bem-vindo(a) ao Immich", "year": "Ano", - "years_ago": "Há {years, plural, one {# ano} other {# anos}}", + "years_ago": "Há {years, plural, one {# ano} other {# anos}} atrás", "yes": "Sim", - "you_dont_have_any_shared_links": "Não há links compartilhados", - "zoom_image": "Ampliar imagem" + "you_dont_have_any_shared_links": "Não tem links partilhados", + "zoom_image": "Ampliar/Reduzir imagem" } diff --git a/web/src/lib/i18n/pt_BR.json b/web/src/lib/i18n/pt_BR.json index 1e0de69aeda59..a3eb6d00dfd36 100644 --- a/web/src/lib/i18n/pt_BR.json +++ b/web/src/lib/i18n/pt_BR.json @@ -148,7 +148,7 @@ "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", "notification_email_from_address": "A partir do endereço", - "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", + "notification_email_from_address_description": "Endereço de e-mail do remetente, por exemplo: \"Immich Photo Server \"", "notification_email_host_description": "Host do servidor de e-mail (por exemplo, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorar erros de certificado", "notification_email_ignore_certificate_errors_description": "Ignorar erros de validação de certificado TLS (não recomendado)", @@ -198,7 +198,7 @@ "refreshing_all_libraries": "Atualizando todas as bibliotecas", "registration": "Registro de Administrador", "registration_description": "Como você é o primeiro usuário no sistema, será designado como o Administrador e será responsável pelas tarefas administrativas. Você também poderá criar usuários adicionais.", - "removing_offline_files": "Removendo arquivos offline", + "removing_deleted_files": "Removendo arquivos offline", "repair_all": "Reparar tudo", "repair_matched_items": "{count, plural, one {# item encontrado} other {# itens encontrados}}", "repaired_items": "{count, plural, one {# item reparado} other {# itens reparados}}", @@ -670,8 +670,8 @@ "unable_to_remove_api_key": "Não foi possível a Chave de API", "unable_to_remove_assets_from_shared_link": "Não foi possível remover arquivos do link compartilhado", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Não foi possível remover arquivos offline", "unable_to_remove_library": "Não foi possível remover a biblioteca", - "unable_to_remove_offline_files": "Não foi possível remover arquivos offline", "unable_to_remove_partner": "Não foi possível remover parceiro", "unable_to_remove_reaction": "Não foi possível remover a reação", "unable_to_remove_user": "", @@ -1068,10 +1068,10 @@ "remove_assets_shared_link_confirmation": "Tem certeza de que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?", "remove_assets_title": "Remover arquivos?", "remove_custom_date_range": "Remover intervalo de datas personalizado", + "remove_deleted_assets": "Remover arquivos offline", "remove_from_album": "Remover do álbum", "remove_from_favorites": "Remover dos favoritos", "remove_from_shared_link": "Remover do link compartilhado", - "remove_offline_files": "Remover arquivos offline", "remove_user": "Remover usuário", "removed_api_key": "Removido a Chave de API: {name}", "removed_from_archive": "Removido do arquivo", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index 534794b6d312c..491b72694fadf 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -41,6 +41,7 @@ "confirm_email_below": "Pentru a confirma, tastați „{email}” mai jos", "confirm_reprocess_all_faces": "Sigur doriți să reprocesați toate fețele? Acest lucru va șterge și persoanele cu nume.", "confirm_user_password_reset": "Sigur doriți să resetați parola utilizatorului {user}?", + "create_job": "Creează sarcină", "crontab_guru": "", "disable_login": "Dezactivați autentificarea", "disabled": "", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Rezoluție imagini miniatură", "image_thumbnail_resolution_description": "Folosit la vizualizarea unor grupuri de fotografii (cronologie principală, vizualizare album etc.). Rezoluțiile mai mari pot păstra mai multe detalii, dar codarea durează mai mult, au dimensiuni mai mari ale fișierelor și pot reduce capacitatea de răspuns a aplicației.", "job_concurrency": "concurență {job}", + "job_created": "Sarcină creată", "job_not_concurrency_safe": "Acest job nu este sigur pentru a rula în concurență.", "job_settings": "Setări sarcină", "job_settings_description": "Administrează concurența sarcinilor", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "NOTĂ: Nu se va mai putea modifica ulterior!", "note_unlimited_quota": "Notă: Introduceți 0 pentru cotă nelimitată", "notification_email_from_address": "De la adresa", - "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”", + "notification_email_from_address_description": "Adresa expeditorului, spre exemplu: „Immich Photo Server ”", "notification_email_host_description": "Adresa serverului de email (e.g. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ingnoră erorile de certificat", "notification_email_ignore_certificate_errors_description": "Ignoră erorile de validare a certificatului TLS (nerecomandat)", @@ -202,7 +204,7 @@ "refreshing_all_libraries": "Bibliotecile sunt în curs de reîmprospǎtare", "registration": "Înregistrare administratori", "registration_description": "Deoarece sunteți primul utilizator de pe sistem, veți fi desemnat ca administrator și sunteți responsabil pentru sarcinile administrative, iar utilizatorii suplimentari vor fi creați de dumneavoastra.", - "removing_offline_files": "Eliminarea fișierelor offline", + "removing_deleted_files": "Eliminarea fișierelor offline", "repair_all": "Reparǎ toate", "repair_matched_items": "{count, plural, one {Potrivit # obiect} other {Potrivite # obiecte}}", "repaired_items": "{count, plural, one {Reparat # obiect} other {Reparate # obiecte}}", @@ -235,95 +237,130 @@ "storage_template_onboarding_description": "Atunci când este activată, această caracteristică va organiza automat fișierele pe baza unui șablon definit de utilizator. Din cauza unor probleme de stabilitate, aceasta caracteristică este dezactivată implicit. Pentru mai multe informații, te rog sa consulți documentația.", "storage_template_path_length": "Limita de lungime pentru calea aproximativă: {length, number}/{limit, number}", "storage_template_settings": "Șablon stocare", - "storage_template_settings_description": "", + "storage_template_settings_description": "Gestionează structura folderelor și numele fișierelor pentru activele încărcate", + "storage_template_user_label": "{label} este eticheta de stocare a utilizatorului", "system_settings": "Setǎri de sistem", + "tag_cleanup_job": "Curățare etichete", "theme_custom_css_settings": "CSS personalizat", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", + "theme_custom_css_settings_description": "Foile de stil în cascadă (CSS) permit personalizarea designului Immich.", + "theme_settings": "Setări temă", + "theme_settings_description": "Gestionează personalizarea interfeței web Immich", + "these_files_matched_by_checksum": "Aceste fișiere sunt comparate folosind sumele de control", + "thumbnail_generation_job": "Gerează miniaturi", + "thumbnail_generation_job_description": "Generează miniaturi mari, mici și estompate pentru fiecare resursă, precum și miniaturi pentru fiecare persoană", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api": "API de accelerare", + "transcoding_acceleration_api_description": "API-ul care va interacționa cu dispozitivul tău pentru a accelera transcodarea. Această setare este 'best effort': va reveni la transcodarea software în caz de eșec. VP9 poate funcționa sau nu, în funcție de hardware-ul tău.", "transcoding_acceleration_nvenc": "NVENC (necesitǎ GPU NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (necesitǎ CPU Intel de generația a 7-a sau mai mare)", "transcoding_acceleration_rkmpp": "RKMPP (doar pe SOC-uri Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codec-uri audio acceptate", - "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_audio_codecs_description": "Selectează care codec-uri audio nu trebuie să fie transcodificate. Se utilizează doar pentru anumite politici de transcodare.", + "transcoding_accepted_containers": "Containere acceptate", + "transcoding_accepted_containers_description": "Selectează formatele de containere care nu trebuie să fie remuxate în MP4. Se utilizează doar pentru anumite politici de transcodare.", "transcoding_accepted_video_codecs": "Codec-uri video acceptate", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", + "transcoding_accepted_video_codecs_description": "Selectează codec-urile video care nu trebuie să fie transcodificate. Se utilizează doar pentru anumite politici de transcodare.", + "transcoding_advanced_options_description": "Opțiuni pe care majoritatea utilizatorilor nu ar trebui să fie necesar să le schimbe", "transcoding_audio_codec": "Codec audio", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", + "transcoding_audio_codec_description": "Opus este opțiunea cu cea mai bună calitate, dar are o compatibilitate mai scăzută cu dispozitivele sau software-ul mai vechi.", + "transcoding_bitrate_description": "Videoclipuri cu un bitrate mai mare decât maximul acceptat sau care nu sunt într-un format acceptat", + "transcoding_codecs_learn_more": "Pentru a afla mai multe despre terminologia folosită aici, consultă documentația FFmpeg pentru codec-ul H.264, codec-ul HEVC și codec-ul VP9.", + "transcoding_constant_quality_mode": "Mod de calitate constantă", + "transcoding_constant_quality_mode_description": "ICQ este mai bun decât CQP, dar unele dispozitive de accelerare hardware nu suportă acest mod. Setarea acestei opțiuni va prefera modul specificat atunci când folosești codificarea bazată pe calitate. Ignorat de NVENC deoarece nu suportă ICQ.", + "transcoding_constant_rate_factor": "Factor de rată constantă (-crf)", + "transcoding_constant_rate_factor_description": "Nivelul de calitate al videoclipului. Valorile tipice sunt 23 pentru H.264, 28 pentru HEVC, 31 pentru VP9 și 35 pentru AV1. Cu cât valoarea este mai mică, cu atât calitatea este mai bună, dar se generează fișiere mai mari.", + "transcoding_disabled_description": "Nu transcodifică niciun videoclip; acest lucru poate afecta redarea pe anumite dispozitive", + "transcoding_hardware_acceleration": "Accelerare Hardware", + "transcoding_hardware_acceleration_description": "Experimental; mult mai rapid, dar va avea o calitate mai scăzută la același bitrate", + "transcoding_hardware_decoding": "Decodare hardware", + "transcoding_hardware_decoding_setting_description": "Se aplică doar pentru NVENC, QSV și RKMPP. Activează accelerarea completă în loc de doar accelerarea codificării. S-ar putea să nu funcționeze pentru toate videoclipurile.", + "transcoding_hevc_codec": "codec HEVC", + "transcoding_max_b_frames": "Număr maxim de cadre B", + "transcoding_max_b_frames_description": "Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. Este posibil să nu fie compatibile cu accelerarea hardware pe dispozitivele mai vechi. 0 dezactivează cadrele B, în timp ce -1 setează această valoare automat.", + "transcoding_max_bitrate": "Bitrate maxim", + "transcoding_max_bitrate_description": "Setarea unei rate maxime de biți poate face dimensiunile fișierelor mai previzibile, cu un cost minor asupra calității. La 720p, valorile tipice sunt 2600k pentru VP9 sau HEVC, sau 4500k pentru H.264. Dezactivat dacă este setat la 0.", + "transcoding_max_keyframe_interval": "Interval maxim între cadre cheie", + "transcoding_max_keyframe_interval_description": "Setează distanța maximă între cadrele cheie. Valorile mai mici reduc eficiența compresiei, dar îmbunătățesc timpii de căutare și pot îmbunătăți calitatea în scenele cu mișcare rapidă. 0 setează această valoare automat.", + "transcoding_optimal_description": "Videoclipuri cu rezoluție mai mare decât cea țintă sau care nu sunt într-un format acceptat", + "transcoding_preferred_hardware_device": "Dispozitiv hardware preferat", + "transcoding_preferred_hardware_device_description": "Se aplică doar la VAAPI și QSV. Setează nodul DRI utilizat pentru transcodarea hardware.", + "transcoding_preset_preset": "Presetare (-preset)", + "transcoding_preset_preset_description": "Viteza de compresie. Presetările mai lente produc fișiere mai mici și îmbunătățesc calitatea atunci când vizezi o anumită rată de biți. VP9 ignoră vitezele de compresie mai mari decât 'mai rapid'.", + "transcoding_reference_frames": "Cadre de referință", + "transcoding_reference_frames_description": "Numărul de cadre de referință atunci când se comprimă un cadru dat. Valorile mai mari îmbunătățesc eficiența compresiei, dar încetinesc codarea. 0 setează această valoare automat.", + "transcoding_required_description": "Numai videoclipuri care nu sunt într-un format acceptat", + "transcoding_settings": "Setări de transcodare video", + "transcoding_settings_description": "Gestionează rezoluția și informațiile de codare ale fișierelor video", + "transcoding_target_resolution": "Rezoluția țintă", + "transcoding_target_resolution_description": "Rezoluțiile mai mari pot păstra mai multe detalii, dar necesită mai mult timp pentru codare, au dimensiuni mai mari ale fișierelor și pot reduce răspunsul aplicației.", + "transcoding_temporal_aq": "AQ temporal", + "transcoding_temporal_aq_description": "Se aplică doar la NVENC. Îmbunătățește calitatea scenelor cu detalii mari și mișcare redusă. Poate să nu fie compatibil cu dispozitivele mai vechi.", + "transcoding_threads": "Fire", + "transcoding_threads_description": "Valorile mai mari conduc la o codare mai rapidă, dar lasă mai puțin spațiu serverului pentru a procesa alte sarcini în timp ce este activ. Această valoare nu ar trebui să fie mai mare decât numărul de nuclee CPU. Maximizați utilizarea dacă este setat la 0.", + "transcoding_tone_mapping": "Mapare tonuri", + "transcoding_tone_mapping_description": "Încearcă să păstreze aspectul videoclipurilor HDR atunci când sunt convertite în SDR. Fiecare algoritm face compromisuri diferite pentru culoare, detalii și strălucire. Hable păstrează detaliile, Mobius păstrează culoarea, iar Reinhard păstrează strălucirea.", + "transcoding_tone_mapping_npl": "Mapare tonuri NPL", + "transcoding_tone_mapping_npl_description": "Culorile vor fi ajustate pentru a arăta normal pe un ecran cu această strălucire. În mod contraintuitiv, valorile mai mici cresc strălucirea videoclipului și invers, deoarece compensează pentru strălucirea ecranului. 0 setează această valoare automat.", + "transcoding_transcode_policy": "Politica de transcodare", + "transcoding_transcode_policy_description": "Politica pentru când un videoclip ar trebui să fie transcodificat. Videoclipurile HDR vor fi întotdeauna transcodificate (cu excepția cazului în care transcodarea este dezactivată).", + "transcoding_two_pass_encoding": "Codare în două treceri", + "transcoding_two_pass_encoding_setting_description": "Transcodificare în două treceri pentru a produce videoclipuri codificate mai bine. Când rata maximă de biți este activată (necesară pentru a funcționa cu H.264 și HEVC), acest mod utilizează un interval de rată de biți bazat pe rata maximă de biți și ignoră CRF. Pentru VP9, CRF poate fi utilizat dacă rata maximă de biți este dezactivată.", "transcoding_video_codec": "Codec video", "transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.", - "trash_enabled_description": "", + "trash_enabled_description": "Activează funcțiile Coș de gunoi", "trash_number_of_days": "Numǎr de zile", "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ", "trash_settings": "Setǎri coș de gunoi", "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", + "untracked_files": "Fișiere neurmărite", + "untracked_files_description": "Aceste fișiere nu sunt urmărite de aplicație. Ele pot fi rezultatul unor mutări eșuate, încărcări întrerupte sau pot rămâne în urmă din cauza unei erori", + "user_cleanup_job": "Curățare utilizator", + "user_delete_delay": "Contul și resursele utilizatorului {user} vor fi programate pentru ștergere permanentă în {delay, plural, one {# zi} other {# zile}}.", + "user_delete_delay_settings": "Întârziere la ștergere", + "user_delete_delay_settings_description": "Numărul de zile după eliminare până la ștergerea permanentă a contului și a resurselor unui utilizator. Procesul de ștergere a utilizatorului rulează la miezul nopții pentru a verifica utilizatorii care sunt pregătiți pentru ștergere. Modificările aduse acestei setări vor fi evaluate la următoarea execuție.", + "user_delete_immediately": "Contul și resursele utilizatorului {user} vor fi puse în coadă pentru ștergere permanentă imediat.", + "user_delete_immediately_checkbox": "Pune utilizatorul și resursele în coadă pentru ștergere imediată", + "user_management": "Gestionarea Utilizatorilor", + "user_password_has_been_reset": "Parola utilizatorului a fost resetată:", + "user_password_reset_description": "Vă rugăm să furnizați utilizatorului parola temporară și să îi informați că va trebui să o schimbe la următoarea autentificare.", + "user_restore_description": "Contul utilizatorului {user} va fi restaurat.", + "user_restore_scheduled_removal": "Restaurare utilizator - ștergere programată pe {date, date, long}", "user_settings": "Setǎri utilizator", "user_settings_description": "Gestioneazǎ setǎrile utilizatorului", - "version_check_enabled_description": "Activeazǎ verificarea periodicǎ pe GitHub pentru versiuni noi", + "user_successfully_removed": "Utilizatorul {email} a fost eliminat cu succes.", + "version_check_enabled_description": "Activează verificarea versiunii", + "version_check_implications": "Funcția de verificare a versiunii se bazează pe comunicarea periodică cu github.com", "version_check_settings": "Verificare versiune", "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni", - "video_conversion_job_description": "Transcodeazǎ videoclipurile pentru compatibilitate cu browsere și dispozitive" + "video_conversion_job": "Transcodați videoclipuri", + "video_conversion_job_description": "Transcodați videoclipurile pentru o compatibilitate mai mare cu browserele și dispozitivele" }, "admin_email": "E-mailul administratorului", - "admin_password": "Parola administratorului", + "admin_password": "Parolă administrator", "administration": "Administrare", "advanced": "Avansat", + "age_months": "Vârstă {months, plural, one {# lună} other {# luni}}", + "age_year_months": "Vârstă de 1 an, {months, plural, one {# lună} other {# luni}}", + "age_years": "{years, plural, other {Vârstă #}}", "album_added": "Album adăugat", "album_added_notification_setting_description": "Primiți o notificare prin e-mail când sunteți adăugat la un album partajat", "album_cover_updated": "Coperta albumului a fost actualizată", - "album_info_updated": "Informațiile albumului au fost actualizate", - "album_name": "Nume de album", - "album_options": "Opțiuni de album", + "album_delete_confirmation": "Ești sigur că vrei să ștergi albumul {album}?", + "album_delete_confirmation_description": "Dacă acest album este partajat, alți utilizatori nu vor mai putea accesa.", + "album_info_updated": "Informații album actualizate", + "album_leave": "Lăsați albumul?", + "album_leave_confirmation": "Ești sigur că dorești să părăsești {album}?", + "album_name": "Nume album", + "album_options": "Opțiuni album", + "album_remove_user": "Eliminare utilizator?", + "album_remove_user_confirmation": "Ești sigur că dorești eliminarea {user}?", + "album_share_no_users": "Se pare că ai partajat acest album cu toți utilizatorii sau nu ai niciun utilizator cu care să-l partajezi.", "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi", + "album_user_left": "A părăsit {album}", + "album_user_removed": "{user} eliminat", + "album_with_link_access": "Permite oricui cu link-ul să vadă fotografiile și persoanele din acest album.", "albums": "Albume", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}", "all": "Toate", @@ -334,40 +371,58 @@ "allow_edits": "Permite editări", "allow_public_user_to_download": "Permite utilizatorului public să descarce", "allow_public_user_to_upload": "Permite utilizatorului public să încarce", + "anti_clockwise": "În sens invers acelor de ceasornic", "api_key": "Cheie API", "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați înainte de a închide fereastra.", "api_key_empty": "Numele cheii API nu trebuie să fie gol", "api_keys": "Chei API", - "app_settings": "Setări în aplicație", + "app_settings": "Setări Aplicație", "appears_in": "Apare în", "archive": "Arhivă", "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia", + "archive_size": "Mărime arhivă", + "archive_size_description": "Configurează dimensiunea arhivei pentru descărcări (în GiB)", "archived": "", - "archived_count": "{count, plural, one {S-a arhivat #}, other {S-au arhivat #}}", + "archived_count": "{count, plural, other {Arhivat/e#}}", + "are_these_the_same_person": "Sunt aceștia aceeași persoană?", "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?", "asset_added_to_album": "Adăugat la album", - "asset_adding_to_album": "Se adauga la album...", + "asset_adding_to_album": "Se adaugă la album...", "asset_description_updated": "Descrierea activelor a fost actualizată", - "asset_filename_is_offline": "Activul {filename} este offline", - "asset_has_unassigned_faces": "Activul are fețe neatribuite", - "asset_hashing": "Hasurare...", + "asset_filename_is_offline": "Resursa {filename} este offline", + "asset_has_unassigned_faces": "Resursa are fețe neatribuite", + "asset_hashing": "Hașurare...", "asset_offline": "Resursă offline", - "asset_offline_description": "Acest activ este offline. Immich nu poate accesa locația fișierului său. Vă rugăm să vă asigurați că activul este disponibil și apoi să efectuați o nouă scanare a bibliotecii.", + "asset_offline_description": "Această resursă este offline. Immich nu poate accesa locația fișierului său. Vă rugăm să vă asigurați că resursa este disponibilă și apoi să efectuați o nouă scanare a bibliotecii.", "asset_skipped": "Sărit", + "asset_skipped_in_trash": "În gunoi", "asset_uploaded": "Încărcat", - "asset_uploading": "Se incărca...", + "asset_uploading": "Se incarcă...", "assets": "Resurse", + "assets_added_count": "Adăugat {count, plural, one {# resursă} other {# resurse}}", + "assets_added_to_album_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în album", + "assets_added_to_name_count": "Am adăugat {count, plural, one {# resursă} other {# resurse}} în {hasName, select, true {{name}} other {albumul nou}}", + "assets_count": "{count, plural, one {# resursă} other {# resurse}}", + "assets_moved_to_trash_count": "Am mutat {count, plural, one {# resursă} other {# resurse}} în coșul de gunoi", + "assets_permanently_deleted_count": "Șters permanent {count, plural, one {# resursă} other {# resurse}}", + "assets_removed_count": "Eliminat {count, plural, one {# resursă} other {# resurse}}", + "assets_restore_confirmation": "Ești sigur că vrei să restaurezi toate resursele tale din coșul de gunoi? Nu poți anula această acțiune!", + "assets_restored_count": "Restaurat {count, plural, one {# resursă} other {# resurse}}", + "assets_trashed_count": "Mutat în coșul de gunoi {count, plural, one {# resursă} other {# resurse}}", + "assets_were_part_of_album_count": "{count, plural, one {Resursa era} other {Resursele erau}} deja parte din album", "authorized_devices": "Dispozitive autorizate", "back": "Înapoi", "back_close_deselect": "Înapoi, închidere sau deselectare", - "backward": "Invers", + "backward": "În sens invers", "birthdate_saved": "Data nașterii salvată cu succes", "birthdate_set_description": "Data nașterii este utilizată pentru a calcula vârsta acestei persoane la momentul realizării fotografiei.", "blurred_background": "Fundal neclar", "build": "Construiți", - "build_image": "Construiți o imagine", - "bulk_delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți în masă {count, plural, one {# duplicate asset} other {# duplicate assets}}? Acest lucru va păstra cel mai mare activ din fiecare grup și va șterge definitiv toate celelalte duplicate. Nu puteți anula această acțiune!", - "buy": "Cumpără Immich", + "build_image": "Construiți imagine", + "bulk_delete_duplicates_confirmation": "Ești sigur că vrei să ștergi în masă {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va șterge permanent toate celelalte duplicate. Nu poți anula această acțiune!", + "bulk_keep_duplicates_confirmation": "Ești sigur că vrei să păstrezi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va rezolva toate grupurile duplicate fără a șterge nimic.", + "bulk_trash_duplicates_confirmation": "Ești sigur că vrei să muți în coșul de gunoi {count, plural, one {# resursă duplicată} other {# resurse duplicate}}? Aceasta va păstra cea mai mare resursă din fiecare grup și va muta în coșul de gunoi toate celelalte duplicate.", + "buy": "Achiziționează Immich", "camera": "Camerǎ", "camera_brand": "Marcǎ cameră", "camera_model": "Model cameră", @@ -381,37 +436,41 @@ "cant_search_people": "", "cant_search_places": "", "change_date": "Schimbă dată", - "change_expiration_time": "Shimbă data expirării", + "change_expiration_time": "Shimbă dată expirare", "change_location": "Schimbă locația", - "change_name": "Schimbă numele", - "change_name_successfully": "Schimbă numele cu succes", - "change_password": "Schimbă parola", - "change_password_description": "Aceasta este fie prima dată când vă conectați la sistem, fie vi s-a solicitat să vă schimbați parola. Vă rugăm să introduceți noua parolă mai jos.", + "change_name": "Schimbă nume", + "change_name_successfully": "Schimbare nume cu succes", + "change_password": "Schimbă Parolă", + "change_password_description": "Aceasta este fie prima dată când te conectezi în sistem, fie s-a făcut o solicitare pentru a schimba parola ta. Te rog să introduci noua parolă mai jos.", "change_your_password": "Schimbă-ți parola", - "changed_visibility_successfully": "Schimbă visibilitate cu succes", - "check_logs": "Verificarea logurilor", - "choose_matching_people_to_merge": "Alegeți persoanele potrivite pentru fuzionare", + "changed_visibility_successfully": "Schimbare vizibilitate cu succes", + "check_all": "Selectează Tot", + "check_logs": "Verifică Jurnale", + "choose_matching_people_to_merge": "Alegeți persoanele care se potrivesc pentru a le fuziona", "city": "Oraș", - "clear": "ȘTERGE", - "clear_all": "Șterge tot", + "clear": "Curăță", + "clear_all": "Curăță tot", + "clear_all_recent_searches": "Curăță toate căutările recente", "clear_message": "Șterge mesajul", - "clear_value": "Valoare clară", + "clear_value": "Șterge valoare", + "clockwise": "În sensul acelor de ceas", "close": "Închide", - "collapse": "Colaps", - "collapse_all": "Închideți pe toate", + "collapse": "Restrânge", + "collapse_all": "Restrânge pe toate", + "color": "Culoare", "color_theme": "Tema de culoare", "comment_deleted": "Comentariu șters", - "comment_options": "Opțiuni de comentariu", - "comments_and_likes": "Comentarii și aprecieri", + "comment_options": "Opțiuni comentariu", + "comments_and_likes": "Comentarii & aprecieri", "comments_are_disabled": "Comentariile sunt dezactivate", "confirm": "Confirmați", "confirm_admin_password": "Confirmați parola de administrator", "confirm_delete_shared_link": "Sunteți sigur că doriți să ștergeți acest link partajat?", "confirm_password": "Confirmați parola", - "contain": "Conține", + "contain": "Încadrează", "context": "Context", "continue": "Continuați", - "copied_image_to_clipboard": "Copiat imaginea în clipboard.", + "copied_image_to_clipboard": "Imaginea copiată în clipboard.", "copied_to_clipboard": "Copiat în clipboard!", "copy_error": "Eroare de copiere", "copy_file_path": "Copiați calea fișierului", @@ -421,64 +480,70 @@ "copy_password": "Copiați parola", "copy_to_clipboard": "Copiere în Clipboard", "country": "Țara", - "cover": "Acoperire", - "covers": "Acoperiri", + "cover": "Umple fereastra", + "covers": "Acoperă", "create": "Creează", "create_album": "Creează album", - "create_library": "Crearea bibliotecii", + "create_library": "Creează bibliotecă", "create_link": "Creează link", "create_link_to_share": "Creează link pentru a distribui", "create_link_to_share_description": "Permiteți oricui are link-ul să vadă fotografia (fotografiile) selectată(e)", "create_new_person": "Creați o persoană nouă", - "create_new_person_hint": "Atribuiți activele selectate unei persoane noi", - "create_new_user": "Crearea unui nou utilizator", + "create_new_person_hint": "Atribuiți resursele selectate unei persoane noi", + "create_new_user": "Creează utilizator nou", + "create_tag": "Creează etichetă", + "create_tag_description": "Creează o etichetă nouă. Pentru etichete imbricate, te rog să introduci calea completă a etichetei, inclusiv bare oblice (/).", "create_user": "Creează utilizator", "created": "Creat", "current_device": "Dispozitiv curent", - "custom_locale": "Local personalizat", + "custom_locale": "Setare regională personalizată", "custom_locale_description": "Formatați datele și numerele în funcție de limbă și regiune", - "dark": "Întuneric", - "date_after": "Data după", + "dark": "Întunecat", + "date_after": "Dată după", "date_and_time": "Dată și Oră", - "date_before": "Data anterioară", + "date_before": "Dată anterioară", "date_of_birth_saved": "Data nașterii salvată cu succes", "date_range": "Interval de date", - "day": "Ziua", + "day": "Zi", "deduplicate_all": "Deduplicați toate", - "default_locale": "Local implicit", - "default_locale_description": "Formatați datele și numerele în funcție de locația browserului dvs.", + "default_locale": "Setare regionlă implicită", + "default_locale_description": "Formatați datele și numerele în funcție de regiunea browserului dvs", "delete": "Șterge", "delete_album": "Șterge album", "delete_api_key_prompt": "Sunteți sigur că doriți să ștergeți această cheie API?", "delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți permanent aceste duplicate?", - "delete_key": "Tasta de ștergere", - "delete_library": "Ștergeți biblioteca", - "delete_link": "Ștergeți linkul", - "delete_shared_link": "Ștergeți link-ul partajat", - "delete_user": "Ștergeți utilizatorul", + "delete_key": "Șterge cheie", + "delete_library": "Șterge biblioteca", + "delete_link": "Șterge linkul", + "delete_shared_link": "Șterge link-ul partajat", + "delete_tag": "Șterge etichetă", + "delete_tag_confirmation_prompt": "Ești sigur că vrei să ștergi eticheta {tagName} ?", + "delete_user": "Șterge utilizator", "deleted_shared_link": "Link partajat șters", "description": "Descriere", - "details": "DETALII", + "details": "Detalii", "direction": "Direcție", "disabled": "Dezactivat", - "disallow_edits": "Interziceți editările", + "disallow_edits": "Interzice modificările", "discover": "Descoperiți", - "dismiss_all_errors": "Eliminați toate erorile", - "dismiss_error": "Anulați eroarea", + "dismiss_all_errors": "Ignoră toate erorile", + "dismiss_error": "Ignorați eroarea", "display_options": "Opțiuni de afișare", "display_order": "Ordine de afișare", "display_original_photos": "Afișați fotografiile originale", - "display_original_photos_setting_description": "Preferați să afișați fotografia originală atunci când vizualizați un bun în loc de miniaturi atunci când bunul original este compatibil cu web. Acest lucru poate duce la o viteză mai mică de afișare a fotografiilor.", - "do_not_show_again": "Nu mai afișați acest mesaj", + "display_original_photos_setting_description": "Preferă să afișezi fotografia originală atunci când vizualizezi o resursă, în loc de miniaturi, atunci când resursa originală este compatibilă cu web-ul. Aceasta poate duce la viteze mai lente de afișare a fotografiilor.", + "do_not_show_again": "Nu mai afișa acest mesaj", "done": "Gata", "download": "Descarcă", + "download_include_embedded_motion_videos": "Videoclipuri încorporate", + "download_include_embedded_motion_videos_description": "Include videoclipurile încorporate în fotografiile în mișcare ca fișier separat", "download_settings": "Descarcă", - "download_settings_description": "Gestionați setările legate de descărcarea activelor", - "downloading": "Descărcare", - "downloading_asset_filename": "Descărcarea activului {filename}", - "drop_files_to_upload": "Aruncați fișiere oriunde pentru a le încărca", + "download_settings_description": "Gestionați setările legate de descărcarea resurselor", + "downloading": "Se descarcă", + "downloading_asset_filename": "Se descarcă resursa {filename}", + "drop_files_to_upload": "Trage fișierele aici pentru a le încărca", "duplicates": "Duplicate", - "duplicates_description": "Rezolvați fiecare grup indicând care, dacă există, sunt duplicate", + "duplicates_description": "Rezolvați fiecare grup indicând care sunt duplicate, dacă există", "duration": "Durată", "durations": { "days": "", @@ -487,13 +552,13 @@ "months": "", "years": "" }, - "edit": "Editare", - "edit_album": "Editare album", - "edit_avatar": "Editare avatar", - "edit_date": "Editează data", - "edit_date_and_time": "Editarea datei și orei", + "edit": "Modifică", + "edit_album": "Modificare album", + "edit_avatar": "Modificare avatar", + "edit_date": "Modifică data", + "edit_date_and_time": "Modifică data și ora", "edit_exclusion_pattern": "Editarea modelului de excludere", - "edit_faces": "Editează fețele", + "edit_faces": "Modifică fețele", "edit_import_path": "Editarea căii de import", "edit_import_paths": "Editarea căilor de import", "edit_key": "Tastă de editare", @@ -501,10 +566,15 @@ "edit_location": "Editează locație", "edit_name": "Editează nume", "edit_people": "Editează persoane", + "edit_tag": "Modifică etichetă", "edit_title": "Editează Titlul", - "edit_user": "", + "edit_user": "Modifică utilizator", "edited": "Editat", - "editor": "", + "editor": "Editor", + "editor_close_without_save_prompt": "Schimbările nu vor fi salvate", + "editor_close_without_save_title": "Închizi editorul?", + "editor_crop_tool_h2_aspect_ratios": "Raporturi de aspect", + "editor_crop_tool_h2_rotation": "Rotire", "email": "Email", "empty": "", "empty_album": "", @@ -520,7 +590,9 @@ "cannot_navigate_next_asset": "Nu se poate naviga către următorul activ", "cannot_navigate_previous_asset": "Nu se poate naviga la activul anterior", "cant_apply_changes": "Nu se pot aplica schimbări", + "cant_change_activity": "Nu se poate {enabled, select, true {dezactiva} other {activa}} activitatea", "cant_change_asset_favorite": "Nu pot schimba favoritul pentru activ", + "cant_change_metadata_assets_count": "Nu se pot modifica metadatele pentru {count, plural, one {# element} other {# elemente}}", "cant_get_faces": "Nu pot obține fețe", "cant_get_number_of_comments": "Nu pot obține numărul de comentarii", "cant_search_people": "Nu pot căuta oameni", @@ -538,12 +610,34 @@ "failed_to_create_album": "A eșuat crearea albumului", "failed_to_create_shared_link": "A eșuat crearea legăturii partajate", "failed_to_edit_shared_link": "A eșuat editarea legăturii partajate", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "failed_to_get_people": "Eșec la obținerea persoanelor", + "failed_to_load_asset": "Eșec la încărcarea resursei", + "failed_to_load_assets": "Eșec la încărcarea resurselor", + "failed_to_load_people": "Eșec la încărcarea oamenilor", + "failed_to_remove_product_key": "Eșec la eliminarea cheii de produs", + "failed_to_stack_assets": "Eșec la combinarea resurselor", + "failed_to_unstack_assets": "Eșec la desfășurarea resurselor", + "import_path_already_exists": "Această cale de import există deja.", + "incorrect_email_or_password": "E-mail sau parolă incorect/ă", + "paths_validation_failed": "{paths, plural, one {# cale} other {# căi}} nu a trecut validarea", + "profile_picture_transparent_pixels": "Pozele de profil nu pot avea pixeli transparenți. Te rugăm să mărești imaginea și/sau să o muți.", + "quota_higher_than_disk_size": "Ai stabilit o cotă mai mare decât dimensiunea discului", + "repair_unable_to_check_items": "Imposibil de verificat {count, select, one {element} other {elemente}}", + "unable_to_add_album_users": "Imposibil de adăugat utilizatori în album", + "unable_to_add_assets_to_shared_link": "Imposibil de adăugat resurse la link-ul partajat", + "unable_to_add_comment": "Imposibil de adăugat comentariu", + "unable_to_add_exclusion_pattern": "Nu se poate adăuga modelul de excluziune", + "unable_to_add_import_path": "Imposibil de adăugat calea de import", + "unable_to_add_partners": "Nu se poate de adăuga parteneri", + "unable_to_add_remove_archive": "Nu se poate {archived, select, true {îndepărta resursa din} other {adăuga resursa în}} arhivă", + "unable_to_add_remove_favorites": "Nu se poate {favorite, select, true {adăuga resursa în} other {îndepărta resursa din}} favorite", + "unable_to_archive_unarchive": "Nu se poate {archived, select, true {arhiva} other {dezarhiva}}", + "unable_to_change_album_user_role": "Nu se poate schimba rolul utilizatorului de album", + "unable_to_change_date": "Imposibil de schimbat data", + "unable_to_change_favorite": "Nu se poate modifica favoritele pentru resursă", + "unable_to_change_location": "Imposibil de schimbat locația", + "unable_to_change_password": "Imposibil de schimbat parola", + "unable_to_change_visibility": "Nu se poate schimba vizibilitatea pentru {count, plural, one {# persoană} other {# persoane}}", "unable_to_check_item": "", "unable_to_check_items": "", "unable_to_create_admin_account": "", @@ -565,22 +659,26 @@ "unable_to_remove_album_users": "", "unable_to_remove_comment": "", "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_offline_files": "Nu se pot șterge fișierele offline", + "unable_to_remove_partner": "Imposibil de eliminat partenerul", + "unable_to_remove_reaction": "Nu se poate elimina reația", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", + "unable_to_repair_items": "Imposibil de a repara elementele", + "unable_to_reset_password": "Imposibil de a reseta parola", + "unable_to_resolve_duplicate": "Nu se poate de rezolvat duplicatul", + "unable_to_restore_assets": "Nu se pot restaura resursele", + "unable_to_restore_trash": "Nu se poate restaura coșul de gunoi", + "unable_to_restore_user": "Nu se poate restaura utilizatorul", + "unable_to_save_album": "Imposibil de salvat albumul", + "unable_to_save_api_key": "Imposibil de salvat cheia API", + "unable_to_save_date_of_birth": "Imposibil de a salva data de naștere", + "unable_to_save_name": "Imposibil de a salva numele", + "unable_to_save_profile": "Imposibil de a salva profilul", + "unable_to_save_settings": "Nu se pot salva setările", + "unable_to_scan_libraries": "Nu se pot scana librăriile", + "unable_to_scan_library": "Nu se poate de scanat librăria", + "unable_to_set_feature_photo": "Nu se poate seta fotografia principală", + "unable_to_set_profile_picture": "Nu se poate seta fotografia de profil", "unable_to_submit_job": "", "unable_to_trash_asset": "", "unable_to_unlink_account": "", @@ -797,10 +895,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "Șterge din album", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 6d28e90db0ea1..98b7c2be73bec 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -1,7 +1,7 @@ { "about": "О продукте", "account": "Учётная запись", - "account_settings": "Настройки учётной записи", + "account_settings": "Настройки аккаунта", "acknowledge": "Подтвердить", "action": "Действие", "actions": "Действия", @@ -41,6 +41,7 @@ "confirm_email_below": "Чтобы подтвердить, введите \"{email}\" ниже", "confirm_reprocess_all_faces": "Вы уверены, что хотите повторно определить все лица? Будут также удалены имена со всех лиц.", "confirm_user_password_reset": "Вы уверены, что хотите сбросить пароль пользователя {user}?", + "create_job": "Создать задание", "crontab_guru": "Crontab Guru", "disable_login": "Отключить вход", "disabled": "Выключено", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Разрешение миниатюр", "image_thumbnail_resolution_description": "Используется при просмотре групп фотографий (на временной шкале, при просмотре альбомов и т.д.). Миниатюры с более высоким разрешением сохраняют больше деталей, но требуют больше времени для кодирования, имеют больший вес и могут снизить скорость отклика приложения.", "job_concurrency": "Параллельная обработка задания - {job}", + "job_created": "Задание создано", "job_not_concurrency_safe": "Эта задача не обеспечивает безопасность параллельности выполнения.", "job_settings": "Настройки заданий", "job_settings_description": "Управление параллельной обработкой заданий", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "ПРИМЕЧАНИЕ: Это невозможно изменить позже!", "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты или оставьте пустым", "notification_email_from_address": "Адрес отправителя", - "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server \"", + "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server \"", "notification_email_host_description": "Доменное имя почтового сервера (например, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Игнорировать ошибки сертификата", "notification_email_ignore_certificate_errors_description": "Игнорировать ошибки проверки сертификата TLS (не рекомендуется)", @@ -198,11 +200,12 @@ "password_settings": "Настройки входа с паролем", "password_settings_description": "Управление настройками входа по паролю", "paths_validated_successfully": "Все пути успешно прошли проверку", + "person_cleanup_job": "Очистка персоны", "quota_size_gib": "Размер квоты (ГБ)", "refreshing_all_libraries": "Обновление всех библиотек", "registration": "Регистрация Администратора", "registration_description": "Поскольку вы являетесь первым пользователем в системе, вам будет присвоена роль администратора, и вы будете отвечать за административные задачи. Дополнительных пользователей будете создавать вы.", - "removing_offline_files": "Удаление недоступных файлов", + "removing_deleted_files": "Удаление недоступных файлов", "repair_all": "Починить всё", "repair_matched_items": "Соответствует {count, plural, one {# элементу} few {# элементам} many {# элементам} other {# элементам}}", "repaired_items": "Восстановлено {count, plural, one {# элемент} few {# элемента} many {# элементов} other {# элемента}}", @@ -211,6 +214,7 @@ "reset_settings_to_recent_saved": "Сбросьте настройки к последним сохраненным настройкам", "scanning_library_for_changed_files": "Поиск измененных файлов", "scanning_library_for_new_files": "Поиск новых файлов", + "search_jobs": "Поиск заданий...", "send_welcome_email": "Отправить приветственное письмо", "server_external_domain_settings": "Внешний домен", "server_external_domain_settings_description": "Домен для общедоступных ссылок, включая http(s)://", @@ -238,6 +242,7 @@ "storage_template_settings_description": "Управление структурой папок и именем загружаемого файла", "storage_template_user_label": "{label} - это метка хранилища пользователя", "system_settings": "Системные настройки", + "tag_cleanup_job": "Очистка тега", "theme_custom_css_settings": "Пользовательские CSS", "theme_custom_css_settings_description": "Каскадные таблицы стилей позволяют настраивать дизайн Immich.", "theme_settings": "Настройки темы", @@ -312,6 +317,7 @@ "trash_settings_description": "Управление настройками корзины", "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", "untracked_files_description": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки", + "user_cleanup_job": "Очистка пользователя", "user_delete_delay": "Аккаунт и ресурсы пользователя {user} будут запланированы для окончательного удаления через {delay, plural, one {# день} few {# дня} many {# дней} other {# дня}}.", "user_delete_delay_settings": "Отложенное удаление", "user_delete_delay_settings_description": "Срок в днях, по истечение которого происходит окончательное удаление учетной записи пользователя и его ресурсов после удаления учётной записи. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.", @@ -391,6 +397,7 @@ "asset_offline": "Объект отключён", "asset_offline_description": "Этот объект находится в офлайн-режиме. Immich не может получить доступ к его расположению. Пожалуйста, убедитесь, что объект доступен, и затем пересканируйте библиотеку.", "asset_skipped": "Пропущено", + "asset_skipped_in_trash": "В корзине", "asset_uploaded": "Загружено", "asset_uploading": "Загрузка...", "assets": "Объекты", @@ -660,6 +667,7 @@ "unable_to_get_comments_number": "Не удалось получить количество комментариев", "unable_to_get_shared_link": "Не удалось получить общую ссылку", "unable_to_hide_person": "Невозможно скрыть персону", + "unable_to_link_motion_video": "Не удается связать движущееся видео", "unable_to_link_oauth_account": "Не удается связать учетную запись OAuth", "unable_to_load_album": "Невозможно загрузить альбом", "unable_to_load_asset_activity": "Не удалось загрузить активность объекта", @@ -676,8 +684,8 @@ "unable_to_remove_api_key": "Не удается удалить ключ API", "unable_to_remove_assets_from_shared_link": "Невозможно удалить объекты из общей ссылки", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Не удается удалить автономные файлы", "unable_to_remove_library": "Не удается удалить библиотеку", - "unable_to_remove_offline_files": "Не удается удалить автономные файлы", "unable_to_remove_partner": "Не удается удалить партнера", "unable_to_remove_reaction": "Не удается удалить реакцию", "unable_to_remove_user": "", @@ -700,6 +708,7 @@ "unable_to_submit_job": "Невозможно отправить задание", "unable_to_trash_asset": "Невозможно удалить актив", "unable_to_unlink_account": "Не удалось отсоединить учетную запись", + "unable_to_unlink_motion_video": "Не удается отсоединить движущееся видео", "unable_to_update_album_cover": "Невозможно обновить обложку альбома", "unable_to_update_album_info": "Невозможно обновить информацию об альбоме", "unable_to_update_library": "Не удалось обновить библиотеку", @@ -845,6 +854,7 @@ "license_trial_info_4": "Пожалуйста, рассмотрите возможность приобретения лицензии для поддержки дальнейшего развития проекта", "light": "Светлая", "like_deleted": "Лайк удален", + "link_motion_video": "Ссылка на движущееся видео", "link_options": "Настройки ссылки", "link_to_oauth": "Присоединение к OAuth", "linked_oauth_account": "Присоединённый аккаунт OAuth", @@ -1078,10 +1088,10 @@ "remove_assets_shared_link_confirmation": "Вы уверены, что хотите удалить {count, plural, one {# ресурс} few {# ресурса} many {# ресурсов} other {# ресурса}} из этой общей ссылки?", "remove_assets_title": "Удалить объекты?", "remove_custom_date_range": "Удалить пользовательский диапазон дат", + "remove_deleted_assets": "Удаление автономных файлов", "remove_from_album": "Удалить из альбома", "remove_from_favorites": "Удалить из избранного", "remove_from_shared_link": "Удалить из общей ссылки", - "remove_offline_files": "Удаление автономных файлов", "remove_user": "Удалить пользователя", "removed_api_key": "Удален ключ API: {name}", "removed_from_archive": "Удален из архива", @@ -1134,8 +1144,10 @@ "search_for_existing_person": "Поиск существующего человека", "search_no_people": "Нет людей", "search_no_people_named": "Нет людей с именем \"{name}\"", + "search_options": "Параметры поиска", "search_people": "Поиск людей", "search_places": "Поиск мест", + "search_settings": "Настройки поиска", "search_state": "Поиск региона...", "search_tags": "Поиск по тегам...", "search_timezone": "Поиск часового пояса...", @@ -1288,6 +1300,7 @@ "unknown_album": "Неизвестный альбом", "unknown_year": "Неизвестный Год", "unlimited": "Не ограничено", + "unlink_motion_video": "Отсоединить движущееся видео", "unlink_oauth": "Отключить OAuth", "unlinked_oauth_account": "Отключить аккаунт OAuth", "unnamed_album": "Альбом без названия", @@ -1340,11 +1353,11 @@ "view_album": "Просмотреть альбом", "view_all": "Посмотреть всё", "view_all_users": "Показать всех пользователей", - "view_in_timeline": "Посмотреть на временной шкале", - "view_links": "Посмотреть ссылки", - "view_next_asset": "Посмотреть следующий объект", - "view_previous_asset": "Посмотреть предыдущий объект", - "view_stack": "Просмотреть Стек", + "view_in_timeline": "Показать на временной шкале", + "view_links": "Показать ссылки", + "view_next_asset": "Показать следующий объект", + "view_previous_asset": "Показать предыдущий объект", + "view_stack": "Показать стек", "viewer": "Наблюдатель", "visibility_changed": "Видимость изменена для {count, plural, one {# человека} other {# людей}}", "waiting": "В очереди", diff --git a/web/src/lib/i18n/sk.json b/web/src/lib/i18n/sk.json index d6c066a4cc840..c71677a367149 100644 --- a/web/src/lib/i18n/sk.json +++ b/web/src/lib/i18n/sk.json @@ -112,7 +112,7 @@ "map_reverse_geocoding": "", "map_reverse_geocoding_enable_description": "", "map_reverse_geocoding_settings": "", - "map_settings": "", + "map_settings": "Mapa", "map_settings_description": "", "map_style_description": "", "metadata_extraction_job_description": "", @@ -460,7 +460,7 @@ "expand_all": "", "expire_after": "Expiruje po", "expired": "Vypršalo", - "explore": "", + "explore": "Preskúmať", "extension": "", "external_libraries": "", "failed_to_get_people": "", @@ -656,10 +656,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "Odstrániť z albumu", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", diff --git a/web/src/lib/i18n/sl.json b/web/src/lib/i18n/sl.json index ccd488174b8f8..dd14e5ef947ed 100644 --- a/web/src/lib/i18n/sl.json +++ b/web/src/lib/i18n/sl.json @@ -671,10 +671,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "Odstrani iz albuma", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index d3569146d7209..e6b9524390c4a 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -41,6 +41,7 @@ "confirm_email_below": "Да бисте потврдили, унесите \"{email}\" испод", "confirm_reprocess_all_faces": "Да ли сте сигурни да желите да поново обрадите сва лица? Ово ће такође обрисати именоване особе.", "confirm_user_password_reset": "Да ли сте сигурни да желите да ресетујете лозинку корисника {user}?", + "create_job": "Креирајте посао", "crontab_guru": "Guru servisnih zadataka", "disable_login": "oneмогући пријаву", "disabled": "", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Резолуција сличице", "image_thumbnail_resolution_description": "Користи се приликом прегледа група фотографија (главна временска линија, приказ албума, итд.). Веће резолуције могу да сачувају више детаља, али им је потребно више времена за кодирање, имају веће величине датотека и могу да смање брзину апликације.", "job_concurrency": "{job} паралелност", + "job_created": "Посао креиран", "job_not_concurrency_safe": "Овај посао није безбедан да буде паралелно активан.", "job_settings": "Подешавања посла", "job_settings_description": "Управљајте паралелношћу послова", @@ -139,7 +141,11 @@ "map_settings_description": "Управљајте подешавањима мапе", "map_style_description": "УРЛ до style.json мапе тема изгледа", "metadata_extraction_job": "Извод метаподатака", - "metadata_extraction_job_description": "Извуците информације о метаподацима из сваке датотеке, као што су ГПС и резолуција", + "metadata_extraction_job_description": "Извуците информације о метаподацима из сваке датотеке, као што су GPS, лица и резолуција", + "metadata_faces_import_setting": "Омогући (enable) увоз лица", + "metadata_faces_import_setting_description": "Увезите лица из EXIF података слика и датотека са бочне траке", + "metadata_settings": "Подешавања метаподатака", + "metadata_settings_description": "Управљајте подешавањима метаподатака", "migration_job": "Миграције", "migration_job_description": "Пренесите сличице датотека и лица у најновију структуру директоријума", "no_paths_added": "Нема додатих путања", @@ -148,7 +154,7 @@ "note_cannot_be_changed_later": "НАПОМЕНА: Ovo se kasnije ne može promeniti!", "note_unlimited_quota": "Напомена: Unesite 0 za neograničenu kvotu", "notification_email_from_address": "Са адресе", - "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server \"", + "notification_email_from_address_description": "Адреса е-поште пошиљаоца, на пример: \"Immich foto server \"", "notification_email_host_description": "Хост сервера е-поште (нпр. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Занемарите грешке сертификата", "notification_email_ignore_certificate_errors_description": "Игноришите грешке у валидацији ТЛС сертификата (не препоручује се)", @@ -194,11 +200,12 @@ "password_settings": "Лозинка за пријаву", "password_settings_description": "Управљајте подешавањима за пријаву лозинком", "paths_validated_successfully": "Све путање су успешно потврђене", + "person_cleanup_job": "Чишћење особа", "quota_size_gib": "Величина квоте (ГиБ)", "refreshing_all_libraries": "Освежавање свих библиотека", "registration": "Регистрација администратора", "registration_description": "Пошто сте први корисник на систему, бићете додељени као Админ и одговорни сте за административне задатке, а додатне кориснике ћете креирати ви.", - "removing_offline_files": "Уклањање ванмрежних датотека", + "removing_deleted_files": "Уклањање ванмрежних датотека", "repair_all": "Поправи све", "repair_matched_items": "Поклапа се са {count, plural, one {1 ставком} few {# ставке} other {# ставки}}", "repaired_items": "{count, plural, one {Поправљена 1 ставка} few {Поправљене # ставке} other {Поправљене # ставки}}", @@ -207,6 +214,7 @@ "reset_settings_to_recent_saved": "Ресетујте подешавања на недавно сачувана подешавања", "scanning_library_for_changed_files": "Скенирање библиотеке за промењене датотеке", "scanning_library_for_new_files": "Скенирање библиотеке за нове датотеке", + "search_jobs": "Тражи послове...", "send_welcome_email": "Пошаљите е-пошту добродошлице", "server_external_domain_settings": "Екстерни домаин", "server_external_domain_settings_description": "Домаин за јавне дељене везе, укључујући http(s)://", @@ -234,6 +242,7 @@ "storage_template_settings_description": "Управљајте структуром директоријума и именом датотеке средства за отпремање", "storage_template_user_label": "{label} је ознака за складиштење корисника", "system_settings": "Подешавања система", + "tag_cleanup_job": "Чишћење ознака (tags)", "theme_custom_css_settings": "Прилагођени CSS", "theme_custom_css_settings_description": "Каскадни листови стилова (CSS) омогућавају прилагођавање дизајна Immich-a.", "theme_settings": "Подешавање тема", @@ -308,6 +317,7 @@ "trash_settings_description": "Управљајте подешавањима смећа", "untracked_files": "Непраћене датотеке", "untracked_files_description": "Апликација не прати ове датотеке. one могу настати због неуспешних премештења, због прекинутих отпремања или као преостатак због грешке", + "user_cleanup_job": "Чишћење корисника", "user_delete_delay": "Налог и датотеке {user} биће заказани за трајно брисање за {delay, plural, one {# дан} other {# дана}}.", "user_delete_delay_settings": "Избриши уз кашњење", "user_delete_delay_settings_description": "Број дана након уклањања за трајно брисање корисничког налога и датотека. Посао брисања корисника се покреће у поноћ да би се проверили корисници који су спремни за брисање. Промене ове поставке ће бити процењене при следећем извршењу.", @@ -387,6 +397,7 @@ "asset_offline": "Датотека одсутна", "asset_offline_description": "Ова датотека је ван мреже. Immich не може да приступи локацији своје датотеке. Уверите се да је датотека доступна, а затим поново скенирајте библиотеку.", "asset_skipped": "Прескочено", + "asset_skipped_in_trash": "У отпад", "asset_uploaded": "Отпремљено (Уплоадед)", "asset_uploading": "Отпремање...", "assets": "Записи", @@ -488,7 +499,7 @@ "create_user": "Направи корисника", "created": "Направљен", "current_device": "Тренутни уређај", - "custom_locale": "Прилагођена локација (лоцале)", + "custom_locale": "Прилагођена локација (locale)", "custom_locale_description": "Форматирајте датуме и бројеве на основу језика и региона", "dark": "Тамно", "date_after": "Датум после", @@ -498,7 +509,7 @@ "date_range": "Распон датума", "day": "Дан", "deduplicate_all": "Де-дуплицирај све", - "default_locale": "Подразумевана локација (лоцале)", + "default_locale": "Подразумевана локација (locale)", "default_locale_description": "Форматирајте датуме и бројеве на основу локализације вашег претраживача", "delete": "Обриши", "delete_album": "Обриши албум", @@ -656,6 +667,7 @@ "unable_to_get_comments_number": "Није могуће добити број коментара", "unable_to_get_shared_link": "Преузимање дељене везе није успело", "unable_to_hide_person": "Није могуће сакрити особу", + "unable_to_link_motion_video": "Није могуће повезати (link) видео снимак", "unable_to_link_oauth_account": "Није могуће повезати OAuth налог", "unable_to_load_album": "Није могуће учитати албум", "unable_to_load_asset_activity": "Није могуће учитати активност средстава", @@ -672,8 +684,8 @@ "unable_to_remove_api_key": "Није могуће уклонити АПИ кључ (key)", "unable_to_remove_assets_from_shared_link": "Није могуће уклонити елементе са дељеног linkа", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Није могуће уклонити ванмрежне датотеке", "unable_to_remove_library": "Није могуће уклонити библиотеку", - "unable_to_remove_offline_files": "Није могуће уклонити ванмрежне датотеке", "unable_to_remove_partner": "Није могуће уклонити партнера", "unable_to_remove_reaction": "Није могуће уклонити реакцију", "unable_to_remove_user": "", @@ -696,6 +708,7 @@ "unable_to_submit_job": "Није могуће предати задатак", "unable_to_trash_asset": "Није могуће избацити материјал у отпад", "unable_to_unlink_account": "Није могуће раскинути профил", + "unable_to_unlink_motion_video": "Није могуће прекинути везу са видео снимком", "unable_to_update_album_cover": "Није могуће ажурирати насловницу албума", "unable_to_update_album_info": "Није могуће ажурирати информације о албуму", "unable_to_update_library": "Није могуће ажурирати библиотеку", @@ -841,6 +854,7 @@ "license_trial_info_4": "Молимо вас да размислите о куповини лиценце за подршку континуираном развоју услуге", "light": "Светло", "like_deleted": "Лајкуј избрисано", + "link_motion_video": "Направи везу за видео запис", "link_options": "Опције везе", "link_to_oauth": "Веза до OAuth-a", "linked_oauth_account": "Повезани OAuth налог", @@ -1074,10 +1088,10 @@ "remove_assets_shared_link_confirmation": "Да ли сте сигурни да желите да уклоните {count, plural, one {# датотеку} other {# датотеке}} са ове дељене везе?", "remove_assets_title": "Уклонити датотеке?", "remove_custom_date_range": "Уклоните прилагођени период", + "remove_deleted_assets": "Уклоните ванмрежне (offline) датотеке", "remove_from_album": "Обриши из албума", "remove_from_favorites": "Уклони из фаворита", "remove_from_shared_link": "Уклоните са дељене везе", - "remove_offline_files": "Уклоните ванмрежне (offline) датотеке", "remove_user": "Уклони корисника", "removed_api_key": "Уклоњен АПИ кључ (key): {name}", "removed_from_archive": "Уклоњено из архиве", @@ -1130,8 +1144,10 @@ "search_for_existing_person": "Потражите постојећу особу", "search_no_people": "Без особа", "search_no_people_named": "Нема особа са именом „{name}“", + "search_options": "Опције претраге", "search_people": "Претражи особе", "search_places": "Претражи места", + "search_settings": "Претрага подешавања", "search_state": "Тражи регион...", "search_tags": "Претражи ознаке (tags)...", "search_timezone": "Претражи временску зону...", @@ -1208,6 +1224,8 @@ "sign_up": "Пријави се", "size": "Величина", "skip_to_content": "Пређи на садржај", + "skip_to_folders": "Прескочи на фасцикле", + "skip_to_tags": "Прескочи на ознаке (tags)", "slideshow": "Слајдови", "slideshow_settings": "Подешавања слајдова", "sort_albums_by": "Сортирај албуме по...", @@ -1282,6 +1300,7 @@ "unknown_album": "Nepoznat Album", "unknown_year": "Непозната Година", "unlimited": "Неограничено", + "unlink_motion_video": "Прекините везу са видео снимком", "unlink_oauth": "Прекини везу са Oauth-om", "unlinked_oauth_account": "Опозвана веза OAuth налога", "unnamed_album": "Неименовани албум", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index 8b0c53ded0a88..b8fec177e88c1 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -41,6 +41,7 @@ "confirm_email_below": "Da biste potvrdili, unesite \"{email}\" ispod", "confirm_reprocess_all_faces": "Da li ste sigurni da želite da ponovo obradite sva lica? Ovo će takođe obrisati imenovane osobe.", "confirm_user_password_reset": "Da li ste sigurni da želite da resetujete lozinku korisnika {user}?", + "create_job": "Kreirajte posao", "crontab_guru": "Guru servisnih zadataka", "disable_login": "Onemogući prijavu", "disabled": "", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Rezolucija sličice", "image_thumbnail_resolution_description": "Koristi se prilikom pregleda grupa fotografija (glavna vremenska linija, prikaz albuma, itd.). Veće rezolucije mogu da sačuvaju više detalja, ali im je potrebno više vremena za kodiranje, imaju veće veličine datoteka i mogu da smanje brzinu aplikacije.", "job_concurrency": "{job} paralelnost", + "job_created": "Posao kreiran", "job_not_concurrency_safe": "Ovaj posao nije bezbedan da bude paralelno aktivan.", "job_settings": "Podešavanja posla", "job_settings_description": "Upravljajte paralelnošću poslova", @@ -139,7 +141,11 @@ "map_settings_description": "Upravljajte podešavanjima mape", "map_style_description": "URL do style.json mape tema izgleda", "metadata_extraction_job": "Izvod metapodataka", - "metadata_extraction_job_description": "Izvucite informacije o metapodacima iz svake datoteke, kao što su GPS i rezolucija", + "metadata_extraction_job_description": "Izvucite informacije o metapodacima iz svake datoteke, kao što su GPS, lica i rezolucija", + "metadata_faces_import_setting": "Omogućite (enable) dodavanje lica", + "metadata_faces_import_setting_description": "Dodajte lica iz EXIF podataka slike i sličnih metapodataka", + "metadata_settings": "Podešavanje metapodataka", + "metadata_settings_description": "Upravljajte podešavanjima metapodataka", "migration_job": "Migracije", "migration_job_description": "Prenesite sličice datoteka i lica u najnoviju strukturu direktorijuma", "no_paths_added": "Nema dodatih putanja", @@ -148,7 +154,7 @@ "note_cannot_be_changed_later": "NAPOMENA: Ovo se kasnije ne može promeniti!", "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu", "notification_email_from_address": "Sa adrese", - "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server \"", + "notification_email_from_address_description": "Adresa e-pošte pošiljaoca, na primer: \"Immich foto server \"", "notification_email_host_description": "Host servera e-pošte (npr. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Zanemarite greške sertifikata", "notification_email_ignore_certificate_errors_description": "Ignorišite greške u validaciji TLS sertifikata (ne preporučuje se)", @@ -194,11 +200,12 @@ "password_settings": "Lozinka za prijavu", "password_settings_description": "Upravljajte podešavanjima za prijavu lozinkom", "paths_validated_successfully": "Sve putanje su uspešno potvrđene", + "person_cleanup_job": "Čišćenje osoba", "quota_size_gib": "Veličina kvote (GiB)", "refreshing_all_libraries": "Osvežavanje svih biblioteka", "registration": "Registracija administratora", "registration_description": "Pošto ste prvi korisnik na sistemu, bićete dodeljeni kao Admin i odgovorni ste za administrativne zadatke, a dodatne korisnike ćete kreirati vi.", - "removing_offline_files": "Uklanjanje vanmrežnih datoteka", + "removing_deleted_files": "Uklanjanje vanmrežnih datoteka", "repair_all": "Popravi sve", "repair_matched_items": "Poklapa se sa {count, plural, one {1 stavkom} few {# stavke} other {# stavki}}", "repaired_items": "{count, plural, one {Popravljena 1 stavka} few {Popravljene # stavke} other {Popravljene # stavki}}", @@ -207,6 +214,7 @@ "reset_settings_to_recent_saved": "Resetujte podešavanja na nedavno sačuvana podešavanja", "scanning_library_for_changed_files": "Skeniranje biblioteke za promenjene datoteke", "scanning_library_for_new_files": "Skeniranje biblioteke za nove datoteke", + "search_jobs": "Traži poslove...", "send_welcome_email": "Pošaljite e-poštu dobrodošlice", "server_external_domain_settings": "Eksterni domain", "server_external_domain_settings_description": "Domain za javne deljene veze, uključujući http(s)://", @@ -234,6 +242,7 @@ "storage_template_settings_description": "Upravljajte strukturom direktorijuma i imenom datoteke sredstva za otpremanje", "storage_template_user_label": "{label} je oznaka za skladištenje korisnika", "system_settings": "Podešavanja sistema", + "tag_cleanup_job": "Čišćenje oznaka (tags)", "theme_custom_css_settings": "Prilagođeni CSS", "theme_custom_css_settings_description": "Kaskadni listovi stilova (CSS) omogućavaju prilagođavanje dizajna Immich-a.", "theme_settings": "Podešavanje tema", @@ -308,6 +317,7 @@ "trash_settings_description": "Upravljajte podešavanjima smeća", "untracked_files": "Nepraćene datoteke", "untracked_files_description": "Aplikacija ne prati ove datoteke. One mogu nastati zbog neuspešnih premeštenja, zbog prekinutih otpremanja ili kao preostatak zbog greške", + "user_cleanup_job": "Čišćenje korisnika", "user_delete_delay": "Nalog i datoteke {user} biće zakazani za trajno brisanje za {delay, plural, one {# dan} other {# dana}}.", "user_delete_delay_settings": "Izbriši uz kašnjenje", "user_delete_delay_settings_description": "Broj dana nakon uklanjanja za trajno brisanje korisničkog naloga i datoteka. Posao brisanja korisnika se pokreće u ponoć da bi se proverili korisnici koji su spremni za brisanje. Promene ove postavke će biti procenjene pri sledećem izvršenju.", @@ -387,6 +397,7 @@ "asset_offline": "Datoteka odsutna", "asset_offline_description": "Ova datoteka je van mreže. Immich ne može da pristupi lokaciji svoje datoteke. Uverite se da je datoteka dostupna, a zatim ponovo skenirajte biblioteku.", "asset_skipped": "Preskočeno", + "asset_skipped_in_trash": "U otpad", "asset_uploaded": "Otpremljeno (Uploaded)", "asset_uploading": "Otpremanje...", "assets": "Zapisi", @@ -656,6 +667,7 @@ "unable_to_get_comments_number": "Nije moguće dobiti broj komentara", "unable_to_get_shared_link": "Preuzimanje deljene veze nije uspelo", "unable_to_hide_person": "Nije moguće sakriti osobu", + "unable_to_link_motion_video": "Nije moguće povezati video sa slikom", "unable_to_link_oauth_account": "Nije moguće povezati OAuth nalog", "unable_to_load_album": "Nije moguće učitati album", "unable_to_load_asset_activity": "Nije moguće učitati aktivnost sredstava", @@ -672,8 +684,8 @@ "unable_to_remove_api_key": "Nije moguće ukloniti API ključ (key)", "unable_to_remove_assets_from_shared_link": "Nije moguće ukloniti elemente sa deljenog linka", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Nije moguće ukloniti vanmrežne datoteke", "unable_to_remove_library": "Nije moguće ukloniti biblioteku", - "unable_to_remove_offline_files": "Nije moguće ukloniti vanmrežne datoteke", "unable_to_remove_partner": "Nije moguće ukloniti partnera", "unable_to_remove_reaction": "Nije moguće ukloniti reakciju", "unable_to_remove_user": "", @@ -696,6 +708,7 @@ "unable_to_submit_job": "Nije moguće predati zadatak", "unable_to_trash_asset": "Nije moguće izbaciti materijal u otpad", "unable_to_unlink_account": "Nije moguće raskinuti profil", + "unable_to_unlink_motion_video": "Nije moguće odvezati video od slike", "unable_to_update_album_cover": "Nije moguće ažurirati naslovnicu albuma", "unable_to_update_album_info": "Nije moguće ažurirati informacije o albumu", "unable_to_update_library": "Nije moguće ažurirati biblioteku", @@ -841,6 +854,7 @@ "license_trial_info_4": "Molimo vas da razmislite o kupovini licence za podršku kontinuiranom razvoju usluge", "light": "Svetlo", "like_deleted": "Lajkuj izbrisano", + "link_motion_video": "Napravi vezu za video zapis", "link_options": "Opcije veze", "link_to_oauth": "Veza do OAuth-a", "linked_oauth_account": "Povezani OAuth nalog", @@ -1074,10 +1088,10 @@ "remove_assets_shared_link_confirmation": "Da li ste sigurni da želite da uklonite {count, plural, one {# datoteku} other {# datoteke}} sa ove deljene veze?", "remove_assets_title": "Ukloniti datoteke?", "remove_custom_date_range": "Uklonite prilagođeni period", + "remove_deleted_assets": "Uklonite vanmrežne (offline) datoteke", "remove_from_album": "Obriši iz albuma", "remove_from_favorites": "Ukloni iz favorita", "remove_from_shared_link": "Uklonite sa deljene veze", - "remove_offline_files": "Uklonite vanmrežne (offline) datoteke", "remove_user": "Ukloni korisnika", "removed_api_key": "Uklonjen API ključ (key): {name}", "removed_from_archive": "Uklonjeno iz arhive", @@ -1130,8 +1144,10 @@ "search_for_existing_person": "Potražite postojeću osobu", "search_no_people": "Bez osoba", "search_no_people_named": "Nema osoba sa imenom „{name}“", + "search_options": "Opcije pretrage", "search_people": "Pretraži osobe", "search_places": "Pretraži mesta", + "search_settings": "Pretraga podešavanja", "search_state": "Traži region...", "search_tags": "Pretraži oznake (tags)...", "search_timezone": "Pretraži vremensku zonu...", @@ -1208,6 +1224,8 @@ "sign_up": "Prijavi se", "size": "Veličina", "skip_to_content": "Pređi na sadržaj", + "skip_to_folders": "Preskoči do mapa (folders)", + "skip_to_tags": "Preskoči do oznaka (tags)", "slideshow": "Slajdovi", "slideshow_settings": "Podešavanja slajdova", "sort_albums_by": "Sortiraj albume po...", @@ -1282,6 +1300,7 @@ "unknown_album": "Nepoznat Album", "unknown_year": "Nepoznata Godina", "unlimited": "Neograničeno", + "unlink_motion_video": "Odveži video od slike", "unlink_oauth": "Prekini vezu sa Oauth-om", "unlinked_oauth_account": "Opozvana veza OAuth naloga", "unnamed_album": "Neimenovani album", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index 2a02ac7138f7f..7cbcfcc680e54 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -152,7 +152,7 @@ "note_cannot_be_changed_later": "OBS: Detta kan inte ändras i efterhand!", "note_unlimited_quota": "OBS: Skriv 0 för obegränsad kvota", "notification_email_from_address": "Från adress", - "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver \"", + "notification_email_from_address_description": "Avsändarens epost, t.ex.: \"Immich Fotoserver \"", "notification_email_host_description": "Värd för epostservern (t.ex. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignorera certifikatfel", "notification_email_ignore_certificate_errors_description": "Ignorera valideringsfel för TLS-certifikat (rekommenderas ej)", @@ -192,7 +192,7 @@ "oauth_storage_quota_claim_description": "Sätter automatiskt angiven användares lagringskvot.", "oauth_storage_quota_default": "Standardlagringskvot (GiB)", "oauth_storage_quota_default_description": "Kvot i GiB som används när ingen fordran angetts (Ange 0 för obegränsad kvot).", - "offline_paths": "Offline-sökvägar", + "offline_paths": "Filer som inte kan hittas", "offline_paths_description": "Dessa resultat kan bero på manuell borttagning av filer som inte är en del av ett externt bibliotek.", "password_enable_description": "Logga in med epost och lösenord", "password_settings": "Lösenordsinloggning", @@ -202,7 +202,7 @@ "refreshing_all_libraries": "Samtliga bibliotek uppdateras", "registration": "Administratörsregistrering", "registration_description": "Du utses till administratör eftersom du är systemets första användare. Du ansvarar för administration och kan skapa ytterligare användare.", - "removing_offline_files": "Tar bort offline-filer", + "removing_deleted_files": "Tar bort offline-filer", "repair_all": "Reparera alla", "repair_matched_items": "Matchade {count, plural, one {# föremål} other {# föremål}}", "repaired_items": "Reparerade {count, plural, one {# item} other {# items}}", @@ -235,10 +235,11 @@ "storage_template_onboarding_description": "Vid aktivering organiserar denna funktion automatiskt filer baserat på en användardefinierad mall. På grunda av stabilitetsproblem är denna funktion avstängd som standard, för mer information se dokumentation.", "storage_template_path_length": "Uppskattad längdbegränsning på sökväg: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmall", - "storage_template_settings_description": "", + "storage_template_settings_description": "Hantera mappstruktur och filnamn för uppladdade resurser", + "storage_template_user_label": "{label} är användarens lagringsmärkning", "system_settings": "Systeminställningar", "theme_custom_css_settings": "Anpassad CSS", - "theme_custom_css_settings_description": "", + "theme_custom_css_settings_description": "Cascading Style Sheets möjliggör designanpassningar av Immich", "theme_settings": "Temainställningar", "theme_settings_description": "Hantera anpassningar av webbgränssnittet för Immich", "these_files_matched_by_checksum": "Dessa filer matchas av deras kontrollsummor", @@ -254,6 +255,7 @@ "transcoding_accepted_audio_codecs": "Accepterade ljud-codecs", "transcoding_accepted_audio_codecs_description": "Välj vilka ljud-codecs som inte behöver omkodas. Används endast för vissa omkodningspolicyer.", "transcoding_accepted_containers": "Accepterade behållare", + "transcoding_accepted_containers_description": "Välj vilka kontainerformat som inte behöver remuxas till MP4. Endast används för vissa transcoding-politischer.", "transcoding_accepted_video_codecs": "Accepterade video-codecs", "transcoding_accepted_video_codecs_description": "Välj vilka video-codecs som inte behöver omkodas. Används endast för vissa omkodningspolicyer.", "transcoding_advanced_options_description": "Val som de flesta användare inte bör behöva ändra", @@ -261,37 +263,37 @@ "transcoding_audio_codec_description": "Opus är bästa kvalitetsvalet, men är inte lika kompatibelt med äldre enheter eller mjukvara.", "transcoding_bitrate_description": "Videor som är i högre än max bithastighet eller inte i ett accepterat format", "transcoding_codecs_learn_more": "För att läsa mer om terminologin här se FFmpeg-dokumentationen för H.264 kodek, HEVC kodek och VP9 kodek.", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", + "transcoding_constant_quality_mode": "Konstant kvalitetsläge", + "transcoding_constant_quality_mode_description": "ICQ är bättre än CQP, men vissa hårdvaruaccelerationsenheter stöder inte detta läge. Om det här alternativet är inställt föredras det angivna läget när kvalitetsbaserad kodning används. NVENC ignoreras eftersom det inte stöder ICQ.", + "transcoding_constant_rate_factor": "Konstant hastighetsfaktor (-crf)", + "transcoding_constant_rate_factor_description": "Nivå på videokvalitet. Typiska värden är 23 för H.264, 28 för HEVC, 31 för VP9 och 35 för AV1. Lägre är bättre, men producerar större filer.", + "transcoding_disabled_description": "Omkoda inte videofiler, detta kan störa uppspelning på vissa klienter", "transcoding_hardware_acceleration": "Hardvaruacceleration", - "transcoding_hardware_acceleration_description": "", + "transcoding_hardware_acceleration_description": "Forskningsmässig; betydligt snabbare men med lägre kvalitet vid samma biträtta", "transcoding_hardware_decoding": "Hårdvaruavkodning", - "transcoding_hardware_decoding_setting_description": "", + "transcoding_hardware_decoding_setting_description": "Tillämpas enbart på NVENC, QSV och RKMPP. Aktiverar end-to-end accelerering i stället för endast kodningsacceleration. Fungerar inte med alla videor.", "transcoding_hevc_codec": "HEVC-codec", - "transcoding_max_b_frames": "", + "transcoding_max_b_frames": "Max B-ramar", "transcoding_max_b_frames_description": "Högre värden förbättrar kompressionseffektiviteten, men saktar ner kodningen. Kan vara inkompatibel med hårdvaruacceleration på äldre enheter. 0 avaktiverar B-frames, medan -1 anger detta värde automatiskt.", "transcoding_max_bitrate": "Max bithastighet", - "transcoding_max_bitrate_description": "", + "transcoding_max_bitrate_description": "En maximal bitrate kan göra filstorlekar mer förutsägbara till en liten kostnad på kvalitet. Vid 720p är typiska värden 2600k för VP9 eller HEVC, eller 4500k för H.264. Inaktiverad om satt till 0.", "transcoding_max_keyframe_interval": "Max nyckelbildruteintervall", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", + "transcoding_max_keyframe_interval_description": "Sätter det maximala bildruteavståndet mellan nyckelbildrutor. Lägre värden försämrar kompressionseffektiviteten, men förbättrar söktiderna och kan förbättra kvaliteten i scener med snabb rörelse. 0 ställer in detta värde automatiskt.", + "transcoding_optimal_description": "Videor som är högre än mållösning eller inte i ett accepterat format", + "transcoding_preferred_hardware_device": "Föredragen hårdvaruenhet", + "transcoding_preferred_hardware_device_description": "Gäller enbart VAAPI och QSV. Ställer in dri-läget som används för hårdvaruomkodning.", "transcoding_preset_preset": "Förinställning (-preset)", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", + "transcoding_preset_preset_description": "Kompressionshastighet. Långsammare preset ger mindre filer och högre kvalitet för en given bitrate. VP9 ignorerar hastigheter högre än 'faster'.", + "transcoding_reference_frames": "Referensbildrutor", + "transcoding_reference_frames_description": "Antalet bildrutor som tas i beaktande när en given bildruta ska komprimeras. Högre värden ger effektivare kompression på bekostnad av långsammare kodning. 0 ställer in detta värde automatiskt.", + "transcoding_required_description": "Enbart videos som inte är ett accepterat format", + "transcoding_settings": "Inställningar för omkodning av video", + "transcoding_settings_description": "Hantera upplösningen och kodningen av videofiler", "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", + "transcoding_target_resolution_description": "En högre upplösning kan bevara fler detaljer men kan ta längre tid at koda, ha större fil storlek och kan försämra appens svarstid.", "transcoding_temporal_aq": "", "transcoding_temporal_aq_description": "", - "transcoding_threads": "", + "transcoding_threads": "Trådar", "transcoding_threads_description": "", "transcoding_tone_mapping": "", "transcoding_tone_mapping_description": "", @@ -732,10 +734,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "Ta bort från album", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", diff --git a/web/src/lib/i18n/ta.json b/web/src/lib/i18n/ta.json index ec3f27124bdc2..175dcd9bc96e1 100644 --- a/web/src/lib/i18n/ta.json +++ b/web/src/lib/i18n/ta.json @@ -143,7 +143,7 @@ "note_cannot_be_changed_later": "குறிப்பு: இதை பின்னர் மாற்ற முடியாது!", "note_unlimited_quota": "குறிப்பு: வரம்பற்ற ஒதுக்கீட்டிற்கு 0 ஐ உள்ளிடவும்", "notification_email_from_address": "முகவரியிலிருந்து", - "notification_email_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் \"", + "notification_email_from_address_description": "அனுப்புநரின் மின்னஞ்சல் முகவரி, எடுத்துக்காட்டாக: \"இம்மிச் புகைப்பட சேவையகம் \"", "notification_email_host_description": "மின்னஞ்சல் சேவையகத்தின் ஹோஸ்ட் (எடுத்துக்காட்டாக: smtp.immich.app)", "notification_email_ignore_certificate_errors": "சான்றிதழ் பிழைகளை புறக்கணிக்கவும்", "notification_email_ignore_certificate_errors_description": "TLS சான்றிதழ் சரிபார்ப்பு பிழைகளை புறக்கணிக்கவும் (பரிந்துரைக்கப்படவில்லை)", @@ -191,7 +191,7 @@ "refreshing_all_libraries": "அனைத்து நூலகங்களையும் புதுப்பிக்கிறது", "registration": "நிர்வாக பதிவு", "registration_description": "நீங்கள் கணினியில் முதல் பயனராக இருப்பதால், நீங்கள் நிர்வாகியாக நியமிக்கப்படுவீர்கள் மற்றும் நிர்வாகப் பணிகளுக்குப் பொறுப்பாவீர்கள், மேலும் உங்களால் கூடுதல் பயனர்கள் உருவாக்கப்படுவார்கள்.", - "removing_offline_files": "ஆஃப்லைன் கோப்புகளை நீக்குகிறது", + "removing_deleted_files": "ஆஃப்லைன் கோப்புகளை நீக்குகிறது", "repair_all": "அனைத்தையும் பழுதுபார்க்கவும்", "repair_matched_items": "பொருந்தியது {count, plural, one {# உருப்படி} other {# உருப்படிகள்}}", "repaired_items": "பழுதுபார்க்கப்பட்டது {count, plural, one {# உருப்படி} other {# உருப்படிகள்}}", @@ -516,8 +516,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "", @@ -758,10 +758,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "rename": "", "repair": "", diff --git a/web/src/lib/i18n/th.json b/web/src/lib/i18n/th.json index 19496b423843f..83675d504031d 100644 --- a/web/src/lib/i18n/th.json +++ b/web/src/lib/i18n/th.json @@ -25,7 +25,7 @@ "add_to_shared_album": "เพิ่มเข้าอัลบั้มที่แชร์", "added_to_archive": "เพิ่มเข้าที่เก็บถาวร", "added_to_favorites": "เพิ่มเข้ารายการโปรด", - "added_to_favorites_count": "{count} รูปถูกเพิ่มเข้ารายการโปรด", + "added_to_favorites_count": "{count, number} รูปถูกเพิ่มเข้ารายการโปรด", "admin": { "add_exclusion_pattern_description": "เพิ่มรูปแบบการยกเว้น การ Glob โดยใช้ *, ** และ ? ถูกรองรับ ถ้าต้องการละเว้นไฟล์ทั้งหมดในไดเร็กทอรีใดๆที่ชื่อว่า \"Raw\" ให้ใช้ \"**/Raw/**\" ถ้าต้องการละเว้นไฟล์ทั้งหมดที่ลงท้ายด้วย \".tif\" ให้ใช้ \"**/*.tif\" ถ้าต้องการละเว้นพาธที่เริ่มจากไดเรกทอรีบนสุดให้ใช้ \"/พาธ/ที่ต้องการ/ละเว้น/**\"", "authentication_settings": "ตั้งค่าการเข้าถึง", @@ -129,16 +129,21 @@ "map_enable_description": "เปิดใช้งานแผนที่", "map_gps_settings": "การตั้งค่าแผนที่และ GPS", "map_gps_settings_description": "จัดการการตั้งค่าแผนที่และ GPS (Reverse Geocoding)", + "map_implications": "ฟีเจอร์แผนที่ต้องการบริการแผ่นแผนที่จากภายนอก (tiles.immich.cloud)", "map_light_style": "แบบสว่าง", "map_manage_reverse_geocoding_settings": "จัดการการตั้งค่าแปลงพิกัดภูมิศาสตร์ ", "map_reverse_geocoding": "ประมวลผลชื่อทางภูมิศาสตร์", "map_reverse_geocoding_enable_description": "เปิดใช้งานประมวลผลชื่อทางภูมิศาสตร์", "map_reverse_geocoding_settings": "การตั้งค่าประมวลผลชื่อทางภูมิศาสตร์", - "map_settings": "การตั้งค่าแผนที่", + "map_settings": "แผนที่", "map_settings_description": "จัดการการตั้งค่าแผนที่", "map_style_description": "URL ไปยังธีมแผนที่ style.json", "metadata_extraction_job": "ดึงข้อมูล metadata", - "metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS และความละเอียด", + "metadata_extraction_job_description": "ดึงข้อมูล metadata จากสื่อ เช่น GPS ใบหน้าและความละเอียด", + "metadata_faces_import_setting": "เปิดการนำเข้าข้อมูลใบหน้า", + "metadata_faces_import_setting_description": "นำเข้าข้อมูลใบหน้าจาก EXIF ของไฟล์ภาพและไฟล์ประกอบ", + "metadata_settings": "การตั้งค่า Metadata", + "metadata_settings_description": "จัดการการตั้งค่า Metadata", "migration_job": "การโยกย้าย", "migration_job_description": "ย้ายภาพตัวอย่างสื่อและใบหน้าไปยังโครงสร้างโฟลเดอร์ล่าสุด", "no_paths_added": "ไม่ได้เพิ่มพาธ", @@ -147,7 +152,7 @@ "note_cannot_be_changed_later": "หมายเหตุ: ไม่สามารถเปลี่ยนภายหลังได้!", "note_unlimited_quota": "หมายเหตุ: ใส่เลข 0 สําหรับโควต้าไม่จํากัด", "notification_email_from_address": "จากที่อยู่", - "notification_email_from_address_description": "อีเมลผู้ส่ง อย่างเช่น \"Immich Photo Server \"", + "notification_email_from_address_description": "อีเมลผู้ส่ง อย่างเช่น \"Immich Photo Server \"", "notification_email_host_description": "ที่อยู่เซิร์ฟเวอร์อีเมล (เช่น smtp.immich.app)", "notification_email_ignore_certificate_errors": "ไม่สนใจข้อผิดพลาดเกี่ยวกับใบรับรอง", "notification_email_ignore_certificate_errors_description": "ไม่สนใจการยืนยันใบรับรอง TLS ผิดพลาด (ไม่แนะนำ)", @@ -173,7 +178,9 @@ "oauth_issuer_url": "ผู้ออก URL", "oauth_mobile_redirect_uri": "URI เปลี่ยนเส้นทางบนโทรศัพท์", "oauth_mobile_redirect_uri_override": "แทนที่ URI เปลี่ยนเส้นทางบนโทรศัพท์", - "oauth_mobile_redirect_uri_override_description": "เปิดเมื่อ 'app.immich:/' เป็น URI เปลี่ยนเส้นทางที่ไม่ถูกต้อง", + "oauth_mobile_redirect_uri_override_description": "เปิดเมื่อ OAuth ไม่รองรับ URI บนอุปกรณ์ เช่น '{callback}'", + "oauth_profile_signing_algorithm": "อัลกอริทึมการรับรองบัญชีผู้ใช้", + "oauth_profile_signing_algorithm_description": "อัลกอริทึมใช้ในการรับรองบัญชีผู้ใช้", "oauth_scope": "ขอบเขต", "oauth_settings": "OAuth", "oauth_settings_description": "จัดการการตั้งค่าล็อกอินผ่าน OAuth", @@ -195,7 +202,7 @@ "refreshing_all_libraries": "รีเฟรชคลังภาพทั้งหมด", "registration": "ลงทะเบียนผู้จัดการ", "registration_description": "เนื่องจากคุณเป็นผู้ใช้งานแรกของระบบ คุณจะถูกแต่งตั้งเป็นผู้จัดการและรับผิดชอบงานบริหาร ผู้ใช้งานเพิ่มเติมจะถูกสร้างโดยคุณ", - "removing_offline_files": "กำลังลบไฟล์ออฟไลน์", + "removing_deleted_files": "กำลังลบไฟล์ออฟไลน์", "repair_all": "ซ่อมแซมทั้งหมด", "repair_matched_items": "จับคู่ {count, plural, one {# รายการ} other {# รายการ}}", "repaired_items": "ซ่อมแซม {count, plural, one {# รายการ} other {# รายการ}}", @@ -722,10 +729,10 @@ "refreshed": "ถูกรีเฟรช", "refreshes_every_file": "", "remove": "เอาออก", + "remove_deleted_assets": "", "remove_from_album": "ลบออกจากอัลบั้ม", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "repair": "ซ่อม", "repair_no_results_message": "", "replace_with_upload": "", @@ -818,7 +825,7 @@ "status": "สถานะ", "stop_motion_photo": "", "stop_photo_sharing": "หยุดแชร์รูปภาพ?", - "storage": "ที่จัดเก็บ", + "storage": "พื้นที่จัดเก็บ", "storage_label": "", "submit": "ส่ง", "suggestions": "ข้อเสนอแนะ", diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 4fefbf2f2132a..9a060cb1091fa 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -151,7 +151,7 @@ "note_cannot_be_changed_later": "NOT: Bu daha sonra değiştirilemez!", "note_unlimited_quota": "NOT: Sınırsız kota için 0 yazın", "notification_email_from_address": "Şu adresten", - "notification_email_from_address_description": "Göndericinin email adresi, örnek: \"Immich Fotoğraf Sunucusu \"", + "notification_email_from_address_description": "Göndericinin email adresi, örnek: \"Immich Fotoğraf Sunucusu \"", "notification_email_host_description": "E-posta sunucusunun ana bilgisayarı (örneğin, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Sertifika hatalarını görmezden gel", "notification_email_ignore_certificate_errors_description": "TLS sertifika doğrulama ayarlarını görmezden gel (Önerilmez)", @@ -201,7 +201,7 @@ "refreshing_all_libraries": "Tüm kütüphaneler yenileniyor", "registration": "Yönetici kaydı", "registration_description": "Sistemdeki ilk kullanıcı olduğunuz için hesabınız Yönetici olarak ayarlandı. Yeni oluşturulan üyeliklerin, ve yönetici görevlerinin sorumlusu olarak atandınız.", - "removing_offline_files": "Çevrimdışı dosyalar kaldırılıyor", + "removing_deleted_files": "Çevrimdışı dosyalar kaldırılıyor", "repair_all": "Tümünü onar", "repair_matched_items": "Eşleşen {sayı, çoğul, bir {# öğe} diğer {# öğeler}}", "repaired_items": "{count, plural, one {# item} other {# items}} tamir edildi", @@ -601,8 +601,8 @@ "unable_to_refresh_user": "", "unable_to_remove_album_users": "", "unable_to_remove_api_key": "", + "unable_to_remove_deleted_assets": "", "unable_to_remove_library": "Kütüphane kaldırılamadı", - "unable_to_remove_offline_files": "", "unable_to_remove_partner": "", "unable_to_remove_reaction": "", "unable_to_repair_items": "Ögeler onarılamadı", @@ -860,10 +860,10 @@ "refreshed": "", "refreshes_every_file": "", "remove": "Kaldır", + "remove_deleted_assets": "", "remove_from_album": "", "remove_from_favorites": "", "remove_from_shared_link": "", - "remove_offline_files": "", "removed_api_key": "", "removed_from_favorites": "Favorilerden kaldırıldı", "rename": "Yeniden adlandır", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index 1d5fe69dd325d..5efa43bb5f009 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -41,6 +41,7 @@ "confirm_email_below": "Для підтвердження введіть \"{email}\" нижче", "confirm_reprocess_all_faces": "Ви впевнені, що хочете повторно визначити всі обличчя? Це також призведе до видалення імен з усіх облич.", "confirm_user_password_reset": "Ви впевнені, що хочете скинути пароль користувача {user}?", + "create_job": "Створити завдання", "crontab_guru": "", "disable_login": "Вимкнути вхід", "disabled": "", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Розмір ескізу", "image_thumbnail_resolution_description": "Використовується при перегляді груп фотографій (основна стрічка, перегляд альбому тощо). Вища роздільна здатність може зберегти більше деталей, але вимагає більше часу для кодування, має більший розмір файлів і може знижувати чутливість додатку.", "job_concurrency": "{job} одночасно", + "job_created": "Завдання створено", "job_not_concurrency_safe": "Це завдання не є безпечним для одночасного виконання.", "job_settings": "Налаштування завдань", "job_settings_description": "Управління паралельністю завдань", @@ -139,7 +141,11 @@ "map_settings_description": "Управління налаштуваннями мапи", "map_style_description": "URL до теми мапи у форматі style.json", "metadata_extraction_job": "Витягнути метадані", - "metadata_extraction_job_description": "Витягнення метаданих інформації з кожного ресурсу, таких як GPS-координати та роздільність", + "metadata_extraction_job_description": "Витягни метадані з кожного об'єкта, таку як GPS, обличчя та роздільна здатність", + "metadata_faces_import_setting": "Увімкни імпорт облич", + "metadata_faces_import_setting_description": "Імпортуй обличчя з EXIF-даних зображень та додаткових файлів", + "metadata_settings": "Налаштування метаданих", + "metadata_settings_description": "Керуй налаштуваннями метаданих", "migration_job": "Міграція", "migration_job_description": "Перемістіть мініатюри для ресурсів та обличчя до оновленої структури папок", "no_paths_added": "Шляхи не додано", @@ -148,7 +154,7 @@ "note_cannot_be_changed_later": "ПРИМІТКА: Це не можна змінити пізніше!", "note_unlimited_quota": "Примітка: Введіть 0 для необмеженого обсягу квоти", "notification_email_from_address": "З адреси", - "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server \"", + "notification_email_from_address_description": "Адреса електронної пошти відправника, наприклад: \"Immich Photo Server \"", "notification_email_host_description": "Хост поштового сервера (наприклад, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ігнорувати помилки сертифіката", "notification_email_ignore_certificate_errors_description": "Ігнорувати помилки перевірки сертифікатів TLS (не рекомендується)", @@ -194,11 +200,12 @@ "password_settings": "Налаштування входу з паролем", "password_settings_description": "Керування налаштуваннями входу за паролем", "paths_validated_successfully": "Усі шляхи успішно перевірено", + "person_cleanup_job": "Очищення особи", "quota_size_gib": "Розмір квоти (GiB)", "refreshing_all_libraries": "Оновлення всіх бібліотек", "registration": "Реєстрація адміністратора", "registration_description": "Оскільки ви перший користувач в системі, ви будете призначені Адміністратором і відповідатимете за адміністративні завдання, а додаткові користувачі будуть створені вами.", - "removing_offline_files": "Видалення недоступних файлів", + "removing_deleted_files": "Видалення недоступних файлів", "repair_all": "Відремонтуйте все", "repair_matched_items": "Відповідає {count, plural, one {# елемент} few {# елементи} many {# елементів} other {# елементів}}", "repaired_items": "Відновлено {count, plural, one {# елемент} few {# елементи} many {# елементів} other {# елементів}}", @@ -207,6 +214,7 @@ "reset_settings_to_recent_saved": "Скинути налаштування до недавно збережених налаштувань", "scanning_library_for_changed_files": "Сканування бібліотеки на наявність змінених файлів", "scanning_library_for_new_files": "Сканування бібліотеки на наявність нових файлів", + "search_jobs": "Пошук завдань...", "send_welcome_email": "Надіслати лист з вітанням", "server_external_domain_settings": "Зовнішній домен", "server_external_domain_settings_description": "Домен для публічних загальнодоступних посилань, включаючи http(s)://", @@ -234,6 +242,7 @@ "storage_template_settings_description": "Керуйте структурою тек та іменем завантаженого файлу", "storage_template_user_label": "{label} - це мітка зберігання користувача", "system_settings": "Системні налаштування", + "tag_cleanup_job": "Очистити тег", "theme_custom_css_settings": "Власний CSS", "theme_custom_css_settings_description": "Каскадні таблиці стилів дозволяють настроювати дизайн Immich.", "theme_settings": "Налаштування теми", @@ -308,6 +317,7 @@ "trash_settings_description": "Керування налаштуваннями кошика", "untracked_files": "Невідстежувані файли", "untracked_files_description": "Ці файли не відстежуються програмою. Вони можуть бути результатом невдалого переміщення, перерваного завантаження або залишитися через помилку програми", + "user_cleanup_job": "Очищення користувача", "user_delete_delay": "Акаунт {user} і його ресурси будуть заплановані для остаточного видалення через {delay, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", "user_delete_delay_settings": "Видалити затримку", "user_delete_delay_settings_description": "Кількість днів після видалення для остаточного видалення акаунта користувача та його ресурсів. Задача видалення користувача запускається опівночі для перевірки користувачів, готових до видалення. Зміни цього налаштування будуть оцінені під час наступного виконання.", @@ -387,6 +397,7 @@ "asset_offline": "Актив вимкнено", "asset_offline_description": "Цей ресурс відключений. Immich не може отримати доступ до його місцезнаходження файлів. Будь ласка, переконайтеся, що ресурс доступний, а потім знову проскануйте бібліотеку.", "asset_skipped": "Пропущено", + "asset_skipped_in_trash": "У смітнику", "asset_uploaded": "Завантажено", "asset_uploading": "Завантаження...", "assets": "елементи", @@ -655,6 +666,7 @@ "unable_to_get_comments_number": "Не вдалося отримати кількість коментарів", "unable_to_get_shared_link": "Не вдалося отримати спільне посилання", "unable_to_hide_person": "Неможливо приховати людину", + "unable_to_link_motion_video": "Не вдається зв'язати рухоме відео", "unable_to_link_oauth_account": "Не вдається прив'язати обліковий запис OAuth", "unable_to_load_album": "Неможливо завантажити альбом", "unable_to_load_asset_activity": "Неможливо завантажити активність активу", @@ -671,8 +683,8 @@ "unable_to_remove_api_key": "Не вдається видалити ключ API", "unable_to_remove_assets_from_shared_link": "Не вдається видалити ресурси зі спільного посилання", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Неможливо видалити автономні файли", "unable_to_remove_library": "Не вдається видалити бібліотеку", - "unable_to_remove_offline_files": "Неможливо видалити автономні файли", "unable_to_remove_partner": "Не вдається видалити партнера", "unable_to_remove_reaction": "Не вдалося видалити реакцію", "unable_to_remove_user": "", @@ -695,6 +707,7 @@ "unable_to_submit_job": "Не вдалося відправити завдання", "unable_to_trash_asset": "Неможливо вилучити актив", "unable_to_unlink_account": "Не вдається відв'язати обліковий запис", + "unable_to_unlink_motion_video": "Не вдається від'єднати рухоме відео", "unable_to_update_album_cover": "Неможливо оновити обкладинку альбому", "unable_to_update_album_info": "Неможливо оновити інформацію про альбом", "unable_to_update_library": "Не вдалося оновити бібліотеку", @@ -840,6 +853,7 @@ "license_trial_info_4": "Будь ласка, розгляньте можливість придбання ліцензії для підтримки подальшого розвитку сервісу", "light": "Світла", "like_deleted": "Лайк видалено", + "link_motion_video": "Посилання на рухоме відео", "link_options": "Налаштування посилання", "link_to_oauth": "Приєднання до OAuth", "linked_oauth_account": "Приєднаний акаунт OAuth", @@ -1072,10 +1086,10 @@ "remove_assets_shared_link_confirmation": "Ви впевнені, що хочете видалити {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}} з цього спільного посилання?", "remove_assets_title": "Видалити об'єкти?", "remove_custom_date_range": "Видалити користувацький діапазон дат", + "remove_deleted_assets": "Видалення автономних файлів", "remove_from_album": "Видалити з альбому", "remove_from_favorites": "Видалити з обраного", "remove_from_shared_link": "Видалити зі спільного посилання", - "remove_offline_files": "Видалення автономних файлів", "remove_user": "Видалити користувача", "removed_api_key": "Видалено ключ API: {name}", "removed_from_archive": "Видалено з архіву", @@ -1128,8 +1142,10 @@ "search_for_existing_person": "Пошук існуючої особи", "search_no_people": "Немає людей", "search_no_people_named": "Немає осіб з іменем \"{name}\"", + "search_options": "Опції пошуку", "search_people": "Шукати людей", "search_places": "Пошук місць", + "search_settings": "Налаштування пошуку", "search_state": "Пошук регіону...", "search_tags": "Пошук тегів...", "search_timezone": "Пошук часового поясу...", @@ -1206,6 +1222,8 @@ "sign_up": "Зареєструватися", "size": "Розмір", "skip_to_content": "Перейти до вмісту", + "skip_to_folders": "Перейти до папок", + "skip_to_tags": "Перейти до тегів", "slideshow": "Слайдшоу", "slideshow_settings": "Налаштування слайд-шоу", "sort_albums_by": "Сортувати альбоми за...", @@ -1280,6 +1298,7 @@ "unknown_album": "", "unknown_year": "Невідомий рік", "unlimited": "Без обмежень", + "unlink_motion_video": "Від'єднати рухоме відео", "unlink_oauth": "Від'єднайте OAuth", "unlinked_oauth_account": "Відключити акаунт OAuth", "unnamed_album": "Альбом без назви", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index 4192e7e5f06a7..c66abfdaf8946 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -41,6 +41,7 @@ "confirm_email_below": "Để xác nhận, nhập \"{email}\" bên dưới", "confirm_reprocess_all_faces": "Bạn có chắc chắn muốn xử lý lại tất cả các khuôn mặt? Thao tác này sẽ xoá tên người đã được gán.", "confirm_user_password_reset": "Bạn có chắc chắn muốn đặt lại mật khẩu của {user}?", + "create_job": "Tạo tác vụ", "crontab_guru": "Crontab Guru", "disable_login": "Vô hiệu hoá đăng nhập", "disabled": "", @@ -70,6 +71,7 @@ "image_thumbnail_resolution": "Độ phân giải ảnh thu nhỏ", "image_thumbnail_resolution_description": "Dùng khi xem một nhóm các ảnh (dòng thời gian chính, xem album, v.v.). Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", "job_concurrency": "{job} thực hiện đồng thời", + "job_created": "Tác vụ đã được tạo", "job_not_concurrency_safe": "Tác vụ này không an toàn để thực hiện đồng thời.", "job_settings": "Tác vụ", "job_settings_description": "Quản lý mức độ thực hiện đồng thời của tác vụ", @@ -152,7 +154,7 @@ "note_cannot_be_changed_later": "LƯU Ý: Cài đặt này không thể thay đổi được sau khi lưu!", "note_unlimited_quota": "Lưu ý: Nhập 0 để hạn mức không giới hạn", "notification_email_from_address": "Địa chỉ email người gửi", - "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server \"", + "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server \"", "notification_email_host_description": "Địa chỉ máy chủ email (ví dụ: smtp.immich.app)", "notification_email_ignore_certificate_errors": "Bỏ qua các lỗi chứng chỉ", "notification_email_ignore_certificate_errors_description": "Bỏ qua lỗi xác thực chứng chỉ TLS (không khuyến nghị)", @@ -198,11 +200,12 @@ "password_settings": "Mật khẩu đăng nhập", "password_settings_description": "Quản lý cài đặt mật khẩu đăng nhập", "paths_validated_successfully": "Tất cả các đường dẫn được xác minh thành công", + "person_cleanup_job": "Dọn dẹp người", "quota_size_gib": "Hạn mức (GiB)", "refreshing_all_libraries": "Làm mới tất cả các thư viện", "registration": "Đăng ký Quản trị viên", "registration_description": "Vì bạn là người dùng đầu tiên, bạn sẽ trở thành Quản trị viên và chịu trách nhiệm cho việc quản lý hệ thống. Ngoài ra, bạn có thể thêm các người dùng khác.", - "removing_offline_files": "Đang xoá các tập tin ngoại tuyến", + "removing_deleted_files": "Đang xoá các tập tin ngoại tuyến", "repair_all": "Sửa chữa tất cả", "repair_matched_items": "Đã tìm thấy {count, plural, one {# mục} other {# mục}} trùng khớp", "repaired_items": "Đã sửa chữa {count, plural, one{# mục} other {# mục}}", @@ -211,6 +214,7 @@ "reset_settings_to_recent_saved": "Đặt lại cài đặt về cài đặt trước đó", "scanning_library_for_changed_files": "Đang quét thư viện để tìm các tập tin đã thay đổi", "scanning_library_for_new_files": "Đang quét thư viện để tìm các tập tin mới", + "search_jobs": "Tìm kiếm tác vụ...", "send_welcome_email": "Gửi email chào mừng", "server_external_domain_settings": "Tên miền công khai", "server_external_domain_settings_description": "Tên miền dành cho các liên kết chia sẻ công khai, bao gồm http(s)://", @@ -238,6 +242,7 @@ "storage_template_settings_description": "Quản lý cấu trúc thư mục và tên tập tin của ảnh tải lên", "storage_template_user_label": "Cụm từ {label} là Nhãn lưu trữ của người dùng", "system_settings": "Cài đặt hệ thống", + "tag_cleanup_job": "Dọn dẹp thẻ", "theme_custom_css_settings": "CSS tùy chỉnh", "theme_custom_css_settings_description": "Cascading Style Sheets cho phép tùy chỉnh thiết kế của Immich.", "theme_settings": "Chủ đề", @@ -312,6 +317,7 @@ "trash_settings_description": "Quản lý cài đặt thùng rác", "untracked_files": "Các tập tin không được theo dõi", "untracked_files_description": "Những tập tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của các thao tác di chuyển thất bại, tải lên bị gián đoạn, hoặc bị bỏ lại do lỗi", + "user_cleanup_job": "Dọn dẹp người dùng", "user_delete_delay": "Tài khoản và các ảnh của {user} sẽ được lên lịch xóa vĩnh viễn sau {delay, plural, one {# ngày} other {# ngày}}.", "user_delete_delay_settings": "Thời gian xóa", "user_delete_delay_settings_description": "Số ngày chờ xóa để xóa vĩnh viễn tài khoản và các ảnh của người dùng. Tác vụ xóa người dùng chạy vào giữa đêm để kiểm tra các người dùng sẵn sàng bị xóa. Thay đổi cài đặt này sẽ được đánh giá vào lần thực hiện tiếp theo.", @@ -391,6 +397,7 @@ "asset_offline": "Ảnh ngoại tuyến", "asset_offline_description": "Tập tin này đang ngoại tuyến. Immich không thể truy cập vị trí tập tin của nó. Vui lòng đảm bảo tập tin có sẵn và sau đó quét lại thư viện.", "asset_skipped": "Đã bỏ qua", + "asset_skipped_in_trash": "Trong thùng rác", "asset_uploaded": "Đã tải lên", "asset_uploading": "Đang tải lên...", "assets": "Các tập tin", @@ -659,6 +666,7 @@ "unable_to_get_comments_number": "Không thể lấy số lượng bình luận", "unable_to_get_shared_link": "Không thể lấy liên kết chia sẻ", "unable_to_hide_person": "Không thể ẩn người", + "unable_to_link_motion_video": "Không thể liên kết video chuyển động", "unable_to_link_oauth_account": "Không thể liên kết tài khoản OAuth", "unable_to_load_album": "Không thể tải album", "unable_to_load_asset_activity": "Không thể tải hoạt động của ảnh", @@ -675,8 +683,8 @@ "unable_to_remove_api_key": "Không thể xóa khóa API", "unable_to_remove_assets_from_shared_link": "Không thể xóa các mục đã chọn khỏi liên kết chia sẻ", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "Không thể xóa tập tin ngoại tuyến", "unable_to_remove_library": "Không thể xóa thư viện", - "unable_to_remove_offline_files": "Không thể xóa tập tin ngoại tuyến", "unable_to_remove_partner": "Không thể xóa người thân", "unable_to_remove_reaction": "Không thể xóa phản ứng", "unable_to_remove_user": "", @@ -699,6 +707,7 @@ "unable_to_submit_job": "Không thể gửi tác vụ", "unable_to_trash_asset": "Không thể chuyển ảnh vào thùng rác", "unable_to_unlink_account": "Không thể hủy liên kết tài khoản", + "unable_to_unlink_motion_video": "Không thể hủy liên kết video chuyển động", "unable_to_update_album_cover": "Không thể cập nhật ảnh bìa album", "unable_to_update_album_info": "Không thể cập nhật thông tin album", "unable_to_update_library": "Không thể cập nhật thư viện", @@ -816,6 +825,7 @@ "library_options": "Tùy chọn thư viện", "light": "Sáng", "like_deleted": "Đã xoá thích", + "link_motion_video": "Liên kết video chuyển động", "link_options": "Tùy chọn liên kết", "link_to_oauth": "Liên kết đến OAuth", "linked_oauth_account": "Tài khoản OAuth đã liên kết", @@ -1048,10 +1058,10 @@ "remove_assets_shared_link_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi liên kết chia sẻ này?", "remove_assets_title": "Xóa mục?", "remove_custom_date_range": "Bỏ chọn khoảng ngày tùy chỉnh", + "remove_deleted_assets": "Loại bỏ tập tin ngoại tuyến", "remove_from_album": "Xóa khỏi album", "remove_from_favorites": "Xóa khỏi Mục yêu thích", "remove_from_shared_link": "Xóa khỏi liên kết chia sẻ", - "remove_offline_files": "Loại bỏ tập tin ngoại tuyến", "remove_user": "Xóa người dùng", "removed_api_key": "Khóa API đã xóa: {name}", "removed_from_archive": "Đã xoá khỏi Kho lưu trữ", @@ -1104,8 +1114,10 @@ "search_for_existing_person": "Tìm kiếm người hiện có", "search_no_people": "Không có người", "search_no_people_named": "Không có người tên \"{name}\"", + "search_options": "Tùy chọn tìm kiếm", "search_people": "Tìm kiếm người", "search_places": "Tìm kiếm địa điểm", + "search_settings": "Cài đặt tìm kiếm", "search_state": "Tìm kiếm tỉnh...", "search_tags": "Tìm kiếm thẻ...", "search_timezone": "Tìm kiếm múi giờ...", @@ -1258,6 +1270,7 @@ "unknown_album": "", "unknown_year": "Năm không xác định", "unlimited": "Không giới hạn", + "unlink_motion_video": "Hủy liên kết video chuyển động", "unlink_oauth": "Huỷ liên kết OAuth", "unlinked_oauth_account": "Đã huỷ liên kết tài khoản OAuth", "unnamed_album": "Album chưa đặt tên", diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index d2aa589da001e..961627a773d35 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -34,13 +34,14 @@ "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令。", "background_task_job": "背景任務", "check_all": "全選", - "cleared_jobs": "已清除 {job} 的任務", + "cleared_jobs": "已清除的作業:{job}", "config_set_by_file": "目前的設定已透過設定檔案設置", "confirm_delete_library": "確定要刪除「{library}」(圖庫)嗎?", "confirm_delete_library_assets": "您確定要刪除此圖庫嗎?這將從 Immich 中刪除{count, plural, one {個項目} other {個項目}},且無法復原。檔案仍會保留在硬碟中。", "confirm_email_below": "請在底下輸入 {email} 來確認", - "confirm_reprocess_all_faces": "您確定要重新處理所有面孔嗎?這將清除已命名的面孔。", + "confirm_reprocess_all_faces": "確定要重新處理所有臉孔嗎?這會清除已命名的人物。", "confirm_user_password_reset": "您確定要重設 {user} 的密碼嗎?", + "create_job": "建立作業", "crontab_guru": "", "disable_login": "停用登入", "disabled": "已禁用", @@ -48,7 +49,7 @@ "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。", "external_library_created_at": "外部圖庫(於 {date} 建立)", "external_library_management": "外部圖庫管理", - "face_detection": "面孔偵測", + "face_detection": "臉孔偵測", "face_detection_description": "使用機器學習檢測資料中的人臉。影片檔只會偵測縮圖。選擇「全部」將重新處理所有資料。選擇「缺失」將把尚未處理的資料加入處理佇列中。被檢測到的人臉將在所有人臉檢測完成後,排入人臉識別佇列中,並將它們分配到現有或新的人物中。", "facial_recognition_job_description": "將檢測到的人臉分組到人物中。此步驟將在人臉檢測完成後運行。選擇「全部」將重新分類所有人臉。選擇「缺失」將把沒有分配人物的人臉排入佇列。", "failed_job_command": "{job} 任務的 {command} 指令執行失敗", @@ -70,12 +71,13 @@ "image_thumbnail_resolution": "縮圖解析度", "image_thumbnail_resolution_description": "觀賞多張照片時(時間軸、相簿等)用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "job_concurrency": "{job}並行", + "job_created": "已建立作業", "job_not_concurrency_safe": "這個任務並行並不安全。", - "job_settings": "任務設定", - "job_settings_description": "管理任務並行", - "job_status": "任務狀態", - "jobs_delayed": "{jobCount, plural, other {# 項任務延遲}}", - "jobs_failed": "{jobCount, plural, other {# 項}}任務失敗", + "job_settings": "作業設定", + "job_settings_description": "管理作業並行", + "job_status": "作業狀態", + "jobs_delayed": "已延後 {jobCount, plural, other {# 項作業}}", + "jobs_failed": "{jobCount, plural, other {# 項}}作業失敗", "library_created": "已建立圖庫:{library}", "library_cron_expression": "Cron 運算式", "library_cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 Crontab Guru", @@ -95,7 +97,7 @@ "logging_level_description": "啟用時的記錄層級。", "logging_settings": "記錄檔", "machine_learning_clip_model": "CLIP 模型", - "machine_learning_clip_model_description": "CLIP 模型 名稱列表。更換模型後須對所有影像重新執行「智慧搜尋」。", + "machine_learning_clip_model_description": "這裏有份 CLIP 模型名單。註:更換模型後須對所有圖片重新執行「智慧搜尋」作業。", "machine_learning_duplicate_detection": "重複項目偵測", "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測", "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。", @@ -103,7 +105,7 @@ "machine_learning_enabled": "啟用機器學習", "machine_learning_enabled_description": "若停用,則無視下方的設定,所有機器學習的功能都將停用。", "machine_learning_facial_recognition": "臉部辨識", - "machine_learning_facial_recognition_description": "針測、分辨、規類影像中的人臉", + "machine_learning_facial_recognition_description": "偵測、認出並對圖片中的臉孔分組", "machine_learning_facial_recognition_model": "人臉辨識模型", "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。大的模型較慢且使用較多記憶體,但成效較嘉。更換模型後須對所有影像重新執行「人臉辨識」。", "machine_learning_facial_recognition_setting": "啟用人臉辨識", @@ -114,7 +116,7 @@ "machine_learning_max_recognition_distance_description": "若兩張人臉間的距離小於此將被判斷為相同人物,範圍為 0-2。數值降低能減少兩人被混在一起的可能性,數值提升能減少同一人被當作不同臉的可能性。由於合並比拆分容易,建議將數值調小。", "machine_learning_min_detection_score": "最低檢測分數", "machine_learning_min_detection_score_description": "最低信任分辨率,從0到1。低值會偵測更多的面孔,但可能導致誤報。", - "machine_learning_min_recognized_faces": "最少認出的臉", + "machine_learning_min_recognized_faces": "最少被認出的臉孔", "machine_learning_min_recognized_faces_description": "要創建一個人的最低認可面數。 增加此項數目使面部識別更為準確,但以增加可能不把面孔識別於任何人的機會為代價.", "machine_learning_settings": "機器學習設定", "machine_learning_settings_description": "管理機器學習的功能和設定", @@ -139,7 +141,11 @@ "map_settings_description": "管理地圖設定", "map_style_description": "地圖主題(style.json)的網址", "metadata_extraction_job": "擷取元資料", - "metadata_extraction_job_description": "擷取每個檔案的 GPS、解析度等元資料資訊", + "metadata_extraction_job_description": "擷取每個檔案的 GPS、臉孔、解析度等元資料資訊", + "metadata_faces_import_setting": "啟用臉孔匯入", + "metadata_faces_import_setting_description": "從圖片的 EXIF 資料和側接檔案匯入臉孔", + "metadata_settings": "元資料設定", + "metadata_settings_description": "管理元資料設定", "migration_job": "遷移", "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構", "no_paths_added": "未添加路徑", @@ -148,7 +154,7 @@ "note_cannot_be_changed_later": "註:之後就無法更改嘍!", "note_unlimited_quota": "註:輸入 0 表示不限制配額", "notification_email_from_address": "寄件地址", - "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", + "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", "notification_email_host_description": "電子郵件伺服器主機(例:smtp.immich.app)", "notification_email_ignore_certificate_errors": "忽略憑證錯誤", "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)", @@ -180,7 +186,7 @@ "oauth_scope": "範圍", "oauth_settings": "OAuth", "oauth_settings_description": "管理 OAuth 登入設定", - "oauth_settings_more_details": "欲瞭解此功能,請參閱文件。", + "oauth_settings_more_details": "欲瞭解此功能,請參閱說明書。", "oauth_signing_algorithm": "簽名算法", "oauth_storage_label_claim": "儲存標籤宣告", "oauth_storage_label_claim_description": "自動將使用者的儲存標籤定爲此宣告之值。", @@ -194,19 +200,21 @@ "password_settings": "密碼登入", "password_settings_description": "管理密碼登入設定", "paths_validated_successfully": "所有路徑驗證成功", + "person_cleanup_job": "清理人物", "quota_size_gib": "配額(GiB)", "refreshing_all_libraries": "正在重新整理所有圖庫", "registration": "管理者註冊", "registration_description": "由於您是本系統的首位使用者,因此將您指派爲負責管理本系統的管理者,其他使用者須由您協助建立帳號。", - "removing_offline_files": "移除離線檔案中", + "removing_deleted_files": "移除離線檔案中", "repair_all": "全部糾正", "repair_matched_items": "有 {count, plural, other {# 個項目相符}}", "repaired_items": "已糾正 {count, plural, other {# 個項目}}", "require_password_change_on_login": "要求使用者在首次登入時更改密碼", "reset_settings_to_default": "將設定重設回預設", "reset_settings_to_recent_saved": "已設回最後儲存的設定", - "scanning_library_for_changed_files": "正在掃描資料庫以檢查文件變更", - "scanning_library_for_new_files": "正在掃描資料庫以檢查新文件", + "scanning_library_for_changed_files": "掃描圖庫中變更的檔案", + "scanning_library_for_new_files": "掃描圖庫中的新檔案", + "search_jobs": "搜尋作業…", "send_welcome_email": "傳送歡迎電子郵件", "server_external_domain_settings": "外部網域", "server_external_domain_settings_description": "公開分享鏈結的網域(包含「http(s)://」)", @@ -234,6 +242,7 @@ "storage_template_settings_description": "管理上傳檔案的資料夾結構和檔名", "storage_template_user_label": "{label} 是使用者的儲存標籤", "system_settings": "系統設定", + "tag_cleanup_job": "清理標記", "theme_custom_css_settings": "自訂 CSS", "theme_custom_css_settings_description": "可以用層疊樣式表(CSS)來自訂 Immich 的設計。", "theme_settings": "主題", @@ -258,7 +267,7 @@ "transcoding_audio_codec": "音頻編解碼器", "transcoding_audio_codec_description": "Opus 是音質最高的選擇,但會與舊設備或軟件有較低的兼容性。", "transcoding_bitrate_description": "比特率高於最大比特率或格式不被接受的視頻", - "transcoding_codecs_learn_more": "欲了解此處使用的術語,請參閱 FFmpeg 文檔中的 H.264 編解碼器HEVC 編解碼器VP9 編解碼器。", + "transcoding_codecs_learn_more": "欲瞭解此處使用的術語,請參閱 FFmpeg 說明書中的 H.264 編解碼器HEVC 編解碼器VP9 編解碼器。", "transcoding_constant_quality_mode": "恆定質量模式", "transcoding_constant_quality_mode_description": "ICQ 比 CQP 更好,但某些硬件加速設備不支持此模式。設置此選項時,會在使用基於質量的編碼時偏好指定的模式。由於 NVENC 不支持 ICQ,此選項對其無效。", "transcoding_constant_rate_factor": "恆定速率因子(-crf)", @@ -308,8 +317,9 @@ "trash_settings_description": "管理垃圾桶設定", "untracked_files": "未被追蹤的檔案", "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", - "user_delete_delay": "{user} 的帳戶和資產將安排在 {delay, plural, one {# 天} other {# 天}} 後進行永久刪除。", - "user_delete_delay_settings": "刪除延遲", + "user_cleanup_job": "清理使用者", + "user_delete_delay": "{user} 的帳號和檔案將於 {delay, plural, other {# 天}}後永久刪除。", + "user_delete_delay_settings": "延後刪除", "user_delete_delay_settings_description": "移除後永久刪除用戶帳戶和資產的天數。用戶刪除任務會在午夜運行,以檢查是否有準備好刪除的用戶。對此設置的更改將在下一次執行時進行評估。", "user_delete_immediately": "{user} 的帳戶和資產將被立即排隊進行永久刪除。", "user_delete_immediately_checkbox": "將用戶和資產排隊進行立即刪除", @@ -387,6 +397,7 @@ "asset_offline": "檔案離線", "asset_offline_description": "此檔案己離線。Immich 無法訪問其文件位置。請確保資產可用,然後重新掃描資料庫。", "asset_skipped": "已略過", + "asset_skipped_in_trash": "已丟掉", "asset_uploaded": "已上傳", "asset_uploading": "上傳中…", "assets": "檔案", @@ -549,7 +560,7 @@ "edit_date": "編輯日期", "edit_date_and_time": "編輯日期與時間", "edit_exclusion_pattern": "編輯排除模式", - "edit_faces": "編輯人面", + "edit_faces": "編輯臉孔", "edit_import_path": "編輯匯入路徑", "edit_import_paths": "編輯匯入路徑", "edit_key": "編輯密鑰", @@ -584,11 +595,11 @@ "cant_change_activity": "無法{enabled, select, true {禁用} other {啟用}}活動", "cant_change_asset_favorite": "無法更改檔案的收藏狀態", "cant_change_metadata_assets_count": "無法更改 {count, plural, other {# 個檔案}}的元資料", - "cant_get_faces": "無法獲取面孔", + "cant_get_faces": "無法取得臉孔", "cant_get_number_of_comments": "無法獲取評論數量", "cant_search_people": "無法搜尋人", "cant_search_places": "無法搜尋地點", - "cleared_jobs": "已清除以下工作的任務: {job}", + "cleared_jobs": "已清除的作業:{job}", "error_adding_assets_to_album": "將檔案加入相簿時出錯", "error_adding_users_to_album": "將使用者加入相簿時出錯", "error_deleting_shared_user": "刪除共享使用者時出錯", @@ -655,6 +666,7 @@ "unable_to_get_comments_number": "無法獲取評論數量", "unable_to_get_shared_link": "取得分享鏈結失敗", "unable_to_hide_person": "無法隱藏人物", + "unable_to_link_motion_video": "無法鏈結動態影片", "unable_to_link_oauth_account": "無法連結 OAuth 帳戶", "unable_to_load_album": "無法載入相簿", "unable_to_load_asset_activity": "無法載入檔案活動", @@ -671,8 +683,8 @@ "unable_to_remove_api_key": "無法移除 API 金鑰", "unable_to_remove_assets_from_shared_link": "無法從分享鏈結中刪除檔案", "unable_to_remove_comment": "", + "unable_to_remove_deleted_assets": "無法移除離線檔案", "unable_to_remove_library": "無法移除資料庫", - "unable_to_remove_offline_files": "無法移除離線檔案", "unable_to_remove_partner": "無法移除夥伴", "unable_to_remove_reaction": "無法移除反應", "unable_to_remove_user": "", @@ -688,13 +700,14 @@ "unable_to_save_name": "無法儲存名稱", "unable_to_save_profile": "無法儲存個人資料", "unable_to_save_settings": "無法儲存設定", - "unable_to_scan_libraries": "無法掃描資料庫", - "unable_to_scan_library": "無法掃描資料庫", + "unable_to_scan_libraries": "無法掃描圖庫", + "unable_to_scan_library": "無法掃描圖庫", "unable_to_set_feature_photo": "無法設置特色照片", "unable_to_set_profile_picture": "無法設置個人頭像", "unable_to_submit_job": "無法提交作業", "unable_to_trash_asset": "無法將檔案丟進垃圾桶", "unable_to_unlink_account": "無法對帳號取消連接", + "unable_to_unlink_motion_video": "無法取消鏈結動態影片", "unable_to_update_album_cover": "無法更新相簿封面", "unable_to_update_album_info": "無法更新相簿資訊", "unable_to_update_library": "無法更新資料庫", @@ -715,7 +728,7 @@ "expired": "已過期", "expires_date": "失效期限:{date}", "explore": "探索", - "explorer": "探測器", + "explorer": "總攬", "export": "匯出", "export_as_json": "匯出 JSON", "extension": "副檔名", @@ -740,7 +753,7 @@ "fix_incorrect_match": "修復不相符的", "folders": "資料夾", "folders_feature_description": "以資料夾瀏覽檔案系統中的照片和影片", - "force_re-scan_library_files": "強制重新掃描所有資料庫檔案", + "force_re-scan_library_files": "強制重新掃描所有圖庫檔案", "forward": "順序", "general": "一般", "get_help": "線上求助", @@ -795,7 +808,7 @@ "invite_to_album": "邀請至相簿", "items_count": "{count, plural, other {# 個項目}}", "job_settings_description": "", - "jobs": "工作", + "jobs": "作業", "keep": "保留", "keep_all": "全部保留", "keyboard_shortcuts": "鍵盤快捷鍵", @@ -811,6 +824,7 @@ "library_options": "資料庫選項", "light": "淺色", "like_deleted": "已刪除的收藏", + "link_motion_video": "鏈結動態影片", "link_options": "鏈結選項", "link_to_oauth": "連接 OAuth", "linked_oauth_account": "已連接 OAuth 帳號", @@ -850,7 +864,7 @@ "menu": "選單", "merge": "合併", "merge_people": "合併人物", - "merge_people_limit": "您一次最多只能合併 5 張臉部", + "merge_people_limit": "一次最多只能合併 5 張臉孔", "merge_people_prompt": "您要合併這些人物嗎?此操作無法撤銷。", "merge_people_successfully": "成功合併人物", "merged_people_count": "合併了 {count, plural, one {# 位人士} other {# 位人士}}", @@ -1042,10 +1056,10 @@ "remove_assets_shared_link_confirmation": "確定要從此分享鏈結中移除{count, plural, other {# 個檔案}}嗎?", "remove_assets_title": "移除檔案?", "remove_custom_date_range": "移除自訂日期範圍", + "remove_deleted_assets": "移除離線檔案", "remove_from_album": "從相簿中移除", "remove_from_favorites": "從收藏中移除", "remove_from_shared_link": "從分享鏈結中移除", - "remove_offline_files": "移除離線檔案", "remove_user": "移除用戶", "removed_api_key": "已移除 API 金鑰:{name}", "removed_from_archive": "從封存中移除", @@ -1098,6 +1112,7 @@ "search_for_existing_person": "搜尋現有的人物", "search_no_people": "沒有人找到", "search_no_people_named": "沒有名爲「{name}」的人物", + "search_options": "搜尋選項", "search_people": "搜尋人物", "search_places": "搜尋地點", "search_state": "搜尋地區…", @@ -1176,6 +1191,8 @@ "sign_up": "註冊", "size": "用量", "skip_to_content": "跳至內容", + "skip_to_folders": "跳到資料夾", + "skip_to_tags": "跳到標記", "slideshow": "幻燈片", "slideshow_settings": "幻燈片設定", "sort_albums_by": "相簿排序方式", @@ -1227,6 +1244,7 @@ "to_change_password": "更改密碼", "to_favorite": "收藏", "to_login": "登入", + "to_parent": "到上一級", "to_root": "到根", "to_trash": "垃圾桶", "toggle_settings": "切換設定", @@ -1249,6 +1267,7 @@ "unknown_album": "", "unknown_year": "不知年份", "unlimited": "不限制", + "unlink_motion_video": "取消鏈結動態影片", "unlink_oauth": "取消連接 OAuth", "unlinked_oauth_account": "已解除連接 OAuth 帳號", "unnamed_album": "未命名相簿", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index afe01754c16b1..1c785681a44fc 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -20,27 +20,28 @@ "add_partner": "添加同伴", "add_path": "添加路径", "add_photos": "添加照片", - "add_to": "添加至...", - "add_to_album": "添加至相册", - "add_to_shared_album": "添加至共享相册", - "added_to_archive": "添加至归档", - "added_to_favorites": "添加至收藏", - "added_to_favorites_count": "添加{count, number}项至收藏", + "add_to": "添加到...", + "add_to_album": "添加到相册", + "add_to_shared_album": "添加到共享相册", + "added_to_archive": "添加到归档", + "added_to_favorites": "添加到收藏", + "added_to_favorites_count": "添加{count, number}项到收藏", "admin": { - "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 进行通配。要忽略名为 “Raw” 的任何目录中的所有文件,请使用 “**/Raw/**”。要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”。要忽略绝对路径,请使用 “/path/to/ignore/**”。", + "add_exclusion_pattern_description": "添加排除规则。支持使用 *、** 和 ? 通配符。比如要忽略任何名为 “Raw” 的文件夹中的所有文件,请使用 “**/Raw/**”;要忽略所有以 “.tif” 结尾的文件,请使用 “**/*.tif”;要忽略绝对路径,请使用 “/path/to/ignore/**”。", "authentication_settings": "认证设置", "authentication_settings_description": "管理密码、OAuth 和其它认证设置", - "authentication_settings_disable_all": "确定要禁用所有的登录方式?此操作将完全禁用登录。", + "authentication_settings_disable_all": "确定要禁用所有的登录方式?此操作将完全禁止登录。", "authentication_settings_reenable": "如需再次启用,使用 服务器指令。", "background_task_job": "后台任务", "check_all": "检查全部", - "cleared_jobs": "已清理作业:{job}", + "cleared_jobs": "已清理任务:{job}", "config_set_by_file": "当前配置已通过配置文件设置", - "confirm_delete_library": "是否确定要删除图库{library} ?", - "confirm_delete_library_assets": "是否确定要删除该图库?这将删除所有包含在Immich中的{count, plural, one {#个项目} other {#个项目}},且无法撤销。文件仍将保留在磁盘中。", + "confirm_delete_library": "确定要删除图库“{library}”吗?", + "confirm_delete_library_assets": "确定要删除该图库吗?这将删除所有包含在Immich中的{count, plural, one {#个项目} other {#个项目}},且无法撤销。但文件仍将保留在磁盘中。", "confirm_email_below": "输入“{email}”来确认", - "confirm_reprocess_all_faces": "是否确定对全部照片重新进行面部识别?这将同时清除所有已命名人物。", - "confirm_user_password_reset": "是否确定重置用户{user}的密码?", + "confirm_reprocess_all_faces": "确定要对全部照片重新进行面部识别吗?这将同时清除所有已命名人物。", + "confirm_user_password_reset": "确定要重置用户{user}的密码吗?", + "create_job": "创建任务", "crontab_guru": "Crontab Guru", "disable_login": "禁用登录", "disabled": "已禁用", @@ -51,7 +52,7 @@ "face_detection": "人脸检测", "face_detection_description": "使用机器学习检测项目中的人脸(视频只检测其缩略图中的人脸)。选择“全部”项将会(重新)处理所有项目。选择“缺失”项将尚未处理的项目置于队列中。人脸检测完成后,检测到的人脸将排队进行面部识别,将它们分组到现有的或新的人物中。", "facial_recognition_job_description": "将检测到的人脸按照人物分组。这一步将在人脸检测完成后执行。选择“全部”项将会(重新)分组所有面孔。选择“缺失”项将尚未分配的人脸置于队列中。", - "failed_job_command": "{command}命令执行失败的作业:{job}", + "failed_job_command": "{command}命令执行失败的任务:{job}", "force_delete_user_warning": "警告:这将立即移除用户以及所有项目。该操作无法撤回且文件无法恢复。", "forcing_refresh_library_files": "强制刷新所有图库文件", "image_format_description": "WebP 文件比 JPEG 文件小,但编码速度较慢。", @@ -70,21 +71,22 @@ "image_thumbnail_resolution": "缩略图分辨率", "image_thumbnail_resolution_description": "用于查看照片组(主时间轴、相册视图等)。更高的分辨率可以保留更多的细节,但编码时间更长,文件体积更大,并会降低应用程序的响应速度。", "job_concurrency": "{job}并发", + "job_created": "任务已创建", "job_not_concurrency_safe": "此任务并发并不安全。", "job_settings": "任务设置", "job_settings_description": "管理任务并发", "job_status": "任务状态", - "jobs_delayed": "{jobCount, plural, other {#项作业已推迟}}", + "jobs_delayed": "{jobCount, plural, other {#项任务已推迟}}", "jobs_failed": "{jobCount, plural, other {#项失败}}", "library_created": "已创建图库:{library}", "library_cron_expression": "Cron 表达式", "library_cron_expression_description": "使用 cron 格式设置扫描间隔。关于 cron 格式请参阅Crontab Guru", "library_cron_expression_presets": "Cron 表达式预设", "library_deleted": "图库已删除", - "library_import_path_description": "指定一个要导入的文件夹。该文件夹及其子文件夹将被扫描以查找图片和视频。", + "library_import_path_description": "指定一个要导入的文件夹。将扫描此文件夹(包括子文件夹)中的图像和视频。", "library_scanning": "定期扫描", - "library_scanning_description": "配置定期图库扫描", - "library_scanning_enable_description": "启用定期图库扫描", + "library_scanning_description": "配置定期扫描图库", + "library_scanning_enable_description": "启用定期扫描图库", "library_settings": "外部图库", "library_settings_description": "管理外部图库设置", "library_tasks_description": "执行图库任务", @@ -95,18 +97,18 @@ "logging_level_description": "启用的日志级别。", "logging_settings": "日志", "machine_learning_clip_model": "CLIP模型", - "machine_learning_clip_model_description": "支持的CLIP模型名称见 此处。注意,更换模型后需要对所有图片重新运行“智能检索”作业。", + "machine_learning_clip_model_description": "支持的CLIP模型名称见 此处。注意,更换模型后需要对所有图片重新运行“智能检索”任务。", "machine_learning_duplicate_detection": "重复项检测", "machine_learning_duplicate_detection_enabled": "启用重复检测", "machine_learning_duplicate_detection_enabled_description": "如果禁用此功能,完全相同的项目仍将被去重。", "machine_learning_duplicate_detection_setting_description": "使用CLIP向量匹配(关键词相似度)来查找可能的重复项", "machine_learning_enabled": "启用机器学习", - "machine_learning_enabled_description": "如果禁用,不论以下设置如何,所有机器学习功能将被禁用。", + "machine_learning_enabled_description": "如果禁用,无论以下如何设置,所有机器学习功能将被禁用。", "machine_learning_facial_recognition": "人脸识别", - "machine_learning_facial_recognition_description": "检测、识别并分组图像中的人脸", + "machine_learning_facial_recognition_description": "检测、识别并将图像中的人脸分组", "machine_learning_facial_recognition_model": "人脸识别模型", "machine_learning_facial_recognition_model_description": "机器学习模型按规模大小降序排列。更大的模型速度更慢,占用的内存更多,但效果更好。请注意,在更换模型后,必须对所有图像重新运行人脸检测。", - "machine_learning_facial_recognition_setting": "启用面部识别", + "machine_learning_facial_recognition_setting": "启用人脸识别", "machine_learning_facial_recognition_setting_description": "如果禁用此功能,图片将不会被编码并用于人脸识别,也不会在探索页面显示人物列表。", "machine_learning_max_detection_distance": "最大检测距离", "machine_learning_max_detection_distance_description": "两张图片被认为是重复的最大距离范围是0.001到0.1。较高的值将检测出更多的重复图片,但可能导致误报。", @@ -115,11 +117,11 @@ "machine_learning_min_detection_score": "最低检测分数", "machine_learning_min_detection_score_description": "面部被检测到的最小置信度分数范围是0到1。较低的值将检测到更多的面孔,但可能导致误报。", "machine_learning_min_recognized_faces": "识别的最少人脸数", - "machine_learning_min_recognized_faces_description": "创建新人物所需最少面部识别等数量。提高这个数字可以使面部识别更精确,但也增加了面孔未能被分配到对应人物的可能性。", + "machine_learning_min_recognized_faces_description": "创建新人物所需最少识别的数量。提高这个值可以使面部识别更精确,但也增加了面孔未能被分配到对应人物的可能性。", "machine_learning_settings": "机器学习设置", "machine_learning_settings_description": "管理机器学习功能和设置", "machine_learning_smart_search": "智能搜索", - "machine_learning_smart_search_description": "使用CLIP相似度进行图像语义搜索", + "machine_learning_smart_search_description": "使用CLIP以文搜图、智能搜图", "machine_learning_smart_search_enabled": "启用智能搜索", "machine_learning_smart_search_enabled_description": "如果禁用,则不会对图像编码以用于智能搜索。", "machine_learning_url_description": "机器学习服务器的URL", @@ -139,7 +141,7 @@ "map_settings_description": "管理地图设置", "map_style_description": "地图主题 style.json 的 URL", "metadata_extraction_job": "提取元数据", - "metadata_extraction_job_description": "从每个项目中提取元数据信息,如GPS和分辨率", + "metadata_extraction_job_description": "从每个项目中提取元数据信息,如GPS、人脸和分辨率", "metadata_faces_import_setting": "启用人脸导入", "metadata_faces_import_setting_description": "从图片的EXIF和辅助元数据中导入人脸", "metadata_settings": "元数据设置", @@ -149,11 +151,11 @@ "no_paths_added": "无已添加路径", "no_pattern_added": "无已添加规则", "note_apply_storage_label_previous_assets": "提示:要将存储标签应用于之前上传的项目,运行以下命令", - "note_cannot_be_changed_later": "注意:此项一旦设定,后续无法更改!", - "note_unlimited_quota": "提示:输入0表示无限制配额", + "note_cannot_be_changed_later": "注意:此项一旦设定,以后无法更改!", + "note_unlimited_quota": "提示:输入0表示无限制", "notification_email_from_address": "发件人地址", - "notification_email_from_address_description": "发件人邮箱地址,例如“Immich Photo Server ”", - "notification_email_host_description": "邮件服务器主机(例如 smtp.immich.app)", + "notification_email_from_address_description": "发件人邮箱地址,例如“Immich Photo Server ”", + "notification_email_host_description": "服务器地址:(例如:smtp.qq.com)", "notification_email_ignore_certificate_errors": "忽略证书错误", "notification_email_ignore_certificate_errors_description": "忽略TLS证书验证错误(不建议)", "notification_email_password_description": "与邮件服务器进行身份验证时使用的密码", @@ -161,8 +163,8 @@ "notification_email_sent_test_email_button": "发送测试邮件并保存", "notification_email_setting_description": "发送邮件通知设置", "notification_email_test_email": "发送测试邮件", - "notification_email_test_email_failed": "发送测试邮件失败,检查你的输入", - "notification_email_test_email_sent": "已向{email}发送了一封测试邮件。请检查您的收件箱。", + "notification_email_test_email_failed": "发送测试邮件失败,请检查你输入的信息", + "notification_email_test_email_sent": "已向{email}发送了一封测试邮件,请注意查收。", "notification_email_username_description": "与邮件服务器进行身份验证时使用的用户名", "notification_enable_email_notifications": "启用邮件通知", "notification_settings": "通知设置", @@ -171,11 +173,11 @@ "oauth_auto_launch_description": "在登录页面自动启动OAuth登录", "oauth_auto_register": "自动注册", "oauth_auto_register_description": "使用OAuth登录后自动注册新用户", - "oauth_button_text": "按钮文本", + "oauth_button_text": "按钮名称", "oauth_client_id": "客户端ID", - "oauth_client_secret": "客户端密匙", + "oauth_client_secret": "客户端密钥", "oauth_enable_description": "使用OAuth登录", - "oauth_issuer_url": "发行方的网址", + "oauth_issuer_url": "发行方网址", "oauth_mobile_redirect_uri": "移动端重定向 URI", "oauth_mobile_redirect_uri_override": "移动端重定向 URI 覆盖", "oauth_mobile_redirect_uri_override_description": "当 OAuth 提供商不允许使用移动 URI 时启用,如“'{callback}'”", @@ -184,33 +186,35 @@ "oauth_scope": "范围", "oauth_settings": "OAuth", "oauth_settings_description": "管理OAuth登录设置", - "oauth_settings_more_details": "关于本功能的更多详细信息,请见文档。", + "oauth_settings_more_details": "关于本功能的更多详细信息,请查看相关文档。", "oauth_signing_algorithm": "签名算法", "oauth_storage_label_claim": "存储标签声明", "oauth_storage_label_claim_description": "自动将用户的存储标签设置为此项的值。", "oauth_storage_quota_claim": "存储配额声明", "oauth_storage_quota_claim_description": "自动将用户的存储配额设置为此项的值。", "oauth_storage_quota_default": "默认存储配额(GB)", - "oauth_storage_quota_default_description": "没有提供声明时,要使用的GB配额(输入0表示无限制配额)。", + "oauth_storage_quota_default_description": "没有提供声明时,要使用的配额大小(GB)(输入0表示无限制)。", "offline_paths": "离线文件", "offline_paths_description": "这可能是由于手动删除了不属于外部图库的文件。", "password_enable_description": "使用邮箱和密码登录", "password_settings": "密码登录", "password_settings_description": "管理密码登录设置", "paths_validated_successfully": "所有路径验证成功", + "person_cleanup_job": "清理人物", "quota_size_gib": "配额大小(GB)", "refreshing_all_libraries": "刷新所有图库", "registration": "注册管理员", - "registration_description": "因为您是本系统的第一个用户,您被指派为管理员,负责相关管理工作,并且由您来创建新的用户。", - "removing_offline_files": "移除离线文件", + "registration_description": "由于您是系统上的第一个用户,您将被指定为管理员并负责管理任务,由您来创建新的用户。", + "removing_deleted_files": "移除离线文件", "repair_all": "修复所有", "repair_matched_items": "匹配到 {count, plural, one {#个项目} other {#个项目}}", "repaired_items": "已修复{count, plural, one {#个项目} other {#个项目}}", - "require_password_change_on_login": "要求用户第一次登录后修改密码", + "require_password_change_on_login": "要求用户首次登录时更改密码", "reset_settings_to_default": "恢复默认设置", "reset_settings_to_recent_saved": "恢复到最近保存的设置", - "scanning_library_for_changed_files": "扫描图库文件变更", - "scanning_library_for_new_files": "扫描图库文件增添", + "scanning_library_for_changed_files": "扫描图库变更的文件", + "scanning_library_for_new_files": "扫描图库新增的文件", + "search_jobs": "搜索任务...", "send_welcome_email": "发送欢迎邮件", "server_external_domain_settings": "外部域名", "server_external_domain_settings_description": "共享链接域名,包括 http(s)://", @@ -223,7 +227,7 @@ "slideshow_duration_description": "显示每张图像的秒数", "smart_search_job_description": "对项目进行机器学习处理以用于智能搜索", "storage_template_date_time_description": "使用项目的创建时间戳作为日期时间信息", - "storage_template_date_time_sample": "取样时间{date}", + "storage_template_date_time_sample": "采样时间{date}", "storage_template_enable_description": "启用存储模板", "storage_template_hash_verification_enabled": "哈希校验已启用", "storage_template_hash_verification_enabled_description": "启用哈希校验,如果您不知道此项的作用请不要禁用此功能", @@ -238,6 +242,7 @@ "storage_template_settings_description": "管理上传项目文件夹结构和文件名", "storage_template_user_label": "{label}是用户的存储标签", "system_settings": "系统设置", + "tag_cleanup_job": "清理标签", "theme_custom_css_settings": "自定义CSS", "theme_custom_css_settings_description": "可以通过CSS自定义Immich外观。", "theme_settings": "主题设置", @@ -249,7 +254,7 @@ "transcoding_acceleration_api": "加速器API", "transcoding_acceleration_api_description": "这个API将与您的设备交互,以加速转码过程。此设置为“尽力而为”——如果转码失败,将回到软件转码。VP9是否工作取决于您的硬件配置。", "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)", - "transcoding_acceleration_qsv": "快速同步(需要 7代及以上的 Intel CPU)", + "transcoding_acceleration_qsv": "快速同步(需要Intel 7代及以上的 CPU)", "transcoding_acceleration_rkmpp": "RKMPP(仅适用于 Rockchip SOCs)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "支持的音频编解码器", @@ -274,18 +279,18 @@ "transcoding_hardware_decoding_setting_description": "仅适用于NVENC、QSV和RKMPP。启用端到端加速,而不仅仅是加速编码。可能并不适用于所有视频。", "transcoding_hevc_codec": "HEVC 编解码器", "transcoding_max_b_frames": "最大B帧数", - "transcoding_max_b_frames_description": "较高的值可以提高压缩效率,但会减慢编码速度。可能与旧设备上的硬件加速不兼容。0值将禁用B帧,而-1值将自动设置此参数。", + "transcoding_max_b_frames_description": "较高的值可以提高压缩效率,但会减慢编码速度。可能与旧设备上的硬件加速不兼容。0表示将禁用B帧,-1表示将自动设置此参数。", "transcoding_max_bitrate": "最高码率", "transcoding_max_bitrate_description": "设置最大比特率可以使文件大小更可控,而对质量的影响很小。在720p时,VP9或HEVC的典型值为2600k,而H.264的典型值则为4500k。如果此项设置为0,则不限制最大比特率。", "transcoding_max_keyframe_interval": "最大关键帧间隔", - "transcoding_max_keyframe_interval_description": "设置关键帧之间的最大帧距离。较低的值会降低压缩效率,但可以提高搜索速度,并且可能在快速运动的场景中提高画质。值设为0将自动设置此参数。", + "transcoding_max_keyframe_interval_description": "设置关键帧之间的最大帧距离。较低的值会降低压缩效率,但可以提高搜索速度,并且可能在快速运动的场景中提高画质。0表示将自动设置此参数。", "transcoding_optimal_description": "视频超过目标分辨率或格式不支持", "transcoding_preferred_hardware_device": "首选硬件设备", "transcoding_preferred_hardware_device_description": "仅适用于VAAPI和QSV。设置用于硬件转码的dri节点。", "transcoding_preset_preset": "预设(-preset)", "transcoding_preset_preset_description": "压缩速度。较慢的预设会产生更小的文件,并在目标特定比特率时提高质量。VP9请忽略faster以上的速度。", "transcoding_reference_frames": "参考帧", - "transcoding_reference_frames_description": "在压缩给定帧时参考的帧数。较高的值可以提高压缩效率,但会减慢编码速度。此项设置为0时将自动设置此参数。", + "transcoding_reference_frames_description": "在压缩给定帧时参考的帧数。较高的值可以提高压缩效率,但会减慢编码速度。0表示将自动设置此参数。", "transcoding_required_description": "仅限不兼容格式的视频", "transcoding_settings": "视频转码设置", "transcoding_settings_description": "管理视频文件的分辨率和编码信息", @@ -294,11 +299,11 @@ "transcoding_temporal_aq": "Temporal AQ", "transcoding_temporal_aq_description": "仅适用于NVENC。提高高细节、低动态场景的质量。可能与旧设备不兼容。", "transcoding_threads": "线程数", - "transcoding_threads_description": "设定值越高,编码速度越快,留给其它任务(Docker外宿主机的任务等)的计算能力越少。此值不应大于CPU核心的数量。如果值设置为0,则最大限度地提高利用率。", + "transcoding_threads_description": "设定值越高,编码速度越快,留给其它任务(Docker外宿主机的任务等)的计算能力越少。此值不应大于CPU核心的数量。0表示最大限度地提高利用率。", "transcoding_tone_mapping": "色调映射", "transcoding_tone_mapping_description": "在将HDR视频转换为SDR时,尝试保持其外观。每种算法在颜色、细节和亮度方面做出了不同的权衡。Hable算法保留细节,Mobius算法保留颜色,而Reinhard算法保留亮度。", "transcoding_tone_mapping_npl": "NPL色调映射", - "transcoding_tone_mapping_npl_description": "对于这种亮度的显示器,颜色将被调整到显示正常。与直觉相反,较低的值会增加视频的亮度,反之亦然,因为它会补偿显示器的亮度。填0将自动设置此值。", + "transcoding_tone_mapping_npl_description": "对于这种亮度的显示器,颜色将被调整到显示正常。与直觉相反,较低的值会增加视频的亮度,反之亦然,因为它会补偿显示器的亮度。0表示将自动设置此值。", "transcoding_transcode_policy": "转码策略", "transcoding_transcode_policy_description": "视频转码策略。HDR视频将始终进行转码(除非禁用了转码功能)。", "transcoding_two_pass_encoding": "二次编码", @@ -312,15 +317,16 @@ "trash_settings_description": "管理回收站设置", "untracked_files": "未被追踪的文件", "untracked_files_description": "这些文件未被系统追踪。 这可能是移动失败、上传中断或因bug而落下", + "user_cleanup_job": "清理用户", "user_delete_delay": "{user}的账户及项目将在{delay, plural, one {#天} other {#天}}后自动永久删除。", "user_delete_delay_settings": "延期删除", - "user_delete_delay_settings_description": "移除用户后永久删除账户及其所有项目的天数。用户删除作业在午夜运行,检查是否有用户可以删除。对该设置的更改将在下次执行时开始计算。", + "user_delete_delay_settings_description": "删除用户后永久删除账户及其所有项目的天数。用户删除作业在午夜运行,检查是否有用户可以删除。对该设置的更改将在下次执行时开始计算。", "user_delete_immediately": "{user}的账户及项目将立即永久删除。", "user_delete_immediately_checkbox": "立即删除检索到的用户及项目", "user_management": "用户管理", "user_password_has_been_reset": "该用户的密码被重置:", "user_password_reset_description": "请向用户提供临时密码,并告知他们下次登录时需要更改密码。", - "user_restore_description": "{user}的账户将被恢复。", + "user_restore_description": "账户“{user}”将被恢复。", "user_restore_scheduled_removal": "恢复用户 - 计划于{date, date, long}删除", "user_settings": "用户设置", "user_settings_description": "管理用户设置", @@ -342,15 +348,15 @@ "album_added": "相册已添加", "album_added_notification_setting_description": "当您被添加到共享相册时,接收电子邮件通知", "album_cover_updated": "相册封面已更新", - "album_delete_confirmation": "是否确定要删除相册{album}?", + "album_delete_confirmation": "确定要删除相册“{album}”吗?", "album_delete_confirmation_description": "如果该相册是共享的,其他用户将无法再访问它。", "album_info_updated": "相册信息已更新", "album_leave": "退出相册?", - "album_leave_confirmation": "确定要退出相册{album}?", + "album_leave_confirmation": "确定要退出相册{album}吗?", "album_name": "相册名称", "album_options": "相册设置", "album_remove_user": "移除用户?", - "album_remove_user_confirmation": "你确定要移除{user}?", + "album_remove_user_confirmation": "你确定要移除{user}吗?", "album_share_no_users": "看起来您已与所有用户共享了此相册,或者您根本没有任何用户可共享。", "album_updated": "相册已更新", "album_updated_setting_description": "当共享相册有新项目时接收邮件通知", @@ -377,20 +383,21 @@ "archive": "归档", "archive_or_unarchive_photo": "归档或取消归档照片", "archive_size": "归档大小", - "archive_size_description": "配置下载归档大小(以GB为单位)", + "archive_size_description": "配置下载归档大小(GB)", "archived": "已归档", "archived_count": "{count, plural, other {已归档 # 项}}", - "are_these_the_same_person": "是否是同一个人?", + "are_these_the_same_person": "是同一个人吗?", "are_you_sure_to_do_this": "确定要这样做吗?", "asset_added_to_album": "已添加至相册", "asset_adding_to_album": "正在添加至相册...", "asset_description_updated": "项目描述已更新", "asset_filename_is_offline": "项目{filename}已离线", - "asset_has_unassigned_faces": "项目中有未认领的人脸", + "asset_has_unassigned_faces": "项目中有未分配的人脸", "asset_hashing": "哈希校验中...", "asset_offline": "项目离线", "asset_offline_description": "项目已离线。Immich无法访问该文件。请确保项目可读并重新扫描项目库。", "asset_skipped": "已跳过", + "asset_skipped_in_trash": "已回收", "asset_uploaded": "已上传", "asset_uploading": "上传中...", "assets": "项目", @@ -464,7 +471,7 @@ "confirm_delete_shared_link": "您确定要删除此共享链接吗?", "confirm_password": "确认密码", "contain": "包含", - "context": "图像语义搜索", + "context": "以文搜图", "continue": "继续", "copied_image_to_clipboard": "已复制图片至剪贴板。", "copied_to_clipboard": "已复制到剪切板!", @@ -487,7 +494,7 @@ "create_new_person": "创建新人物", "create_new_person_hint": "指派已选择项目到新的人物", "create_new_user": "创建新用户", - "create_tag": "新建标签", + "create_tag": "创建标签", "create_tag_description": "创建一个新标签。对于嵌套标签,请输入标签的完整路径,包括正斜杠(/)。", "create_user": "创建用户", "created": "已创建", @@ -495,25 +502,25 @@ "custom_locale": "自定义地区", "custom_locale_description": "日期和数字显示格式跟随语言和地区", "dark": "深色", - "date_after": "日期之后", + "date_after": "开始日期", "date_and_time": "日期与时间", - "date_before": "日期之前", + "date_before": "结束日期", "date_of_birth_saved": "出生日期保存成功", "date_range": "日期范围", - "day": "天", + "day": "日", "deduplicate_all": "删除所有重复项", "default_locale": "默认地区", "default_locale_description": "根据您的浏览器地区设置日期和数字显示格式", "delete": "删除", "delete_album": "删除相册", - "delete_api_key_prompt": "是否确定删除此API key?", - "delete_duplicates_confirmation": "你是否希望永久删除这些重复项?", - "delete_key": "删除秘钥", + "delete_api_key_prompt": "确定删除此API key吗?", + "delete_duplicates_confirmation": "你要永久删除这些重复项吗?", + "delete_key": "删除密钥", "delete_library": "删除图库", "delete_link": "删除链接", "delete_shared_link": "删除共享链接", "delete_tag": "删除标签", - "delete_tag_confirmation_prompt": "您确定要删除{tagName}标签吗?", + "delete_tag_confirmation_prompt": "您确定要删除“{tagName}”标签吗?", "delete_user": "删除用户", "deleted_shared_link": "共享链接已删除", "description": "描述", @@ -575,13 +582,13 @@ "empty": "空", "empty_album": "清空相册", "empty_trash": "清空回收站", - "empty_trash_confirmation": "确定要清空回收站?这将从Immich永久移除回收站中的所有项目。\n该操作无法撤消!", + "empty_trash_confirmation": "确定要清空回收站?这将永久删除回收站中的所有项目。\n注意:该操作无法撤消!", "enable": "启用", "enabled": "已启用", "end_date": "结束日期", "error": "错误", "error_loading_image": "加载图片时出错", - "error_title": "错误 - 我们遇到了一些问题", + "error_title": "错误 - 出了点问题", "errors": { "cannot_navigate_next_asset": "无法导航到下一个项目", "cannot_navigate_previous_asset": "无法导航到上一个项目", @@ -593,11 +600,11 @@ "cant_get_number_of_comments": "无法获取评论数量", "cant_search_people": "无法检索人物", "cant_search_places": "无法检索地点", - "cleared_jobs": "已删除作业:{job}", + "cleared_jobs": "已删除任务:{job}", "error_adding_assets_to_album": "添加项目到相册时出错", "error_adding_users_to_album": "添加用户到相册时出错", "error_deleting_shared_user": "删除共享用户时出错", - "error_downloading": "下载{filename}时出错", + "error_downloading": "下载“{filename}”时出错", "error_hiding_buy_button": "隐藏购买按钮时出错", "error_removing_assets_from_album": "从相册中移除项目时出错,请到控制台获取更详细信息", "error_selecting_all_assets": "选择所有项目时出错", @@ -614,7 +621,7 @@ "failed_to_stack_assets": "无法堆叠项目", "failed_to_unstack_assets": "无法取消堆叠项目", "import_path_already_exists": "此导入路径已存在。", - "incorrect_email_or_password": "错误的邮箱或密码", + "incorrect_email_or_password": "邮箱或密码错误", "paths_validation_failed": "{paths, plural, one {#条路径} other {#条路径}} 校验失败", "profile_picture_transparent_pixels": "个人资料图片不可以包含透明像素。请放大或移动此图片。", "quota_higher_than_disk_size": "设置的配额大于磁盘容量", @@ -625,8 +632,8 @@ "unable_to_add_exclusion_pattern": "无法添加排除规则", "unable_to_add_import_path": "无法添加导入路径", "unable_to_add_partners": "无法添加同伴", - "unable_to_add_remove_archive": "无法{archived, select, true {从归档中移除} other {添加项目至归档}}", - "unable_to_add_remove_favorites": "无法{favorite, select, true {添加项目至收藏} other {从收藏中移除}}", + "unable_to_add_remove_archive": "无法{archived, select, true {从归档中移除} other {添加项目到归档}}", + "unable_to_add_remove_favorites": "无法{favorite, select, true {添加项目到收藏} other {从收藏中移除}}", "unable_to_archive_unarchive": "无法{archived, select, true {归档} other {取消归档}}", "unable_to_change_album_user_role": "无法更改相册用户规则", "unable_to_change_date": "无法更改日期", @@ -660,6 +667,7 @@ "unable_to_get_comments_number": "无法获取评论数量", "unable_to_get_shared_link": "获取共享链接失败", "unable_to_hide_person": "无法隐藏人物", + "unable_to_link_motion_video": "无法链接到动态视频", "unable_to_link_oauth_account": "无法关联OAuth账户", "unable_to_load_album": "无法加载相册", "unable_to_load_asset_activity": "无法加载项目活动", @@ -676,10 +684,10 @@ "unable_to_remove_api_key": "无法移除API Key", "unable_to_remove_assets_from_shared_link": "无法从共享链接中移除项目", "unable_to_remove_comment": "无法移除评论", + "unable_to_remove_deleted_assets": "无法移除离线文件", "unable_to_remove_library": "无法移除图库", - "unable_to_remove_offline_files": "无法移除离线文件", "unable_to_remove_partner": "无法移除同伴", - "unable_to_remove_reaction": "无法移除反应", + "unable_to_remove_reaction": "无法移除回应", "unable_to_remove_user": "无法移除用户", "unable_to_repair_items": "无法修复项目", "unable_to_reset_password": "无法重置密码", @@ -697,9 +705,10 @@ "unable_to_scan_library": "无法扫描库", "unable_to_set_feature_photo": "无法设置人物头像", "unable_to_set_profile_picture": "无法设置个人资料图片", - "unable_to_submit_job": "无法提交作业", + "unable_to_submit_job": "无法提交任务", "unable_to_trash_asset": "无法放入回收站", "unable_to_unlink_account": "无法取消账户链接", + "unable_to_unlink_motion_video": "无法取消链接动态视频", "unable_to_update_album_cover": "无法更新相册封面", "unable_to_update_album_info": "无法更新相册信息", "unable_to_update_library": "无法更新库", @@ -758,15 +767,15 @@ "group_no": "未分组", "group_owner": "按所有者分组", "group_year": "按年分组", - "has_quota": "有限额", + "has_quota": "配额大小", "hi_user": "你好,{name}({email})", "hide_all_people": "隐藏所有人物", "hide_gallery": "隐藏相册", - "hide_named_person": "隐藏人物{name}", + "hide_named_person": "隐藏人物“{name}”", "hide_password": "隐藏密码", "hide_person": "隐藏人物", "hide_unnamed_people": "隐藏未命名的人物", - "host": "主机", + "host": "服务器", "hour": "时", "image": "图片", "image_alt_text_date": "在{date}拍摄的{isVideo, select, true {视频} other {照片}}", @@ -790,7 +799,7 @@ "in_albums": "在{count, plural, one {#个相册} other {#个相册}}中", "in_archive": "在归档中", "include_archived": "包括已归档", - "include_shared_albums": "包含共享相册", + "include_shared_albums": "包括共享相册", "include_shared_partner_assets": "包括同伴共享项目", "individual_share": "个人分享", "info": "信息", @@ -806,7 +815,7 @@ "job_settings_description": "管理任务并发", "jobs": "任务", "keep": "保留", - "keep_all": "保留", + "keep_all": "保留所有", "keyboard_shortcuts": "键盘快捷键", "language": "语言", "language_setting_description": "选择您的语言偏好", @@ -845,6 +854,7 @@ "license_trial_info_4": "请考虑购买授权来支持此服务的持续开发", "light": "浅色", "like_deleted": "已删除的收藏", + "link_motion_video": "链接动态视频", "link_options": "链接选项", "link_to_oauth": "链接到OAuth", "linked_oauth_account": "绑定OAuth账户", @@ -852,15 +862,15 @@ "loading": "加载中", "loading_search_results_failed": "加载搜索结果失败", "log_out": "注销", - "log_out_all_devices": "登出所有设备", - "logged_out_all_devices": "从所有设备登出", - "logged_out_device": "从设备登出", + "log_out_all_devices": "注销所有设备", + "logged_out_all_devices": "从所有设备注销", + "logged_out_device": "从设备注销", "login": "登录", "login_has_been_disabled": "登录已禁用。", - "logout_all_device_confirmation": "确定要从所有设备登出?", - "logout_this_device_confirmation": "确定要从本设备登出?", + "logout_all_device_confirmation": "确定要从所有设备注销?", + "logout_this_device_confirmation": "确定要从本设备注销?", "longitude": "经度", - "look": "查看", + "look": "样式", "loop_videos": "循环视频", "loop_videos_description": "启用在详细信息中自动循环播放视频。", "make": "品牌", @@ -885,7 +895,7 @@ "merge": "合并", "merge_people": "合并人物", "merge_people_limit": "每次最多只能合并 5 个人", - "merge_people_prompt": "你想合并这些人吗?此操作不可逆转。", + "merge_people_prompt": "你想合并这些人吗?此操作不可逆。", "merge_people_successfully": "合并人物成功", "merged_people_count": "已合并{count, plural, one {#个人} other {#个人}}", "minimize": "最小化", @@ -926,7 +936,7 @@ "no_shared_albums_message": "创建相册以共享照片和视频", "not_in_any_album": "不在任何相册中", "note_apply_storage_label_to_previously_uploaded assets": "提示:要将存储标签应用于之前上传的项目,运行以下命令", - "note_unlimited_quota": "注:输入 0 表示无限制配额", + "note_unlimited_quota": "注:输入 0 表示无限配额", "notes": "提示", "notification_toggle_setting_description": "启用邮件通知", "notifications": "通知", @@ -941,7 +951,7 @@ "onboarding_privacy_description": "以下(可选)功能依赖外部服务,可随时在管理设置中禁用。", "onboarding_theme_description": "选择服务的颜色主题。稍后可以在设置中进行修改。", "onboarding_welcome_description": "我们在启用服务前先做一些通用设置。", - "onboarding_welcome_user": "欢迎,{user}", + "onboarding_welcome_user": "欢迎你,{user}", "online": "在线", "only_favorites": "仅显示已收藏", "only_refreshes_modified_files": "仅刷新修改的文件", @@ -981,7 +991,7 @@ "people": "人物", "people_edits_count": "{count, plural, one {#个人物} other {#个人物}}已编辑", "people_feature_description": "按人物分组进行浏览照片和视频", - "people_sidebar_description": "在侧边栏中显示指向人物的链接", + "people_sidebar_description": "在侧边栏中显示“人物”链接", "perform_library_tasks": "", "permanent_deletion_warning": "永久删除警告", "permanent_deletion_warning_setting_description": "当永久删除项目时显示警告", @@ -1031,7 +1041,7 @@ "purchase_button_select": "选择", "purchase_failed_activation": "激活失败!请检查您的邮箱以获取正确的产品密钥!", "purchase_individual_description_1": "适用于个人", - "purchase_individual_description_2": "Supporter 状态", + "purchase_individual_description_2": "支持者状态", "purchase_individual_title": "个人", "purchase_input_suggestion": "已有一个产品密钥?请在下方输入密钥", "purchase_license_subtitle": "购买 Immich 以支持此项目的持续发展", @@ -1056,7 +1066,7 @@ "rating_count": "{count, plural, one {#星} other {#星}}", "rating_description": "在信息面板中展示EXIF星级", "raw": "Raw", - "reaction_options": "反应选项", + "reaction_options": "回应选项", "read_changelog": "阅读更新日志", "reassign": "重新指派", "reassigned_assets_to_existing_person": "重新指派{count, plural, one {#个项目} other {#个项目}}到{name, select, null {已存在的人物} other {{name}}}", @@ -1077,11 +1087,11 @@ "remove_assets_album_confirmation": "确定要从项目中移除{count, plural, one {#个项目} other {#个项目}}?", "remove_assets_shared_link_confirmation": "确定要从共享链接中移除{count, plural, one {#个项目} other {#个项目}}?", "remove_assets_title": "移除项目?", - "remove_custom_date_range": "根据自定义日期范围移除", + "remove_custom_date_range": "取消自定义日期范围", + "remove_deleted_assets": "删除离线文件", "remove_from_album": "从相册中移除", "remove_from_favorites": "移出收藏", "remove_from_shared_link": "从共享链接中移除", - "remove_offline_files": "删除离线文件", "remove_user": "移除用户", "removed_api_key": "移除的API Key:{name}", "removed_from_archive": "从归档中移除", @@ -1100,7 +1110,7 @@ "reset_people_visibility": "重置人物可见性", "reset_settings_to_default": "恢复到默认设置", "reset_to_default": "恢复默认值", - "resolve_duplicates": "处理查复项", + "resolve_duplicates": "处理重复项", "resolved_all_duplicates": "解决所有重复问题", "restore": "恢复", "restore_all": "恢复所有", @@ -1134,6 +1144,7 @@ "search_for_existing_person": "搜索已有人物", "search_no_people": "找不到人物", "search_no_people_named": "人物“{name}”不存在", + "search_options": "搜索选项", "search_people": "搜索人物", "search_places": "搜索地点", "search_state": "搜索省份...", @@ -1176,16 +1187,16 @@ "share": "共享", "shared": "共享", "shared_by": "共享自", - "shared_by_user": "由{user}共享", + "shared_by_user": "由“{user}”共享", "shared_by_you": "你的共享", - "shared_from_partner": "来自{partner}的照片", + "shared_from_partner": "来自“{partner}”的照片", "shared_link_options": "共享链接选项", "shared_links": "共享链接", "shared_photos_and_videos_count": "{assetCount, plural, other {#项已共享照片&视频。}}", - "shared_with_partner": "与{partner}共享", + "shared_with_partner": "与“{partner}”共享", "sharing": "共享", "sharing_enter_password": "请输入密码后查看此页面。", - "sharing_sidebar_description": "在侧边栏中显示共享链接", + "sharing_sidebar_description": "在侧边栏中显示“共享”链接", "shift_to_permanent_delete": "按住Shift键永久删除项目", "show_album_options": "显示相册选项", "show_albums": "显示相册", @@ -1211,9 +1222,9 @@ "sign_out": "注销", "sign_up": "注册", "size": "大小", - "skip_to_content": "跳到内容", - "skip_to_folders": "跳到文件夹", - "skip_to_tags": "跳到标签", + "skip_to_content": "跳转到内容", + "skip_to_folders": "跳转到文件夹", + "skip_to_tags": "跳转到标签", "slideshow": "幻灯片放映", "slideshow_settings": "放映设置", "sort_albums_by": "相册排序依据...", @@ -1236,15 +1247,15 @@ "status": "状态", "stop_motion_photo": "定格照片", "stop_photo_sharing": "停止共享照片?", - "stop_photo_sharing_description": "{partner}将不能访问你的照片。", + "stop_photo_sharing_description": "“{partner}”将不能访问你的照片。", "stop_sharing_photos_with_user": "停止与此用户共享照片", "storage": "存储空间", "storage_label": "存储标签", - "storage_usage": "总容量:{available},已使用{used}", + "storage_usage": "总量:{available}/已用:{used}", "submit": "提交", "suggestions": "建议", "sunrise_on_the_beach": "海滩上的日出", - "swap_merge_direction": "交换合并方向", + "swap_merge_direction": "互换合并方向", "sync": "同步", "tag": "标签", "tag_assets": "标记项目", @@ -1257,7 +1268,7 @@ "template": "模版", "theme": "主题", "theme_selection": "主题选项", - "theme_selection_description": "根据浏览器的系统首选项自动设置主题色", + "theme_selection_description": "跟随浏览器自动设置主题颜色", "they_will_be_merged_together": "项目将会合并到一起", "time_based_memories": "基于时间的回忆", "timezone": "时区", @@ -1288,6 +1299,7 @@ "unknown_album": "未知相册", "unknown_year": "未知年份", "unlimited": "无限制", + "unlink_motion_video": "取消链接动态视频", "unlink_oauth": "解绑OAuth", "unlinked_oauth_account": "解绑OAuth账户", "unnamed_album": "未命名相册", @@ -1313,15 +1325,15 @@ "upload_success": "上传成功,刷新页面查看新上传的项目。", "url": "URL", "usage": "用量", - "use_custom_date_range": "使用自定义日期范围", + "use_custom_date_range": "自定义日期范围", "user": "用户", "user_id": "用户ID", "user_license_settings": "授权", "user_license_settings_description": "管理你的授权", - "user_liked": "{user}点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", + "user_liked": "“{user}”点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", "user_purchase_settings": "购买", "user_purchase_settings_description": "管理购买订单", - "user_role_set": "设置{user}为{role}", + "user_role_set": "设置“{user}”为“{role}”", "user_usage_detail": "用户用量详情", "username": "用户名", "users": "用户", @@ -1330,7 +1342,7 @@ "variables": "变量", "version": "版本", "version_announcement_closing": "你的朋友,Alex", - "version_announcement_message": "嗨,伙计,当前应用出新版本了,请抽空阅读一下发行说明,并及时更新你的docker-compose.yml.env文件,避免存在错误配置,特别是当你是使用WatchTower或其它类似的自动升级工具时。", + "version_announcement_message": "嗨,朋友,当前应用出新版本了,请抽空阅读一下发行说明,并及时更新你的docker-compose.yml.env文件,避免存在配置错误,特别是当你是使用WatchTower或其它类似的自动升级工具时。", "video": "视频", "video_hover_setting": "鼠标悬停时播放视频缩略图", "video_hover_setting_description": "当鼠标悬停在项目上时播放视频缩略图。即使禁用了这个功能,也可以通过将鼠标悬停在播放图标上来开始播放。", @@ -1347,7 +1359,7 @@ "view_stack": "查看堆叠项目", "viewer": "预览", "visibility_changed": "{count, plural, one {#个人物} other {#个人物}}的可见性已修改", - "waiting": "队列中", + "waiting": "准备处理", "warning": "警告", "week": "周", "welcome": "欢迎", diff --git a/web/src/lib/stores/asset.store.spec.ts b/web/src/lib/stores/asset.store.spec.ts index 7787bf794d090..918a29e97df3b 100644 --- a/web/src/lib/stores/asset.store.spec.ts +++ b/web/src/lib/stores/asset.store.spec.ts @@ -141,7 +141,10 @@ describe('AssetStore', () => { }); it('adds assets to new bucket', () => { - const asset = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' }); + const asset = assetFactory.build({ + localDateTime: '2024-01-20T12:00:00.000Z', + fileCreatedAt: '2024-01-20T12:00:00.000Z', + }); assetStore.addAssets([asset]); expect(assetStore.buckets.length).toEqual(1); @@ -152,7 +155,10 @@ describe('AssetStore', () => { }); it('adds assets to existing bucket', () => { - const [assetOne, assetTwo] = assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' }); + const [assetOne, assetTwo] = assetFactory.buildList(2, { + localDateTime: '2024-01-20T12:00:00.000Z', + fileCreatedAt: '2024-01-20T12:00:00.000Z', + }); assetStore.addAssets([assetOne]); assetStore.addAssets([assetTwo]); @@ -163,9 +169,18 @@ describe('AssetStore', () => { }); it('orders assets in buckets by descending date', () => { - const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' }); - const assetTwo = assetFactory.build({ fileCreatedAt: '2024-01-15T12:00:00.000Z' }); - const assetThree = assetFactory.build({ fileCreatedAt: '2024-01-16T12:00:00.000Z' }); + const assetOne = assetFactory.build({ + fileCreatedAt: '2024-01-20T12:00:00.000Z', + localDateTime: '2024-01-20T12:00:00.000Z', + }); + const assetTwo = assetFactory.build({ + fileCreatedAt: '2024-01-15T12:00:00.000Z', + localDateTime: '2024-01-15T12:00:00.000Z', + }); + const assetThree = assetFactory.build({ + fileCreatedAt: '2024-01-16T12:00:00.000Z', + localDateTime: '2024-01-16T12:00:00.000Z', + }); assetStore.addAssets([assetOne, assetTwo, assetThree]); const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); @@ -177,9 +192,9 @@ describe('AssetStore', () => { }); it('orders buckets by descending date', () => { - const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' }); - const assetTwo = assetFactory.build({ fileCreatedAt: '2024-04-20T12:00:00.000Z' }); - const assetThree = assetFactory.build({ fileCreatedAt: '2023-01-20T12:00:00.000Z' }); + const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); + const assetTwo = assetFactory.build({ localDateTime: '2024-04-20T12:00:00.000Z' }); + const assetThree = assetFactory.build({ localDateTime: '2023-01-20T12:00:00.000Z' }); assetStore.addAssets([assetOne, assetTwo, assetThree]); expect(assetStore.buckets.length).toEqual(3); @@ -239,8 +254,8 @@ describe('AssetStore', () => { }); it('replaces bucket date when asset date changes', () => { - const asset = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' }); - const updatedAsset = { ...asset, fileCreatedAt: '2024-03-20T12:00:00.000Z' }; + const asset = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); + const updatedAsset = { ...asset, localDateTime: '2024-03-20T12:00:00.000Z' }; assetStore.addAssets([asset]); expect(assetStore.buckets.length).toEqual(1); @@ -264,7 +279,7 @@ describe('AssetStore', () => { }); it('ignores invalid IDs', () => { - assetStore.addAssets(assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' })); + assetStore.addAssets(assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' })); assetStore.removeAssets(['', 'invalid', '4c7d9acc']); expect(assetStore.assets.length).toEqual(2); @@ -273,7 +288,7 @@ describe('AssetStore', () => { }); it('removes asset from bucket', () => { - const [assetOne, assetTwo] = assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' }); + const [assetOne, assetTwo] = assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }); assetStore.addAssets([assetOne, assetTwo]); assetStore.removeAssets([assetOne.id]); @@ -283,7 +298,7 @@ describe('AssetStore', () => { }); it('removes bucket when empty', () => { - const assets = assetFactory.buildList(2, { fileCreatedAt: '2024-01-20T12:00:00.000Z' }); + const assets = assetFactory.buildList(2, { localDateTime: '2024-01-20T12:00:00.000Z' }); assetStore.addAssets(assets); assetStore.removeAssets(assets.map((asset) => asset.id)); @@ -376,8 +391,8 @@ describe('AssetStore', () => { }); it('returns the bucket index', () => { - const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' }); - const assetTwo = assetFactory.build({ fileCreatedAt: '2024-02-15T12:00:00.000Z' }); + const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); + const assetTwo = assetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' }); assetStore.addAssets([assetOne, assetTwo]); expect(assetStore.getBucketIndexByAssetId(assetTwo.id)).toEqual(0); @@ -385,8 +400,8 @@ describe('AssetStore', () => { }); it('ignores removed buckets', () => { - const assetOne = assetFactory.build({ fileCreatedAt: '2024-01-20T12:00:00.000Z' }); - const assetTwo = assetFactory.build({ fileCreatedAt: '2024-02-15T12:00:00.000Z' }); + const assetOne = assetFactory.build({ localDateTime: '2024-01-20T12:00:00.000Z' }); + const assetTwo = assetFactory.build({ localDateTime: '2024-02-15T12:00:00.000Z' }); assetStore.addAssets([assetOne, assetTwo]); assetStore.removeAssets([assetTwo.id]); diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index ed6f9fdf04799..5412464766f29 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -638,9 +638,7 @@ export class AssetStore { this.options.userId || this.options.personId || this.options.albumId || - isMismatched(this.options.isArchived, asset.isArchived) || - isMismatched(this.options.isFavorite, asset.isFavorite) || - isMismatched(this.options.isTrashed, asset.isTrashed) + this.isExcluded(asset) ) { // If asset is already in the bucket we don't need to recalculate // asset store containers @@ -661,7 +659,7 @@ export class AssetStore { const updatedBuckets = new Set(); for (const asset of assets) { - const timeBucket = DateTime.fromISO(asset.fileCreatedAt).toUTC().startOf('month').toString(); + const timeBucket = DateTime.fromISO(asset.localDateTime).toUTC().startOf('month').toString(); let bucket = this.getBucketByDate(timeBucket); if (!bucket) { @@ -699,26 +697,22 @@ export class AssetStore { async findAndLoadBucketAsPending(id: string) { const bucketInfo = this.assetToBucket[id]; - if (bucketInfo) { - const bucket = bucketInfo.bucket; + let bucket: AssetBucket | null = bucketInfo?.bucket ?? null; + if (!bucket) { + const asset = await getAssetInfo({ id }); + if (!asset || this.isExcluded(asset)) { + return; + } + + bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true }); + } + + if (bucket && bucket.assets.some((a) => a.id === id)) { this.pendingScrollBucket = bucket; this.pendingScrollAssetId = id; this.emit(false); return bucket; } - const asset = await getAssetInfo({ id }); - if (asset) { - if (this.options.isArchived !== asset.isArchived) { - return; - } - const bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true }); - if (bucket) { - this.pendingScrollBucket = bucket; - this.pendingScrollAssetId = asset.id; - this.emit(false); - } - return bucket; - } } /* Must be paired with matching clearPendingScroll() call */ @@ -791,7 +785,7 @@ export class AssetStore { if (assets.length === 0) { return; } - const assetsToReculculate: AssetResponseDto[] = []; + const assetsToRecalculate: AssetResponseDto[] = []; for (const _asset of assets) { const asset = this.assets.find((asset) => asset.id === _asset.id); @@ -799,17 +793,17 @@ export class AssetStore { continue; } - const recalculate = asset.fileCreatedAt !== _asset.fileCreatedAt; + const recalculate = asset.localDateTime !== _asset.localDateTime; Object.assign(asset, _asset); if (recalculate) { - assetsToReculculate.push(asset); + assetsToRecalculate.push(asset); } } - this.removeAssets(assetsToReculculate.map((asset) => asset.id)); - this.addAssetsToBuckets(assetsToReculculate); - this.emit(assetsToReculculate.length > 0); + this.removeAssets(assetsToRecalculate.map((asset) => asset.id)); + this.addAssetsToBuckets(assetsToRecalculate); + this.emit(assetsToRecalculate.length > 0); } removeAssets(ids: string[]) { @@ -905,6 +899,14 @@ export class AssetStore { } this.store$.set(this); } + + private isExcluded(asset: AssetResponseDto) { + return ( + isMismatched(this.options.isArchived ?? false, asset.isArchived) || + isMismatched(this.options.isFavorite, asset.isFavorite) || + isMismatched(this.options.isTrashed ?? false, asset.isTrashed) + ); + } } export const isSelectingAllAssets = writable(false); diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 14d1e4e66e895..358765fe0b111 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -32,6 +32,8 @@ export const serverConfig = writable({ isInitialized: false, isOnboarded: false, externalDomain: '', + mapDarkStyleUrl: '', + mapLightStyleUrl: '', }); export const retrieveServerConfig = async () => { diff --git a/web/src/lib/stores/slideshow.store.ts b/web/src/lib/stores/slideshow.store.ts index daa4e987916b4..3be96fda3eadb 100644 --- a/web/src/lib/stores/slideshow.store.ts +++ b/web/src/lib/stores/slideshow.store.ts @@ -38,6 +38,7 @@ function createSlideshowStore() { const showProgressBar = persisted('slideshow-show-progressbar', true); const slideshowDelay = persisted('slideshow-delay', 5, {}); + const slideshowTransition = persisted('slideshow-transition', true); return { restartProgress: { @@ -67,6 +68,7 @@ function createSlideshowStore() { slideshowState, slideshowDelay, showProgressBar, + slideshowTransition, }; } diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index 395d9796f4fb3..dccb03c9bf55e 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -19,6 +19,7 @@ import { type AssetResponseDto, type PersonResponseDto, type SharedLinkResponseDto, + type UserResponseDto, } from '@immich/sdk'; import { mdiCogRefreshOutline, mdiDatabaseRefreshOutline, mdiImageRefreshOutline } from '@mdi/js'; import { sortBy } from 'lodash-es'; @@ -204,7 +205,8 @@ export const getAssetPlaybackUrl = (options: string | { id: string; checksum?: s return createUrl(getAssetPlaybackPath(id), { key: getKey(), c: checksum }); }; -export const getProfileImageUrl = (userId: string) => createUrl(getUserProfileImagePath(userId)); +export const getProfileImageUrl = (user: UserResponseDto) => + createUrl(getUserProfileImagePath(user.id), { updatedAt: user.profileChangedAt }); export const getPeopleThumbnailUrl = (person: PersonResponseDto, updatedAt?: string) => createUrl(getPeopleThumbnailPath(person.id), { updatedAt: updatedAt ?? person.updatedAt }); @@ -255,11 +257,7 @@ export const copyToClipboard = async (secret: string) => { }; export const makeSharedLinkUrl = (externalDomain: string, key: string) => { - let url = externalDomain || window.location.origin; - if (!url.endsWith('/')) { - url += '/'; - } - return `${url}share/${key}`; + return new URL(`share/${key}`, externalDomain || window.location.origin).href; }; export const oauth = { diff --git a/web/src/lib/utils/actions.ts b/web/src/lib/utils/actions.ts index f1772c200e15a..291d3926ee151 100644 --- a/web/src/lib/utils/actions.ts +++ b/web/src/lib/utils/actions.ts @@ -6,7 +6,8 @@ import { handleError } from './handle-error'; export type OnDelete = (assetIds: string[]) => void; export type OnRestore = (ids: string[]) => void; -export type OnLink = (asset: AssetResponseDto) => void; +export type OnLink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void; +export type OnUnlink = (assets: { still: AssetResponseDto; motion: AssetResponseDto }) => void; export type OnArchive = (ids: string[], isArchived: boolean) => void; export type OnFavorite = (ids: string[], favorite: boolean) => void; export type OnStack = (ids: string[]) => void; diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 84a896452f7f1..0522f1709a8f3 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -211,13 +211,6 @@ export const downloadArchive = async (fileName: string, options: Omit { const $t = get(t); - if (asset.isOffline) { - notificationController.show({ - type: NotificationType.Info, - message: $t('asset_filename_is_offline', { values: { filename: asset.originalFileName } }), - }); - return; - } const assets = [ { filename: asset.originalFileName, diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts index 9ca5bc8773e34..a7e9a4340c081 100644 --- a/web/src/lib/utils/handle-error.ts +++ b/web/src/lib/utils/handle-error.ts @@ -2,9 +2,21 @@ import { isHttpError } from '@immich/sdk'; import { notificationController, NotificationType } from '../components/shared-components/notification/notification'; export function getServerErrorMessage(error: unknown) { - if (isHttpError(error)) { - return error.data?.message || error.message; + if (!isHttpError(error)) { + return; } + + // errors for endpoints without return types aren't parsed as json + let data = error.data; + if (typeof data === 'string') { + try { + data = JSON.parse(data); + } catch { + // Not a JSON string + } + } + + return data?.message || error.message; } export function handleError(error: unknown, message: string) { diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 541ebea7f5444..7e65dcdb997bf 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -36,6 +36,9 @@ export type ScrollTargetListener = ({ export const fromLocalDateTime = (localDateTime: string) => DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) }); +export const fromDateTimeOriginal = (dateTimeOriginal: string, timeZone: string) => + DateTime.fromISO(dateTimeOriginal, { zone: timeZone }); + export const groupDateFormat: Intl.DateTimeFormatOptions = { weekday: 'short', month: 'short', diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6e75273f3bc2c..3df4a25b83e1f 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -169,12 +169,13 @@ const handleToggleEnableActivity = async () => { try { - album = await updateAlbumInfo({ + await updateAlbumInfo({ id: album.id, updateAlbumDto: { isActivityEnabled: !album.isActivityEnabled, }, }); + await refreshAlbum(); notificationController.show({ type: NotificationType.Info, message: $t('activity_changed', { values: { enabled: album.isActivityEnabled } }), @@ -277,7 +278,7 @@ }; const refreshAlbum = async () => { - album = await getAlbumInfo({ id: album.id, withoutAssets: true }); + data.album = await getAlbumInfo({ id: album.id, withoutAssets: true }); }; const handleAddAssets = async () => { @@ -330,12 +331,13 @@ const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { try { - album = await addUsersToAlbum({ + await addUsersToAlbum({ id: album.id, addUsersDto: { albumUsers, }, }); + await refreshAlbum(); viewMode = ViewMode.VIEW; } catch (error) { @@ -470,7 +472,7 @@ {:else} {#if viewMode === ViewMode.VIEW} - goto(backUrl)}> + goto(backUrl)}> {#if isEditor} +

{#if $timelineSelected.size === 0} @@ -554,7 +556,7 @@ {/if} {#if viewMode === ViewMode.SELECT_THUMBNAIL} - (viewMode = ViewMode.VIEW)}> + (viewMode = ViewMode.VIEW)}> {$t('select_album_cover')} {/if} @@ -583,13 +585,18 @@ isSelectionMode={viewMode === ViewMode.SELECT_THUMBNAIL} singleSelect={viewMode === ViewMode.SELECT_THUMBNAIL} showArchiveIcon - on:select={({ detail: asset }) => handleUpdateThumbnail(asset.id)} - on:escape={handleEscape} + onSelect={({ id }) => handleUpdateThumbnail(id)} + onEscape={handleEscape} > {#if viewMode !== ViewMode.SELECT_THUMBNAIL}

- + (album.albumName = albumName)} + /> {#if album.assetCount > 0} @@ -674,8 +681,8 @@ disabled={!album.isActivityEnabled} {isLiked} numberOfComments={$numberOfComments} - on:favorite={handleFavorite} - on:openActivityTab={handleOpenAndCloseActivityTab} + onFavorite={handleFavorite} + onOpenActivityTab={handleOpenAndCloseActivityTab} />
{/if} @@ -697,10 +704,10 @@ albumId={album.id} {isLiked} bind:reactions - on:addComment={() => updateNumberOfComments(1)} - on:deleteComment={() => updateNumberOfComments(-1)} - on:deleteLike={() => (isLiked = null)} - on:close={handleOpenAndCloseActivityTab} + onAddComment={() => updateNumberOfComments(1)} + onDeleteComment={() => updateNumberOfComments(-1)} + onDeleteLike={() => (isLiked = null)} + onClose={handleOpenAndCloseActivityTab} />
@@ -709,8 +716,8 @@ {#if viewMode === ViewMode.SELECT_USERS} handleAddUsers(users)} - on:share={() => (viewMode = ViewMode.LINK_SHARING)} + onSelect={handleAddUsers} + onShare={() => (viewMode = ViewMode.LINK_SHARING)} onClose={() => (viewMode = ViewMode.VIEW)} /> {/if} @@ -723,8 +730,8 @@ (viewMode = ViewMode.VIEW)} {album} - on:remove={({ detail: userId }) => handleRemoveUser(userId)} - on:refreshAlbum={refreshAlbum} + onRemove={handleRemoveUser} + onRefreshAlbum={refreshAlbum} /> {/if} @@ -737,9 +744,9 @@ albumOrder = order; await setModeToView(); }} - on:close={() => (viewMode = ViewMode.VIEW)} - on:toggleEnableActivity={handleToggleEnableActivity} - on:showSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} + onClose={() => (viewMode = ViewMode.VIEW)} + onToggleEnabledActivity={handleToggleEnableActivity} + onShowSelectSharedUser={() => (viewMode = ViewMode.SELECT_USERS)} /> {/if} diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0ea0ed18bb733..adbc3cfe699a3 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -113,7 +113,7 @@ {#if $featureFlags.loaded && $featureFlags.map}
- onViewAssets(event.detail)} /> +
@@ -122,9 +122,9 @@ 1} - on:next={navigateNext} - on:previous={navigatePrevious} - on:close={() => { + onNext={navigateNext} + onPrevious={navigatePrevious} + onClose={() => { assetViewingStore.showAssetViewer(false); handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); }} @@ -137,11 +137,11 @@ {#if showSettingsModal} (showSettingsModal = false)} - on:save={async ({ detail }) => { - const shouldUpdate = !isEqual(omit(detail, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); + onClose={() => (showSettingsModal = false)} + onSave={async (settings) => { + const shouldUpdate = !isEqual(omit(settings, 'allowDarkMode'), omit($mapSettings, 'allowDarkMode')); showSettingsModal = false; - $mapSettings = detail; + $mapSettings = settings; if (shouldUpdate) { mapMarkers = await loadMapMarkers(); diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index b580c4faa5454..2caab9de82508 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -38,7 +38,7 @@ {:else} - goto(AppRoute.SHARING)}> + goto(AppRoute.SHARING)}>

{data.partner.name}'s photos diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index f1a2674e24905..b6d25c48bf937 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -302,9 +302,9 @@ {personMerge1} {personMerge2} {potentialMergePeople} - on:close={() => (showMergeModal = false)} - on:reject={() => changeName()} - on:confirm={(event) => handleMergeSamePerson(event.detail)} + onClose={() => (showMergeModal = false)} + onReject={changeName} + onConfirm={handleMergeSamePerson} /> {/if} @@ -349,10 +349,10 @@ handleChangeName(person)} - on:set-birth-date={() => handleSetBirthDate(person)} - on:merge-people={() => handleMergePeople(person)} - on:hide-person={() => handleHidePerson(person)} + onChangeName={() => handleChangeName(person)} + onSetBirthDate={() => handleSetBirthDate(person)} + onMergePeople={() => handleMergePeople(person)} + onHidePerson={() => handleHidePerson(person)} /> {:else} @@ -397,8 +397,8 @@ {#if showSetBirthDateModal} (showSetBirthDateModal = false)} - on:updated={(event) => submitBirthDateChange(event.detail)} + onClose={() => (showSetBirthDateModal = false)} + onUpdate={submitBirthDateChange} /> {/if} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index daa5821e8506b..037feaf35f6f1 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -347,8 +347,8 @@ a.id)} personAssets={person} - on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} - on:confirm={handleUnmerge} + onClose={() => (viewMode = ViewMode.VIEW_ASSETS)} + onConfirm={handleUnmerge} /> {/if} @@ -357,22 +357,22 @@ {personMerge1} {personMerge2} {potentialMergePeople} - on:close={() => (viewMode = ViewMode.VIEW_ASSETS)} - on:reject={() => changeName()} - on:confirm={(event) => handleMergeSamePerson(event.detail)} + onClose={() => (viewMode = ViewMode.VIEW_ASSETS)} + onReject={changeName} + onConfirm={handleMergeSamePerson} /> {/if} {#if viewMode === ViewMode.BIRTH_DATE} (viewMode = ViewMode.VIEW_ASSETS)} - on:updated={(event) => handleSetBirthDate(event.detail)} + onClose={() => (viewMode = ViewMode.VIEW_ASSETS)} + onUpdate={handleSetBirthDate} /> {/if} {#if viewMode === ViewMode.MERGE_PEOPLE} - handleMerge(detail)} /> + {/if}

@@ -400,7 +400,7 @@ {:else} {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} - goto(previousRoute)}> + goto(previousRoute)}> (viewMode = ViewMode.VIEW_ASSETS)}> + (viewMode = ViewMode.VIEW_ASSETS)}> {$t('select_featured_photo')} {/if} @@ -444,8 +444,8 @@ {assetInteractionStore} isSelectionMode={viewMode === ViewMode.SELECT_PERSON} singleSelect={viewMode === ViewMode.SELECT_PERSON} - on:select={({ detail: asset }) => handleSelectFeaturePhoto(asset)} - on:escape={handleEscape} + onSelect={handleSelectFeaturePhoto} + onEscape={handleEscape} > {#if viewMode === ViewMode.VIEW_ASSETS || viewMode === ViewMode.SUGGEST_MERGE || viewMode === ViewMode.BIRTH_DATE} @@ -464,7 +464,7 @@ bind:suggestedPeople name={person.name} bind:isSearchingPeople - on:change={(event) => handleNameChange(event.detail)} + onChange={handleNameChange} {thumbnailData} /> {:else} @@ -486,17 +486,10 @@
- {#if person.name} -

{person.name}

-

- {$t('assets_count', { values: { count: numberOfAssets } })} -

- {:else} -

{$t('add_a_name')}

-

- {$t('find_them_fast')} -

- {/if} +

{person.name || $t('add_a_name')}

+

+ {$t('assets_count', { values: { count: numberOfAssets } })} +

diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index a1131ecfbb16c..ba8ee13cc9f1f 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -23,8 +23,9 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { preferences, user } from '$lib/stores/user.store'; + import type { OnLink, OnUnlink } from '$lib/utils/actions'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; - import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; + import { AssetTypeEnum } from '@immich/sdk'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; @@ -35,12 +36,21 @@ const { isMultiSelectState, selectedAssets } = assetInteractionStore; let isAllFavorite: boolean; + let isAllOwned: boolean; let isAssetStackSelected: boolean; + let isLinkActionAvailable: boolean; $: { const selection = [...$selectedAssets]; + isAllOwned = selection.every((asset) => asset.ownerId === $user.id); isAllFavorite = selection.every((asset) => asset.isFavorite); isAssetStackSelected = selection.length === 1 && !!selection[0].stack; + const isLivePhoto = selection.length === 1 && !!selection[0].livePhotoVideoId; + const isLivePhotoCandidate = + selection.length === 2 && + selection.some((asset) => asset.type === AssetTypeEnum.Image) && + selection.some((asset) => asset.type === AssetTypeEnum.Image); + isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate); } const handleEscape = () => { @@ -53,11 +63,14 @@ } }; - const handleLink = (asset: AssetResponseDto) => { - if (asset.livePhotoVideoId) { - assetStore.removeAssets([asset.livePhotoVideoId]); - } - assetStore.updateAssets([asset]); + const handleLink: OnLink = ({ still, motion }) => { + assetStore.removeAssets([motion.id]); + assetStore.updateAssets([still]); + }; + + const handleUnlink: OnUnlink = ({ still, motion }) => { + assetStore.addAssets([motion]); + assetStore.updateAssets([still]); }; onDestroy(() => { @@ -87,8 +100,13 @@ onUnstack={(assets) => assetStore.addAssets(assets)} /> {/if} - {#if $selectedAssets.size === 2 && [...$selectedAssets].some((asset) => asset.type === AssetTypeEnum.Image && [...$selectedAssets].some((asset) => asset.type === AssetTypeEnum.Video))} - + {#if isLinkActionAvailable} + {/if} @@ -109,7 +127,7 @@ {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE} - on:escape={handleEscape} + onEscape={handleEscape} withStacked > {#if $preferences.memories.enabled} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index da85eb49c8267..9c6a8f9e75891 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -246,7 +246,7 @@
{:else}
- goto(previousRoute)} backIcon={mdiArrowLeft}> + goto(previousRoute)} backIcon={mdiArrowLeft}>
diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index 5e934143dff2a..67e80f4703858 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -52,7 +52,7 @@ }; - goto(AppRoute.SHARING)}> + goto(AppRoute.SHARING)}> {$t('shared_links')} diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 27ad5bb3f072a..862d9382a462f 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -33,7 +33,8 @@ handlePromiseError(goto(AppRoute.PHOTOS)); } - const assetStore = new AssetStore({ isTrashed: true }); + const options = { isTrashed: true }; + const assetStore = new AssetStore(options); const assetInteractionStore = createAssetInteractionStore(); const { isMultiSelectState, selectedAssets } = assetInteractionStore; @@ -47,16 +48,15 @@ } try { - await emptyTrash(); - - const deletedAssetIds = assetStore.assets.map((a) => a.id); - const numberOfAssets = deletedAssetIds.length; - assetStore.removeAssets(deletedAssetIds); + const { count } = await emptyTrash(); notificationController.show({ - message: $t('assets_permanently_deleted_count', { values: { count: numberOfAssets } }), + message: $t('assets_permanently_deleted_count', { values: { count } }), type: NotificationType.Info, }); + + // reset asset grid (TODO fix in asset store that it should reset when it is empty) + await assetStore.updateOptions(options); } catch (error) { handleError(error, $t('errors.unable_to_empty_trash')); } @@ -71,16 +71,14 @@ return; } try { - await restoreTrash(); - - const restoredAssetIds = assetStore.assets.map((a) => a.id); - const numberOfAssets = restoredAssetIds.length; - assetStore.removeAssets(restoredAssetIds); - + const { count } = await restoreTrash(); notificationController.show({ - message: $t('assets_restored_count', { values: { count: numberOfAssets } }), + message: $t('assets_restored_count', { values: { count } }), type: NotificationType.Info, }); + + // reset asset grid (TODO fix in asset store that it should reset when it is empty) + await assetStore.updateOptions(options); } catch (error) { handleError(error, $t('errors.unable_to_restore_trash')); } diff --git a/web/src/routes/(user)/user-settings/+page.svelte b/web/src/routes/(user)/user-settings/+page.svelte index ea302454b2fbc..4ed46b580fd31 100644 --- a/web/src/routes/(user)/user-settings/+page.svelte +++ b/web/src/routes/(user)/user-settings/+page.svelte @@ -27,5 +27,5 @@ {#if isShowKeyboardShortcut} - (isShowKeyboardShortcut = false)} /> + (isShowKeyboardShortcut = false)} /> {/if} diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index 34889261d542e..e1029b7ccbfff 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -42,7 +42,7 @@ { key: ['s'], action: $t('view') }, { key: ['d'], action: $t('unselect_all_duplicates') }, { key: ['⇧', 'c'], action: $t('resolve_duplicates') }, - { key: ['⇧', 'c'], action: $t('stack_duplicates') }, + { key: ['⇧', 's'], action: $t('stack_duplicates') }, ], }; @@ -196,5 +196,5 @@ {#if isShowKeyboardShortcut} - (isShowKeyboardShortcut = false)} /> + (isShowKeyboardShortcut = false)} /> {/if} diff --git a/web/src/routes/admin/jobs-status/+page.svelte b/web/src/routes/admin/jobs-status/+page.svelte index dcd6630a01c56..16c2541e61b53 100644 --- a/web/src/routes/admin/jobs-status/+page.svelte +++ b/web/src/routes/admin/jobs-status/+page.svelte @@ -3,10 +3,17 @@ import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; import Icon from '$lib/components/elements/icon.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; + import Combobox, { type ComboBoxOption } from '$lib/components/shared-components/combobox.svelte'; + import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte'; + import { + notificationController, + NotificationType, + } from '$lib/components/shared-components/notification/notification'; import { AppRoute } from '$lib/constants'; import { asyncTimeout } from '$lib/utils'; - import { getAllJobsStatus, type AllJobStatusResponseDto } from '@immich/sdk'; - import { mdiCog } from '@mdi/js'; + import { handleError } from '$lib/utils/handle-error'; + import { createJob, getAllJobsStatus, ManualJobName, type AllJobStatusResponseDto } from '@immich/sdk'; + import { mdiCog, mdiPlus } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -16,6 +23,8 @@ let jobs: AllJobStatusResponseDto; let running = true; + let isOpen = false; + let selectedJob: ComboBoxOption | undefined = undefined; onMount(async () => { while (running) { @@ -27,10 +36,38 @@ onDestroy(() => { running = false; }); + + const options = [ + { title: $t('admin.person_cleanup_job'), value: ManualJobName.PersonCleanup }, + { title: $t('admin.tag_cleanup_job'), value: ManualJobName.TagCleanup }, + { title: $t('admin.user_cleanup_job'), value: ManualJobName.UserCleanup }, + ].map(({ value, title }) => ({ id: value, label: title, value })); + + const handleCancel = () => (isOpen = false); + + const handleCreate = async () => { + if (!selectedJob) { + return; + } + + try { + await createJob({ jobCreateDto: { name: selectedJob.value as ManualJobName } }); + notificationController.show({ message: $t('admin.job_created'), type: NotificationType.Info }); + handleCancel(); + } catch (error) { + handleError(error, $t('errors.unable_to_submit_job')); + } + };
+ (isOpen = true)}> +
+ + {$t('admin.create_job')} +
+
@@ -46,3 +83,24 @@ + +{#if isOpen} + + +
+ +
+ +
+{/if} diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 74db5628ba3a6..daa92491ebb60 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -20,7 +20,6 @@ getAllLibraries, getLibraryStatistics, getUserAdmin, - removeOfflineFiles, scanLibrary, updateLibrary, type LibraryResponseDto, @@ -122,7 +121,7 @@ const handleScanAll = async () => { try { for (const library of libraries) { - await scanLibrary({ id: library.id, scanLibraryDto: {} }); + await scanLibrary({ id: library.id }); } notificationController.show({ message: $t('admin.refreshing_all_libraries'), @@ -135,9 +134,9 @@ const handleScan = async (libraryId: string) => { try { - await scanLibrary({ id: libraryId, scanLibraryDto: {} }); + await scanLibrary({ id: libraryId }); notificationController.show({ - message: $t('admin.scanning_library_for_new_files'), + message: $t('admin.scanning_library'), type: NotificationType.Info, }); } catch (error) { @@ -145,42 +144,6 @@ } }; - const handleScanChanges = async (libraryId: string) => { - try { - await scanLibrary({ id: libraryId, scanLibraryDto: { refreshModifiedFiles: true } }); - notificationController.show({ - message: $t('admin.scanning_library_for_changed_files'), - type: NotificationType.Info, - }); - } catch (error) { - handleError(error, $t('errors.unable_to_scan_library')); - } - }; - - const handleForceScan = async (libraryId: string) => { - try { - await scanLibrary({ id: libraryId, scanLibraryDto: { refreshAllFiles: true } }); - notificationController.show({ - message: $t('admin.forcing_refresh_library_files'), - type: NotificationType.Info, - }); - } catch (error) { - handleError(error, $t('errors.unable_to_scan_library')); - } - }; - - const handleRemoveOffline = async (libraryId: string) => { - try { - await removeOfflineFiles({ id: libraryId }); - notificationController.show({ - message: $t('admin.removing_offline_files'), - type: NotificationType.Info, - }); - } catch (error) { - handleError(error, $t('errors.unable_to_remove_offline_files')); - } - }; - const onRenameClicked = (index: number) => { closeAll(); renameLibrary = index; @@ -193,7 +156,7 @@ updateLibraryIndex = index; }; - const onScanNewLibraryClicked = async (library: LibraryResponseDto) => { + const onScanClicked = async (library: LibraryResponseDto) => { closeAll(); if (library) { @@ -207,27 +170,6 @@ updateLibraryIndex = index; }; - const onScanAllLibraryFilesClicked = async (library: LibraryResponseDto) => { - closeAll(); - if (library) { - await handleScanChanges(library.id); - } - }; - - const onForceScanAllLibraryFilesClicked = async (library: LibraryResponseDto) => { - closeAll(); - if (library) { - await handleForceScan(library.id); - } - }; - - const onRemoveOfflineFilesClicked = async (library: LibraryResponseDto) => { - closeAll(); - if (library) { - await handleRemoveOffline(library.id); - } - }; - const handleDelete = async (library: LibraryResponseDto, index: number) => { closeAll(); @@ -267,10 +209,7 @@ {#if toCreateLibrary} - handleCreate(detail.ownerId)} - on:cancel={() => (toCreateLibrary = false)} - /> + (toCreateLibrary = false)} /> {/if} @@ -354,59 +293,37 @@ icon={mdiDotsVertical} title={$t('library_options')} > + onScanClicked(library)} text={$t('scan_library')} /> +
onRenameClicked(index)} text={$t('rename')} /> onEditImportPathClicked(index)} text={$t('edit_import_paths')} /> onScanSettingClicked(index)} text={$t('scan_settings')} />
- onScanNewLibraryClicked(library)} text={$t('scan_new_library_files')} /> onScanAllLibraryFilesClicked(library)} - text={$t('scan_all_library_files')} - subtitle={$t('only_refreshes_modified_files')} - /> - onForceScanAllLibraryFilesClicked(library)} - text={$t('force_re-scan_library_files')} - subtitle={$t('refreshes_every_file')} - /> -
- onRemoveOfflineFilesClicked(library)} - text={$t('remove_offline_files')} - /> - handleDelete(library, index)} activeColor="bg-red-200" textColor="text-red-600" - onClick={() => handleDelete(library, index)} + text={$t('delete_library')} /> {#if renameLibrary === index}
- handleUpdate(detail)} - on:cancel={() => (renameLibrary = null)} - /> + (renameLibrary = null)} />
{/if} {#if editImportPaths === index}
- handleUpdate(detail)} - on:cancel={() => (editImportPaths = null)} - /> + (editImportPaths = null)} />
{/if} {#if editScanSettings === index}
handleUpdate(library)} - on:cancel={() => (editScanSettings = null)} + onSubmit={handleUpdate} + onCancel={() => (editScanSettings = null)} />
{/if} diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index d03865cb39075..e443c1b51875d 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -27,11 +27,33 @@ import { copyToClipboard } from '$lib/utils'; import { downloadBlob } from '$lib/utils/asset-utils'; import type { SystemConfigDto } from '@immich/sdk'; - import { mdiAlert, mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js'; + import { + mdiAccountOutline, + mdiAlert, + mdiBellOutline, + mdiBookshelf, + mdiContentCopy, + mdiDatabaseOutline, + mdiDownload, + mdiFileDocumentOutline, + mdiFolderOutline, + mdiImageOutline, + mdiLockOutline, + mdiMapMarkerOutline, + mdiPaletteOutline, + mdiRobotOutline, + mdiServerOutline, + mdiSync, + mdiTrashCanOutline, + mdiUpdate, + mdiUpload, + mdiVideoOutline, + } from '@mdi/js'; import type { PageData } from './$types'; import { t } from 'svelte-i18n'; import type { ComponentType, SvelteComponent } from 'svelte'; import type { SettingsComponentProps } from '$lib/components/admin-page/settings/admin-settings'; + import SearchBar from '$lib/components/elements/search-bar.svelte'; export let data: PageData; @@ -68,104 +90,128 @@ title: string; subtitle: string; key: string; + icon: string; }> = [ { component: AuthSettings, title: $t('admin.authentication_settings'), subtitle: $t('admin.authentication_settings_description'), - key: 'image', + key: 'authentication', + icon: mdiLockOutline, }, { component: ImageSettings, title: $t('admin.image_settings'), subtitle: $t('admin.image_settings_description'), key: 'image', + icon: mdiImageOutline, }, { component: JobSettings, title: $t('admin.job_settings'), subtitle: $t('admin.job_settings_description'), key: 'job', + icon: mdiSync, }, { component: MetadataSettings, title: $t('admin.metadata_settings'), subtitle: $t('admin.metadata_settings_description'), key: 'metadata', + icon: mdiDatabaseOutline, }, { component: LibrarySettings, title: $t('admin.library_settings'), subtitle: $t('admin.library_settings_description'), key: 'external-library', + icon: mdiBookshelf, }, { component: LoggingSettings, title: $t('admin.logging_settings'), subtitle: $t('admin.manage_log_settings'), key: 'logging', + icon: mdiFileDocumentOutline, }, { component: MachineLearningSettings, title: $t('admin.machine_learning_settings'), subtitle: $t('admin.machine_learning_settings_description'), key: 'machine-learning', + icon: mdiRobotOutline, }, { component: MapSettings, title: $t('admin.map_gps_settings'), subtitle: $t('admin.map_gps_settings_description'), key: 'location', + icon: mdiMapMarkerOutline, }, { component: NotificationSettings, title: $t('admin.notification_settings'), subtitle: $t('admin.notification_settings_description'), key: 'notifications', + icon: mdiBellOutline, }, { component: ServerSettings, title: $t('admin.server_settings'), subtitle: $t('admin.server_settings_description'), key: 'server', + icon: mdiServerOutline, }, { component: StorageTemplateSettings, title: $t('admin.storage_template_settings'), subtitle: $t('admin.storage_template_settings_description'), key: 'storage-template', + icon: mdiFolderOutline, }, { component: ThemeSettings, title: $t('admin.theme_settings'), subtitle: $t('admin.theme_settings_description'), key: 'theme', + icon: mdiPaletteOutline, }, { component: TrashSettings, title: $t('admin.trash_settings'), subtitle: $t('admin.trash_settings_description'), key: 'trash', + icon: mdiTrashCanOutline, }, { component: UserSettings, title: $t('admin.user_settings'), subtitle: $t('admin.user_settings_description'), key: 'user-settings', + icon: mdiAccountOutline, }, { component: NewVersionCheckSettings, title: $t('admin.version_check_settings'), subtitle: $t('admin.version_check_settings_description'), key: 'version-check', + icon: mdiUpdate, }, { component: FFmpegSettings, title: $t('admin.transcoding_settings'), subtitle: $t('admin.transcoding_settings_description'), key: 'video-transcoding', + icon: mdiVideoOutline, }, ]; + + let searchQuery = ''; + + $: filteredSettings = settings.filter(({ title, subtitle }) => { + const query = searchQuery.toLowerCase(); + return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query); + }); @@ -182,6 +228,9 @@
+ copyToClipboard(JSON.stringify(config, null, 2))}>
@@ -206,10 +255,13 @@
-
+
+
+ +
- {#each settings as { component: Component, title, subtitle, key }} - + {#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)} + handleSave(config)} onReset={(options) => handleReset(options)} diff --git a/web/src/routes/admin/user-management/+page.svelte b/web/src/routes/admin/user-management/+page.svelte index 6028a7dae8c7a..2313b17cb1ea1 100644 --- a/web/src/routes/admin/user-management/+page.svelte +++ b/web/src/routes/admin/user-management/+page.svelte @@ -1,6 +1,5 @@