diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 07e07f422..13c2df740 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -87,7 +87,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }} - name: Build and push image - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v5.2.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index dd1c53468..5b92e44ff 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -121,7 +121,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v5.1.0 + uses: docker/build-push-action@v5.2.0 with: context: ${{ matrix.context }} file: ${{ matrix.file }} diff --git a/cli/package-lock.json b/cli/package-lock.json index 760d9863e..9574fa26f 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -16,7 +16,6 @@ }, "devDependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", - "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", @@ -47,7 +46,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.92.1", + "version": "1.98.0", "dev": true, "license": "GNU Affero General Public License version 3", "devDependencies": { @@ -389,12 +388,6 @@ "node": ">=6.9.0" } }, - "node_modules/@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "dev": true - }, "node_modules/@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -817,22 +810,22 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -853,9 +846,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@immich/sdk": { @@ -1230,15 +1223,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "node_modules/@testcontainers/postgresql": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", - "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", - "dev": true, - "dependencies": { - "testcontainers": "^10.7.1" - } - }, "node_modules/@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -1254,26 +1238,6 @@ "@types/node": "*" } }, - "node_modules/@types/docker-modem": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", - "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/ssh2": "*" - } - }, - "node_modules/@types/dockerode": { - "version": "3.3.23", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", - "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", - "dev": true, - "dependencies": { - "@types/docker-modem": "*", - "@types/node": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -1317,9 +1281,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1337,44 +1301,17 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, - "node_modules/@types/ssh2": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.18.tgz", - "integrity": "sha512-7eH4ppQMFlzvn//zhwD54MWaITR1aSc1oFBye9vb76GZ2Y9PSFYdwVIwOlxRXWs5+1hifntXyt+8a6SUbOD7Hg==", - "dev": true, - "dependencies": { - "@types/node": "^18.11.18" - } - }, - "node_modules/@types/ssh2-streams": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", - "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/ssh2/node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", - "dev": true, - "dependencies": { - "undici-types": "~5.26.4" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", - "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/type-utils": "7.0.2", - "@typescript-eslint/utils": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1400,15 +1337,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", - "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" }, "engines": { @@ -1428,13 +1365,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", - "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1445,13 +1382,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", - "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1472,9 +1409,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", - "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1485,13 +1422,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", - "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1537,17 +1474,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" }, "engines": { @@ -1562,12 +1499,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", - "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -1777,95 +1714,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/archiver-utils/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/archiver-utils/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1881,15 +1729,6 @@ "node": ">=8" } }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, "node_modules/assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", @@ -1899,70 +1738,12 @@ "node": "*" } }, - "node_modules/async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "node_modules/async-lock": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", - "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==", - "dev": true - }, - "node_modules/b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", - "dev": true - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2017,49 +1798,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "dev": true, - "optional": true, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -2072,15 +1810,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", @@ -2174,12 +1903,6 @@ "node": "*" } }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, "node_modules/ci-info": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", @@ -2255,21 +1978,6 @@ "node": ">=18" } }, - "node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2295,52 +2003,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cpu-features": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", - "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "dependencies": { - "buildcheck": "~0.0.6", - "nan": "^2.17.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true, - "bin": { - "crc32": "bin/crc32.njs" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2411,59 +2073,6 @@ "node": ">=8" } }, - "node_modules/docker-compose": { - "version": "0.24.3", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.3.tgz", - "integrity": "sha512-x3/QN3AIOMe7j2c8f/jcycizMft7dl8MluoB9OGPAYCyKHHiPUFqI9GjCcsU0kYy24vYKMCcfR6+5ZaEyQlrxg==", - "dev": true, - "dependencies": { - "yaml": "^2.2.2" - }, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/docker-modem": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz", - "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==", - "dev": true, - "dependencies": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.11.0" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz", - "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==", - "dev": true, - "dependencies": { - "@balena/dockerignore": "^1.0.2", - "docker-modem": "^3.0.0", - "tar-fs": "~2.0.1" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/dockerode/node_modules/tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "dev": true, - "dependencies": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -2494,15 +2103,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2572,16 +2172,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -2701,19 +2301,7 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/eslint-scope": { + "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", @@ -2729,13 +2317,16 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, "engines": { - "node": ">=4.0" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { @@ -2767,15 +2358,6 @@ "node": ">=0.10" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, "node_modules/esrecurse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", @@ -2788,7 +2370,7 @@ "node": ">=4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { + "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", @@ -2850,12 +2432,6 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "node_modules/fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -2981,12 +2557,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3022,18 +2592,6 @@ "node": "*" } }, - "node_modules/get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "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", @@ -3139,12 +2697,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3193,26 +2745,6 @@ "node": ">=16.17.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3365,12 +2897,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3514,48 +3040,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3611,42 +3095,12 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "node_modules/lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true - }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "node_modules/loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -3773,24 +3227,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, "node_modules/mlly": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", @@ -3818,13 +3254,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", - "dev": true, - "optional": true - }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -3849,26 +3278,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -3896,15 +3305,6 @@ "semver": "bin/semver" } }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -4263,55 +3663,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/properties-reader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", - "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", - "dev": true, - "dependencies": { - "mkdirp": "^1.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/steveukx/properties?sponsor=1" - } - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -4341,12 +3692,6 @@ } ] }, - "node_modules/queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -4455,50 +3800,6 @@ "node": ">=8" } }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "dependencies": { - "minimatch": "^5.1.0" - } - }, - "node_modules/readdir-glob/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/readdir-glob/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -4555,15 +3856,6 @@ "node": ">=4" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -4664,32 +3956,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4821,50 +4087,6 @@ "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", "dev": true }, - "node_modules/split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "dev": true - }, - "node_modules/ssh-remote-port-forward": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", - "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", - "dev": true, - "dependencies": { - "@types/ssh2": "^0.5.48", - "ssh2": "^1.4.0" - } - }, - "node_modules/ssh-remote-port-forward/node_modules/@types/ssh2": { - "version": "0.5.52", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", - "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/ssh2-streams": "*" - } - }, - "node_modules/ssh2": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", - "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2" - }, - "engines": { - "node": ">=10.16.0" - }, - "optionalDependencies": { - "cpu-features": "~0.0.8", - "nan": "^2.17.0" - } - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4877,25 +4099,6 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, - "node_modules/streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", - "dev": true, - "dependencies": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5044,44 +4247,6 @@ "url": "https://opencollective.com/unts" } }, - "node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dev": true, - "dependencies": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - } - }, - "node_modules/tar-fs/node_modules/tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", - "dev": true, - "dependencies": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5116,29 +4281,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/testcontainers": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", - "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", - "dev": true, - "dependencies": { - "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.21", - "archiver": "^5.3.2", - "async-lock": "^1.4.0", - "byline": "^5.0.0", - "debug": "^4.3.4", - "docker-compose": "^0.24.2", - "dockerode": "^3.3.5", - "get-port": "^5.1.1", - "node-fetch": "^2.7.0", - "proper-lockfile": "^4.1.2", - "properties-reader": "^2.3.0", - "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.4", - "tmp": "^0.2.1" - } - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5169,18 +4311,6 @@ "node": ">=14.0.0" } }, - "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5202,12 +4332,6 @@ "node": ">=8.0" } }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, "node_modules/ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -5226,12 +4350,6 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5329,12 +4447,6 @@ "punycode": "^2.1.0" } }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, "node_modules/v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -5501,22 +4613,6 @@ } } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5652,10 +4748,13 @@ "dev": true }, "node_modules/yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz", + "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==", "dev": true, + "bin": { + "yaml": "bin.mjs" + }, "engines": { "node": ">= 14" } @@ -5671,61 +4770,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/zip-stream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } } }, "dependencies": { @@ -5911,12 +4955,6 @@ "to-fast-properties": "^2.0.0" } }, - "@balena/dockerignore": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", - "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", - "dev": true - }, "@bcoe/v8-coverage": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", @@ -6117,19 +5155,19 @@ } }, "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true }, "@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" } }, @@ -6140,9 +5178,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "@immich/sdk": { @@ -6391,15 +5429,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "@testcontainers/postgresql": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", - "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", - "dev": true, - "requires": { - "testcontainers": "^10.7.1" - } - }, "@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -6415,26 +5444,6 @@ "@types/node": "*" } }, - "@types/docker-modem": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", - "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/ssh2": "*" - } - }, - "@types/dockerode": { - "version": "3.3.23", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", - "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", - "dev": true, - "requires": { - "@types/docker-modem": "*", - "@types/node": "*" - } - }, "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6478,9 +5487,9 @@ } }, "@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -6498,46 +5507,17 @@ "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, - "@types/ssh2": { - "version": "1.11.18", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.18.tgz", - "integrity": "sha512-7eH4ppQMFlzvn//zhwD54MWaITR1aSc1oFBye9vb76GZ2Y9PSFYdwVIwOlxRXWs5+1hifntXyt+8a6SUbOD7Hg==", - "dev": true, - "requires": { - "@types/node": "^18.11.18" - }, - "dependencies": { - "@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", - "dev": true, - "requires": { - "undici-types": "~5.26.4" - } - } - } - }, - "@types/ssh2-streams": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.12.tgz", - "integrity": "sha512-Sy8tpEmCce4Tq0oSOYdfqaBpA3hDM8SoxoFh5vzFsu2oL+znzGz8oVWW7xb4K920yYMUY+PIG31qZnFMfPWNCg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@typescript-eslint/eslint-plugin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", - "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/type-utils": "7.0.2", - "@typescript-eslint/utils": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -6547,54 +5527,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", - "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", - "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" } }, "@typescript-eslint/type-utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", - "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", - "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", - "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -6624,27 +5604,27 @@ } }, "@typescript-eslint/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", - "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -6792,85 +5772,6 @@ "color-convert": "^2.0.1" } }, - "archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "requires": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -6883,71 +5784,18 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, "assertion-error": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "dev": true }, - "async": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true - }, - "async-lock": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.0.tgz", - "integrity": "sha512-coglx5yIWuetakm3/1dsX9hxCNox22h7+V80RQOu2XUUMidtArxKoZoOtHUPuR84SycKTXzgGzAUR5hJxujyJQ==", - "dev": true - }, - "b4a": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", - "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", - "dev": true - }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -6979,41 +5827,12 @@ "update-browserslist-db": "^1.0.13" } }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "dev": true, - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, - "buildcheck": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", - "integrity": "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A==", - "dev": true, - "optional": true - }, "builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, - "byline": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", - "integrity": "sha512-s6webAy+R4SR8XVuJWt2V2rGvhnrhxN+9S15GNuTK3wKPOXFF6RNc+8ug2XhH+2s4f+uudG4kUVYmYOQWL2g0Q==", - "dev": true - }, "byte-size": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", @@ -7072,12 +5891,6 @@ "get-func-name": "^2.0.2" } }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, "ci-info": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", @@ -7131,18 +5944,6 @@ "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", "dev": true }, - "compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -7164,39 +5965,6 @@ "browserslist": "^4.22.2" } }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "cpu-features": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.9.tgz", - "integrity": "sha512-AKjgn2rP2yJyfbepsmLfiYcmtNn/2eUvocUyM/09yB0YDiz39HteK/5/T4Onf0pmdYDMgkBoGvRLvEguzyL7wQ==", - "dev": true, - "optional": true, - "requires": { - "buildcheck": "~0.0.6", - "nan": "^2.17.0" - } - }, - "crc-32": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", - "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", - "dev": true - }, - "crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "requires": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - } - }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -7247,52 +6015,6 @@ "path-type": "^4.0.0" } }, - "docker-compose": { - "version": "0.24.3", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.3.tgz", - "integrity": "sha512-x3/QN3AIOMe7j2c8f/jcycizMft7dl8MluoB9OGPAYCyKHHiPUFqI9GjCcsU0kYy24vYKMCcfR6+5ZaEyQlrxg==", - "dev": true, - "requires": { - "yaml": "^2.2.2" - } - }, - "docker-modem": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.8.tgz", - "integrity": "sha512-f0ReSURdM3pcKPNS30mxOHSbaFLcknGmQjwSfmbcdOw1XWKXVhukM3NJHhr7NpY9BIyyWQb0EBo3KQvvuU5egQ==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "readable-stream": "^3.5.0", - "split-ca": "^1.0.1", - "ssh2": "^1.11.0" - } - }, - "dockerode": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.5.tgz", - "integrity": "sha512-/0YNa3ZDNeLr/tSckmD69+Gq+qVNhvKfAHNeZJBnp7EOP6RGKV8ORrJHkUn20So5wU+xxT7+1n5u8PjHbfjbSA==", - "dev": true, - "requires": { - "@balena/dockerignore": "^1.0.2", - "docker-modem": "^3.0.0", - "tar-fs": "~2.0.1" - }, - "dependencies": { - "tar-fs": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", - "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", - "dev": true, - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - } - } - }, "doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -7320,15 +6042,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7382,16 +6095,16 @@ "dev": true }, "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -7425,24 +6138,6 @@ "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" - }, - "dependencies": { - "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - } - }, - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } } }, "eslint-config-prettier": { @@ -7486,6 +6181,16 @@ "strip-indent": "^3.0.0" } }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -7510,14 +6215,6 @@ "dev": true, "requires": { "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } } }, "esrecurse": { @@ -7527,16 +6224,14 @@ "dev": true, "requires": { "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } } }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, "estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -7581,12 +6276,6 @@ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", "dev": true }, - "fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "dev": true - }, "fast-glob": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", @@ -7687,12 +6376,6 @@ "signal-exit": "^4.0.1" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -7718,12 +6401,6 @@ "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true }, - "get-port": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", - "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", - "dev": true - }, "get-stream": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", @@ -7795,12 +6472,6 @@ "slash": "^3.0.0" } }, - "graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true - }, "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -7840,12 +6511,6 @@ "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -7953,12 +6618,6 @@ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -8073,47 +6732,6 @@ "json-buffer": "3.0.1" } }, - "lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dev": true, - "requires": { - "readable-stream": "^2.0.5" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -8154,42 +6772,12 @@ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", - "dev": true - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "loupe": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", @@ -8283,18 +6871,6 @@ "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", "dev": true }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true - }, "mlly": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", @@ -8319,13 +6895,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==", - "dev": true, - "optional": true - }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -8338,15 +6907,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "requires": { - "whatwg-url": "^5.0.0" - } - }, "node-releases": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", @@ -8373,12 +6933,6 @@ } } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, "npm-run-path": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", @@ -8612,50 +7166,6 @@ } } }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - }, - "dependencies": { - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - } - } - }, - "properties-reader": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/properties-reader/-/properties-reader-2.3.0.tgz", - "integrity": "sha512-z597WicA7nDZxK12kZqHr2TcvwNU1GCfA5UwfDY/HDp3hXPoPlb5rlEx9bwGTiJnc0OqbBTkU975jDToth8Gxw==", - "dev": true, - "requires": { - "mkdirp": "^1.0.4" - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8668,12 +7178,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "queue-tick": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", - "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", - "dev": true - }, "react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", @@ -8756,46 +7260,6 @@ } } }, - "readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "readdir-glob": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", - "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", - "dev": true, - "requires": { - "minimatch": "^5.1.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "requires": { - "brace-expansion": "^2.0.1" - } - } - } - }, "regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -8836,12 +7300,6 @@ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true - }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -8905,18 +7363,6 @@ "queue-microtask": "^1.2.2" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -9020,46 +7466,6 @@ "integrity": "sha512-lpT8hSQp9jAKp9mhtBU4Xjon8LPGBvLIuBiSVhMEtmLecTh2mO0tlqrAMp47tBXzMr13NJMQ2lf7RpQGLJ3HsQ==", "dev": true }, - "split-ca": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", - "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", - "dev": true - }, - "ssh-remote-port-forward": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/ssh-remote-port-forward/-/ssh-remote-port-forward-1.0.4.tgz", - "integrity": "sha512-x0LV1eVDwjf1gmG7TTnfqIzf+3VPRz7vrNIjX6oYLbeCrf/PeVY6hkT68Mg+q02qXxQhrLjB0jfgvhevoCRmLQ==", - "dev": true, - "requires": { - "@types/ssh2": "^0.5.48", - "ssh2": "^1.4.0" - }, - "dependencies": { - "@types/ssh2": { - "version": "0.5.52", - "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.52.tgz", - "integrity": "sha512-lbLLlXxdCZOSJMCInKH2+9V/77ET2J6NPQHpFI0kda61Dd1KglJs+fPQBchizmzYSOJBgdTajhPqBO1xxLywvg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/ssh2-streams": "*" - } - } - } - }, - "ssh2": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.14.0.tgz", - "integrity": "sha512-AqzD1UCqit8tbOKoj6ztDDi1ffJZ2rV2SwlgrVVrHPkV5vWqGJOVp5pmtj18PunkPJAuKQsnInyKV+/Nb2bUnA==", - "dev": true, - "requires": { - "asn1": "^0.2.6", - "bcrypt-pbkdf": "^1.0.2", - "cpu-features": "~0.0.8", - "nan": "^2.17.0" - } - }, "stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -9072,25 +7478,6 @@ "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, - "streamx": { - "version": "2.15.6", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz", - "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==", - "dev": true, - "requires": { - "fast-fifo": "^1.1.0", - "queue-tick": "^1.0.1" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, - "requires": { - "safe-buffer": "~5.2.0" - } - }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9194,43 +7581,6 @@ "tslib": "^2.6.2" } }, - "tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", - "dev": true, - "requires": { - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "dependencies": { - "tar-stream": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.6.tgz", - "integrity": "sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==", - "dev": true, - "requires": { - "b4a": "^1.6.4", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - } - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -9258,29 +7608,6 @@ } } }, - "testcontainers": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", - "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", - "dev": true, - "requires": { - "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.21", - "archiver": "^5.3.2", - "async-lock": "^1.4.0", - "byline": "^5.0.0", - "debug": "^4.3.4", - "docker-compose": "^0.24.2", - "dockerode": "^3.3.5", - "get-port": "^5.1.1", - "node-fetch": "^2.7.0", - "proper-lockfile": "^4.1.2", - "properties-reader": "^2.3.0", - "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.4", - "tmp": "^0.2.1" - } - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -9305,15 +7632,6 @@ "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", "dev": true }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } - }, "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -9329,12 +7647,6 @@ "is-number": "^7.0.0" } }, - "tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, "ts-api-utils": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", @@ -9348,12 +7660,6 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -9412,12 +7718,6 @@ "punycode": "^2.1.0" } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, "v8-to-istanbul": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz", @@ -9492,22 +7792,6 @@ "why-is-node-running": "^2.2.2" } }, - "webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "requires": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -9596,9 +7880,9 @@ "dev": true }, "yaml": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz", - "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.0.tgz", + "integrity": "sha512-j9iR8g+/t0lArF4V6NE/QCfT+CO7iLqrXAHZbJdo+LfjqP1vR8Fg5bSiaq6Q2lOD1AUEVrEVIgABvBFYojJVYQ==", "dev": true }, "yocto-queue": { @@ -9606,51 +7890,6 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true - }, - "zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "requires": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "dependencies": { - "archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "requires": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } } } } diff --git a/cli/package.json b/cli/package.json index de36dfcf6..630add5b0 100644 --- a/cli/package.json +++ b/cli/package.json @@ -14,7 +14,6 @@ ], "devDependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", - "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", diff --git a/cli/src/commands/upload.command.ts b/cli/src/commands/upload.command.ts index 8029b1313..250fd79c6 100644 --- a/cli/src/commands/upload.command.ts +++ b/cli/src/commands/upload.command.ts @@ -66,8 +66,8 @@ class Asset { assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)), deviceAssetId: this.deviceAssetId, deviceId: 'CLI', - fileCreatedAt: this.fileCreatedAt, - fileModifiedAt: this.fileModifiedAt, + fileCreatedAt: this.fileCreatedAt.toISOString(), + fileModifiedAt: this.fileModifiedAt.toISOString(), isFavorite: String(false), }; const formData = new FormData(); diff --git a/cli/src/services/session.service.ts b/cli/src/services/session.service.ts index c9eae671f..0235b30a4 100644 --- a/cli/src/services/session.service.ts +++ b/cli/src/services/session.service.ts @@ -3,6 +3,7 @@ import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/p import path from 'node:path'; import yaml from 'yaml'; import { ImmichApi } from './api.service'; + class LoginError extends Error { constructor(message: string) { super(message); @@ -14,14 +15,12 @@ class LoginError extends Error { } export class SessionService { - readonly configDirectory!: string; - readonly authPath!: string; - - constructor(configDirectory: string) { - this.configDirectory = configDirectory; - this.authPath = path.join(configDirectory, '/auth.yml'); + private get authPath() { + return path.join(this.configDirectory, '/auth.yml'); } + constructor(private configDirectory: string) {} + async connect(): Promise { let instanceUrl = process.env.IMMICH_INSTANCE_URL; let apiKey = process.env.IMMICH_API_KEY; @@ -48,6 +47,8 @@ export class SessionService { } } + instanceUrl = await this.resolveApiEndpoint(instanceUrl); + const api = new ImmichApi(instanceUrl, apiKey); const pingResponse = await api.pingServer().catch((error) => { @@ -62,7 +63,9 @@ export class SessionService { } async login(instanceUrl: string, apiKey: string): Promise { - console.log('Logging in...'); + console.log(`Logging in to ${instanceUrl}`); + + instanceUrl = await this.resolveApiEndpoint(instanceUrl); const api = new ImmichApi(instanceUrl, apiKey); @@ -83,7 +86,7 @@ export class SessionService { await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 }); - console.log('Wrote auth info to ' + this.authPath); + console.log(`Wrote auth info to ${this.authPath}`); return api; } @@ -98,4 +101,18 @@ export class SessionService { console.log('Successfully logged out'); } + + private async resolveApiEndpoint(instanceUrl: string): Promise { + const wellKnownUrl = new URL('.well-known/immich', instanceUrl); + try { + const wellKnown = await fetch(wellKnownUrl).then((response) => response.json()); + const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString(); + if (endpoint !== instanceUrl) { + console.debug(`Discovered API at ${endpoint}`); + } + return endpoint; + } catch { + return instanceUrl; + } + } } diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index 58dd707ea..28c68ced4 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -38,7 +38,7 @@ Note: Either a manual or scheduled library scan must have been performed to iden 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 user account settings > 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. +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 @@ -50,8 +50,6 @@ If the import paths are edited in a way that an external file is no longer in an Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check: -- Is the external path set correctly? Each import path must be contained in the external path. -- Make sure the external path does not contain spaces - In the docker-compose file, are the volumes mounted correctly? - Are the volumes identical between the `server` and `microservices` container? - Are the import paths set correctly, and do they match the path set in docker-compose file? @@ -61,18 +59,6 @@ Sometimes, an external library will not scan correctly. This can happen if immic To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files. -### Security Considerations - -:::caution - -Please read and understand this section before setting external paths, as there are important security considerations. - -::: - -For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server. - -With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below. - ### Exclusion Patterns By default, all files in the import paths will be added to the library. If there are files that should not be added, exclusion patterns can be used to exclude them. Exclusion patterns are glob patterns are matched against the full file path. If a file matches an exclusion pattern, it will not be added to the library. Exclusion patterns can be added in the Scan Settings page for each library. Under the hood, Immich uses the [glob](https://www.npmjs.com/package/glob) package to match patterns, so please refer to [their documentation](https://github.com/isaacs/node-glob#glob-primer) to see what patterns are supported. @@ -90,6 +76,16 @@ This feature - currently hidden in the config file - is considered experimental 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. +#### Troubleshooting + +If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files. + +``` +ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg' +``` + +In rare cases, the library watcher can hang, preventing Immich from starting up. In this case, disable the library watcher in the configuration file. If the watcher is enabled from within Immich, the app must be started without the microservices. Disable the microservices in the docker compose file, start Immich, disable the library watcher in the admin settings, close Immich, re-enable the microservices, and then Immich can be started normally. + ### 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. @@ -135,27 +131,13 @@ The `ro` flag at the end only gives read-only access to the volumes. While Immic _Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._ ::: -### Set External Path - -Only an admin can do this. - -- Navigate to `Administration > Users` page on the web. -- Click on the user edit button. -- Set `/mnt/media` to be the external path. This folder will only contain the three folders that we want to import, so nothing else can be accessed. - :::note - Spaces in the internal path aren't currently supported. - - You must import it as: - `..:/mnt/media/my-media:ro` - instead of - `..:/mnt/media/my media:ro` - ::: - ### Create External Libraries -- Click on your user name in the top right corner -> Account Settings -- Click on Libraries +These actions must be performed by the Immich administrator. + +- Click on Administration -> Libraries - Click on Create External Library +- Select which user owns the library, this can not be changed later - Click the drop-down menu on the newly created library - Click on Rename Library and rename it to "Christmas Trip" - Click Edit Import Paths @@ -166,7 +148,7 @@ NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/ Next, we'll add an exclusion pattern to filter out raw files. -- Click the drop-down menu on the newly christmas library +- Click the drop-down menu on the newly-created Christmas library - Click on Manage - Click on Scan Settings - Click on Add Exclusion Pattern diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 49b7330fb..fe369f899 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -13,7 +13,7 @@ Run `docker exec -it immich_postgres psql immich ` to connect to th ## Assets :::note -The `"originalFileName"` column is the name of the uploaded file _without_ the extension. +The `"originalFileName"` column is the name of the file at time of upload, including the extension. ::: ```sql title="Find by original filename" @@ -40,6 +40,10 @@ SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL; SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL; ``` +```sql title="size < 100,000 bytes, smallest to largest" +SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC; +``` + ```sql title="Without thumbnails" SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL; ``` diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 9de18f5d5..b1d4b67b2 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -28,6 +28,10 @@ On my computer, for example, I use this path: EXTERNAL_PATH=/home/tenino/photos ``` +:::info EXTERNAL_PATH design +The design choice to put the EXTERNAL_PATH into .env rather than put two copies of the absolute path in the yml file in order to make everything easier, so if you have two copies of the same path that have to be kept in sync, then someday later when you move the data, update only one of the paths, without everything will break mysteriously. +::: + Restart Immich. ``` @@ -35,47 +39,26 @@ docker compose down docker compose up -d ``` -# Set the External Path +# Create the library In the Immich web UI: - click the **Administration** link in the upper right corner. -- Select the **Users** tab - +- Select the **External Libraries** tab + -- Select the **pencil** next to your user ID - +- Click the **Create Library** button + -- Fill in the **External Path** field with `/usr/src/app/external` - - -Notice this matches the path _inside the container_ where we mounted your photos. -The purpose of the external path field is for administrators who have multiple users -on their Immich instance. It lets you prevent other authorized users from -navigating to your external library. - -# Import the library - -In the Immich web UI: - -- Click your user avatar in the upper-right corner (circle with your initials) - - -- Click **Account Settings** - - -- Click to expand **Libraries** - - -- Click the **Create External Library** button - +- In the dialog, select which user should own the new library + - Click the three-dots menu and select **Edit Import Paths** -- Click \*_Add path_ +- Click Add path - Enter **/usr/src/app/external** as the path and click Add diff --git a/docs/docs/guides/img/account-settings.png b/docs/docs/guides/img/account-settings.png deleted file mode 100644 index 8cece6f46..000000000 Binary files a/docs/docs/guides/img/account-settings.png and /dev/null differ diff --git a/docs/docs/guides/img/create-external-library-button.png b/docs/docs/guides/img/create-external-library-button.png deleted file mode 100644 index 66b19ef0d..000000000 Binary files a/docs/docs/guides/img/create-external-library-button.png and /dev/null differ diff --git a/docs/docs/guides/img/create-external-library.png b/docs/docs/guides/img/create-external-library.png new file mode 100644 index 000000000..7ccd53aed Binary files /dev/null and b/docs/docs/guides/img/create-external-library.png differ diff --git a/docs/docs/guides/img/external-libraries.png b/docs/docs/guides/img/external-libraries.png new file mode 100644 index 000000000..0d0f5d237 Binary files /dev/null and b/docs/docs/guides/img/external-libraries.png differ diff --git a/docs/docs/guides/img/external-path.png b/docs/docs/guides/img/external-path.png deleted file mode 100644 index bc54f781f..000000000 Binary files a/docs/docs/guides/img/external-path.png and /dev/null differ diff --git a/docs/docs/guides/img/library-owner.png b/docs/docs/guides/img/library-owner.png new file mode 100644 index 000000000..de630b51b Binary files /dev/null and b/docs/docs/guides/img/library-owner.png differ diff --git a/docs/docs/guides/img/pencil.png b/docs/docs/guides/img/pencil.png deleted file mode 100644 index d3b8f4505..000000000 Binary files a/docs/docs/guides/img/pencil.png and /dev/null differ diff --git a/docs/docs/guides/img/users-tab.png b/docs/docs/guides/img/users-tab.png deleted file mode 100644 index f634989ab..000000000 Binary files a/docs/docs/guides/img/users-tab.png and /dev/null differ diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md index 8a7776a42..0d7c8dafc 100644 --- a/docs/docs/install/config-file.md +++ b/docs/docs/install/config-file.md @@ -128,6 +128,9 @@ The default configuration looks like this: "theme": { "customCss": "" }, + "user": { + "deleteDelay": 7 + }, "library": { "scan": { "enabled": true, diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 2849c6054..a915e20e2 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -124,16 +124,18 @@ Redis (Sentinel) URL example JSON before encoding: ## Machine Learning -| Variable | Description | Default | Services | -| :----------------------------------------------- | :----------------------------------------------------------------- | :-----------------: | :--------------- | -| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | -| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | -| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | -| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | -| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | -| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | -| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | -| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning | +| Variable | Description | Default | Services | +| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- | +| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | +| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | +| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | +| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | +| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | +| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | +| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. diff --git a/docs/docs/install/truenas.md b/docs/docs/install/truenas.md index 742478f10..271cd52ca 100644 --- a/docs/docs/install/truenas.md +++ b/docs/docs/install/truenas.md @@ -28,6 +28,10 @@ Or before beginning app installation, [create the datasets](https://www.truenas. Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**. You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on. +:::info Permissions +The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions. +::: + ## Installing the Immich Application To install the **Immich** application, go to **Apps**, click **Discover Apps**, either begin typing Immich into the search field or scroll down to locate the **Immich** application widget. diff --git a/docs/package-lock.json b/docs/package-lock.json index c78ca4b2e..9a55af7f5 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -12231,7 +12231,7 @@ "mime-format": "2.0.0", "mime-types": "2.1.27", "postman-url-encoder": "2.1.3", - "sanitize-html": "^2.11.0", + "sanitize-html": "^2.12.1", "semver": "^7.5.4", "uuid": "3.4.0" } @@ -14762,9 +14762,9 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/sanitize-html": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz", - "integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz", + "integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==", "dependencies": { "deepmerge": "^4.2.2", "escape-string-regexp": "^4.0.0", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 7d013eeca..338d6d2a1 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -49,7 +49,6 @@ }, "devDependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", - "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", @@ -80,7 +79,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.97.0", + "version": "1.98.0", "dev": true, "license": "GNU Affero General Public License version 3", "devDependencies": { @@ -924,12 +923,12 @@ } }, "node_modules/@playwright/test": { - "version": "1.41.2", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", - "integrity": "sha512-qQB9h7KbibJzrDpkXkYvsmiDJK14FULCCZgEcoe2AvFAS64oCirWTwzTlAYEbKaRxWs5TFesE1Na6izMv3HfGg==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.42.1.tgz", + "integrity": "sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==", "dev": true, "dependencies": { - "playwright": "1.41.2" + "playwright": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -1156,9 +1155,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3870,12 +3869,12 @@ } }, "node_modules/playwright": { - "version": "1.41.2", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.41.2.tgz", - "integrity": "sha512-v0bOa6H2GJChDL8pAeLa/LZC4feoAMbSQm1/jF/ySsWWoaNItvrMP7GEkvEEFyCTUYKMxjQKaTSg5up7nR6/8A==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.42.1.tgz", + "integrity": "sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==", "dev": true, "dependencies": { - "playwright-core": "1.41.2" + "playwright-core": "1.42.1" }, "bin": { "playwright": "cli.js" @@ -3888,9 +3887,9 @@ } }, "node_modules/playwright-core": { - "version": "1.41.2", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.41.2.tgz", - "integrity": "sha512-VaTvwCA4Y8kxEe+kfm2+uUUw5Lubf38RxF7FpBxLPmGe5sdNkSg5e3ChEigaGrX7qdqT3pt2m/98LiyvU2x6CA==", + "version": "1.42.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.42.1.tgz", + "integrity": "sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==", "dev": true, "bin": { "playwright-core": "cli.js" diff --git a/e2e/package.json b/e2e/package.json index 9f231c9dd..14685df51 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -5,7 +5,8 @@ "main": "index.js", "type": "module", "scripts": { - "test": "vitest --config vitest.config.ts", + "test": "vitest --run", + "test:watch": "vitest", "test:web": "npx playwright test", "start:web": "npx playwright test --ui", "format": "prettier --check .", diff --git a/e2e/src/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts index 365ad66dc..5d3cf7220 100644 --- a/e2e/src/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -9,7 +9,7 @@ import { } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; @@ -23,12 +23,11 @@ describe('/activity', () => { create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) }); beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); - nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); - asset = await apiUtils.createAsset(admin.accessToken); + admin = await utils.adminSetup(); + nonOwner = await utils.userSetup(admin.accessToken, createUserDto.user1); + asset = await utils.createAsset(admin.accessToken); album = await createAlbum( { createAlbumDto: { @@ -42,7 +41,7 @@ describe('/activity', () => { }); beforeEach(async () => { - await dbUtils.reset(['activity']); + await utils.resetDatabase(['activity']); }); describe('GET /activity', () => { diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 99a50106e..4faa5eac3 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -7,7 +7,7 @@ import { } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; @@ -29,49 +29,48 @@ describe('/album', () => { let user3: LoginResponseDto; // deleted beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); [user1, user2, user3] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); [user1Asset1, user1Asset2] = await Promise.all([ - apiUtils.createAsset(user1.accessToken, { isFavorite: true }), - apiUtils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken, { isFavorite: true }), + utils.createAsset(user1.accessToken), ]); const albums = await Promise.all([ // user 1 - apiUtils.createAlbum(user1.accessToken, { + utils.createAlbum(user1.accessToken, { albumName: user1SharedUser, sharedWithUserIds: [user2.userId], assetIds: [user1Asset1.id], }), - apiUtils.createAlbum(user1.accessToken, { + utils.createAlbum(user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset1.id], }), - apiUtils.createAlbum(user1.accessToken, { + utils.createAlbum(user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset1.id, user1Asset2.id], }), // user 2 - apiUtils.createAlbum(user2.accessToken, { + utils.createAlbum(user2.accessToken, { albumName: user2SharedUser, sharedWithUserIds: [user1.userId], assetIds: [user1Asset1.id], }), - apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), - apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }), + utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), + utils.createAlbum(user2.accessToken, { albumName: user2NotShared }), // user 3 - apiUtils.createAlbum(user3.accessToken, { + utils.createAlbum(user3.accessToken, { albumName: 'Deleted', sharedWithUserIds: [user1.userId], }), @@ -82,12 +81,12 @@ describe('/album', () => { await Promise.all([ // add shared link to user1SharedLink album - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: user1Albums[1].id, }), // add shared link to user2SharedLink album - apiUtils.createSharedLink(user2.accessToken, { + utils.createSharedLink(user2.accessToken, { type: SharedLinkType.Album, albumId: user2Albums[1].id, }), @@ -366,7 +365,7 @@ describe('/album', () => { }); it('should be able to add own asset to own album', async () => { - const asset = await apiUtils.createAsset(user1.accessToken); + const asset = await utils.createAsset(user1.accessToken); const { status, body } = await request(app) .put(`/album/${user1Albums[0].id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) @@ -377,7 +376,7 @@ describe('/album', () => { }); it('should be able to add own asset to shared album', async () => { - const asset = await apiUtils.createAsset(user1.accessToken); + const asset = await utils.createAsset(user1.accessToken); const { status, body } = await request(app) .put(`/album/${user2Albums[0].id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) @@ -398,7 +397,7 @@ describe('/album', () => { }); it('should update an album', async () => { - const album = await apiUtils.createAlbum(user1.accessToken, { + const album = await utils.createAlbum(user1.accessToken, { albumName: 'New album', }); const { status, body } = await request(app) @@ -485,7 +484,7 @@ describe('/album', () => { let album: AlbumResponseDto; beforeEach(async () => { - album = await apiUtils.createAlbum(user1.accessToken, { + album = await utils.createAlbum(user1.accessToken, { albumName: 'testAlbum', }); }); diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 2873bb0c3..f1bb35531 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -7,13 +7,12 @@ import { } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; import { DateTime } from 'luxon'; -import { createHash } from 'node:crypto'; import { readFile, writeFile } from 'node:fs/promises'; import { basename, join } from 'node:path'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils, tempDir, testAssetDir, wsUtils } from 'src/utils'; +import { app, tempDir, testAssetDir, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -21,8 +20,6 @@ const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; -const sha1 = (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'); - const readTags = async (bytes: Buffer, filename: string) => { const filepath = join(tempDir, filename); await writeFile(filepath, bytes); @@ -47,42 +44,41 @@ describe('/asset', () => { let ws: Socket; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); [ws, user1, user2, userStats] = await Promise.all([ - wsUtils.connect(admin.accessToken), - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.connectWebsocket(admin.accessToken), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); // asset location - assetLocation = await apiUtils.createAsset(admin.accessToken, { + assetLocation = await utils.createAsset(admin.accessToken, { assetData: { filename: 'thompson-springs.jpg', bytes: await readFile(locationAssetFilepath), }, }); - await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id }); + await utils.waitForWebsocketEvent({ event: 'upload', assetId: assetLocation.id }); user1Assets = await Promise.all([ - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken, { + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken, { isFavorite: true, isReadOnly: true, fileCreatedAt: yesterday.toISO(), fileModifiedAt: yesterday.toISO(), assetData: { filename: 'example.mp4' }, }), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), ]); - user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]); + user2Assets = await Promise.all([utils.createAsset(user2.accessToken)]); for (const asset of [...user1Assets, ...user2Assets]) { expect(asset.duplicate).toBe(false); @@ -90,27 +86,27 @@ describe('/asset', () => { await Promise.all([ // stats - apiUtils.createAsset(userStats.accessToken), - apiUtils.createAsset(userStats.accessToken, { isFavorite: true }), - apiUtils.createAsset(userStats.accessToken, { isArchived: true }), - apiUtils.createAsset(userStats.accessToken, { + utils.createAsset(userStats.accessToken), + utils.createAsset(userStats.accessToken, { isFavorite: true }), + utils.createAsset(userStats.accessToken, { isArchived: true }), + utils.createAsset(userStats.accessToken, { isArchived: true, isFavorite: true, assetData: { filename: 'example.mp4' }, }), ]); - const person1 = await apiUtils.createPerson(user1.accessToken, { + const person1 = await utils.createPerson(user1.accessToken, { name: 'Test Person', }); - await dbUtils.createFace({ + await utils.createFace({ assetId: user1Assets[0].id, personId: person1.id, }); }, 30_000); afterAll(() => { - wsUtils.disconnect(ws); + utils.disconnectWebsocket(ws); }); describe('GET /asset/:id', () => { @@ -145,7 +141,7 @@ describe('/asset', () => { }); it('should work with a shared link', async () => { - const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { + const sharedLink = await utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, assetIds: [user1Assets[0].id], }); @@ -175,7 +171,7 @@ describe('/asset', () => { ], }); - const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { + const sharedLink = await utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, assetIds: [user1Assets[0].id], }); @@ -247,12 +243,12 @@ describe('/asset', () => { describe('GET /asset/random', () => { beforeAll(async () => { await Promise.all([ - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), ]); }); @@ -335,7 +331,7 @@ describe('/asset', () => { }); it('should favorite an asset', async () => { - const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id); + const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id); expect(before.isFavorite).toBe(false); const { status, body } = await request(app) @@ -347,7 +343,7 @@ describe('/asset', () => { }); it('should archive an asset', async () => { - const before = await apiUtils.getAssetInfo(user1.accessToken, user1Assets[0].id); + const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id); expect(before.isArchived).toBe(false); const { status, body } = await request(app) @@ -475,9 +471,9 @@ describe('/asset', () => { }); it('should move an asset to the trash', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); + const { id: assetId } = await utils.createAsset(admin.accessToken); - const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const before = await utils.getAssetInfo(admin.accessToken, assetId); expect(before.isTrashed).toBe(false); const { status } = await request(app) @@ -486,7 +482,7 @@ describe('/asset', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(true); }); }); @@ -497,7 +493,7 @@ describe('/asset', () => { input: 'formats/jpg/el_torcal_rocks.jpg', expected: { type: AssetTypeEnum.Image, - originalFileName: 'el_torcal_rocks', + originalFileName: 'el_torcal_rocks.jpg', resized: true, exifInfo: { dateTimeOriginal: '2012-08-05T11:39:59.000Z', @@ -521,7 +517,7 @@ describe('/asset', () => { input: 'formats/heic/IMG_2682.heic', expected: { type: AssetTypeEnum.Image, - originalFileName: 'IMG_2682', + originalFileName: 'IMG_2682.heic', resized: true, fileCreatedAt: '2019-03-21T16:04:22.348Z', exifInfo: { @@ -546,7 +542,7 @@ describe('/asset', () => { input: 'formats/png/density_plot.png', expected: { type: AssetTypeEnum.Image, - originalFileName: 'density_plot', + originalFileName: 'density_plot.png', resized: true, exifInfo: { exifImageWidth: 800, @@ -561,7 +557,7 @@ describe('/asset', () => { input: 'formats/raw/Nikon/D80/glarus.nef', expected: { type: AssetTypeEnum.Image, - originalFileName: 'glarus', + originalFileName: 'glarus.nef', resized: true, fileCreatedAt: '2010-07-20T17:27:12.000Z', exifInfo: { @@ -583,7 +579,7 @@ describe('/asset', () => { input: 'formats/raw/Nikon/D700/philadelphia.nef', expected: { type: AssetTypeEnum.Image, - originalFileName: 'philadelphia', + originalFileName: 'philadelphia.nef', resized: true, fileCreatedAt: '2016-09-22T22:10:29.060Z', exifInfo: { @@ -607,15 +603,15 @@ describe('/asset', () => { for (const { input, expected } of tests) { it(`should generate a thumbnail for ${input}`, async () => { const filepath = join(testAssetDir, input); - const { id, duplicate } = await apiUtils.createAsset(admin.accessToken, { + const { id, duplicate } = await utils.createAsset(admin.accessToken, { assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, }); expect(duplicate).toBe(false); - await wsUtils.waitForEvent({ event: 'upload', assetId: id }); + await utils.waitForWebsocketEvent({ event: 'upload', assetId: id }); - const asset = await apiUtils.getAssetInfo(admin.accessToken, id); + const asset = await utils.getAssetInfo(admin.accessToken, id); expect(asset.exifInfo).toBeDefined(); expect(asset.exifInfo).toMatchObject(expected.exifInfo); @@ -625,7 +621,7 @@ describe('/asset', () => { it('should handle a duplicate', async () => { const filepath = 'formats/jpeg/el_torcal_rocks.jpeg'; - const { duplicate } = await apiUtils.createAsset(admin.accessToken, { + const { duplicate } = await utils.createAsset(admin.accessToken, { assetData: { bytes: await readFile(join(testAssetDir, filepath)), filename: basename(filepath), @@ -657,21 +653,21 @@ describe('/asset', () => { for (const { filepath, checksum } of motionTests) { it(`should extract motionphoto video from ${filepath}`, async () => { - const response = await apiUtils.createAsset(admin.accessToken, { + const response = await utils.createAsset(admin.accessToken, { assetData: { bytes: await readFile(join(testAssetDir, filepath)), filename: basename(filepath), }, }); - await wsUtils.waitForEvent({ event: 'upload', assetId: response.id }); + await utils.waitForWebsocketEvent({ event: 'upload', assetId: response.id }); expect(response.duplicate).toBe(false); - const asset = await apiUtils.getAssetInfo(admin.accessToken, response.id); + const asset = await utils.getAssetInfo(admin.accessToken, response.id); expect(asset.livePhotoVideoId).toBeDefined(); - const video = await apiUtils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string); + const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string); expect(video.checksum).toStrictEqual(checksum); }); } @@ -690,7 +686,7 @@ describe('/asset', () => { .get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`) .set('Authorization', `Bearer ${admin.accessToken}`); - await wsUtils.waitForEvent({ + await utils.waitForWebsocketEvent({ event: 'upload', assetId: assetLocation.id, }); @@ -736,11 +732,11 @@ describe('/asset', () => { expect(body).toBeDefined(); expect(type).toBe('image/jpeg'); - const asset = await apiUtils.getAssetInfo(admin.accessToken, assetLocation.id); + const asset = await utils.getAssetInfo(admin.accessToken, assetLocation.id); const original = await readFile(locationAssetFilepath); - const originalChecksum = sha1(original); - const downloadChecksum = sha1(body); + const originalChecksum = utils.sha1(original); + const downloadChecksum = utils.sha1(body); expect(originalChecksum).toBe(downloadChecksum); expect(downloadChecksum).toBe(asset.checksum); diff --git a/e2e/src/api/specs/audit.e2e-spec.ts b/e2e/src/api/specs/audit.e2e-spec.ts index 13c753039..2b551fd24 100644 --- a/e2e/src/api/specs/audit.e2e-spec.ts +++ b/e2e/src/api/specs/audit.e2e-spec.ts @@ -1,24 +1,23 @@ import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk'; -import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils'; +import { asBearerAuth, utils } from 'src/utils'; import { beforeAll, describe, expect, it } from 'vitest'; describe('/audit', () => { let admin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - await fileUtils.reset(); + await utils.resetDatabase(); + await utils.resetFilesystem(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); }); describe('GET :/file-report', () => { it('excludes assets without issues from report', async () => { const [trashedAsset, archivedAsset] = await Promise.all([ - apiUtils.createAsset(admin.accessToken), - apiUtils.createAsset(admin.accessToken), - apiUtils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), + utils.createAsset(admin.accessToken), ]); await Promise.all([ diff --git a/e2e/src/api/specs/auth.e2e-spec.ts b/e2e/src/api/specs/auth.e2e-spec.ts index a58e21571..28445f79d 100644 --- a/e2e/src/api/specs/auth.e2e-spec.ts +++ b/e2e/src/api/specs/auth.e2e-spec.ts @@ -1,19 +1,15 @@ import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk'; import { loginDto, signupDto, uuidDto } from 'src/fixtures'; import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeEach, describe, expect, it } from 'vitest'; const { name, email, password } = signupDto.admin; describe(`/auth/admin-sign-up`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - beforeEach(async () => { - await dbUtils.reset(); + await utils.resetDatabase(); }); describe('POST /auth/admin-sign-up', () => { @@ -84,7 +80,7 @@ describe('/auth/*', () => { let admin: LoginResponseDto; beforeEach(async () => { - await dbUtils.reset(); + await utils.resetDatabase(); await signUpAdmin({ signUpDto: signupDto.admin }); admin = await login({ loginCredentialDto: loginDto.admin }); }); diff --git a/e2e/src/api/specs/download.e2e-spec.ts b/e2e/src/api/specs/download.e2e-spec.ts index cf4aae3e0..ef14778da 100644 --- a/e2e/src/api/specs/download.e2e-spec.ts +++ b/e2e/src/api/specs/download.e2e-spec.ts @@ -1,18 +1,19 @@ import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk'; +import { readFile, writeFile } from 'node:fs/promises'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, tempDir, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; describe('/download', () => { let admin: LoginResponseDto; let asset1: AssetFileUploadResponseDto; + let asset2: AssetFileUploadResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - asset1 = await apiUtils.createAsset(admin.accessToken); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + [asset1, asset2] = await Promise.all([utils.createAsset(admin.accessToken), utils.createAsset(admin.accessToken)]); }); describe('POST /download/info', () => { @@ -40,6 +41,39 @@ describe('/download', () => { }); }); + describe('POST /download/archive', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .post(`/download/archive`) + .send({ assetIds: [asset1.id, asset2.id] }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should download an archive', async () => { + const { status, body } = await request(app) + .post('/download/archive') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ assetIds: [asset1.id, asset2.id] }); + + expect(status).toBe(200); + expect(body instanceof Buffer).toBe(true); + + await writeFile(`${tempDir}/archive.zip`, body); + await utils.unzip(`${tempDir}/archive.zip`, `${tempDir}/archive`); + const files = [ + { filename: 'example.png', id: asset1.id }, + { filename: 'example+1.png', id: asset2.id }, + ]; + for (const { id, filename } of files) { + const bytes = await readFile(`${tempDir}/archive/${filename}`); + const asset = await utils.getAssetInfo(admin.accessToken, id); + expect(utils.sha1(bytes)).toBe(asset.checksum); + } + }); + }); + describe('POST /download/asset/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).post(`/download/asset/${asset1.id}`); diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 8213cc86e..e8f9a46bb 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -1,7 +1,7 @@ import { LibraryResponseDto, LibraryType, LoginResponseDto, getAllLibraries } from '@immich/sdk'; import { userDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils, testAssetDirInternal } from 'src/utils'; +import { app, asBearerAuth, testAssetDirInternal, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -11,11 +11,10 @@ describe('/library', () => { let library: LibraryResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - user = await apiUtils.userSetup(admin.accessToken, userDto.user1); - library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External }); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, userDto.user1); + library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); }); describe('GET /library', () => { @@ -303,7 +302,7 @@ describe('/library', () => { }); it('should get library by id', async () => { - const library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External }); + const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); const { status, body } = await request(app) .get(`/library/${library.id}`) @@ -359,7 +358,7 @@ describe('/library', () => { }); it('should delete an external library', async () => { - const library = await apiUtils.createLibrary(admin.accessToken, { type: LibraryType.External }); + const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External }); const { status, body } = await request(app) .delete(`/library/${library.id}`) @@ -415,14 +414,14 @@ describe('/library', () => { }); it('should pass with no import paths', async () => { - const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { importPaths: [] }); + const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [] }); expect(response.importPaths).toEqual([]); }); it('should fail if path does not exist', async () => { const pathToTest = `${testAssetDirInternal}/does/not/exist`; - const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { + const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [pathToTest], }); @@ -439,7 +438,7 @@ describe('/library', () => { it('should fail if path is a file', async () => { const pathToTest = `${testAssetDirInternal}/albums/nature/el_torcal_rocks.jpg`; - const response = await apiUtils.validateLibrary(admin.accessToken, library.id, { + const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [pathToTest], }); diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts index 1324d3fa7..81c4b452c 100644 --- a/e2e/src/api/specs/oauth.e2e-spec.ts +++ b/e2e/src/api/specs/oauth.e2e-spec.ts @@ -1,16 +1,12 @@ import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, describe, expect, it } from 'vitest'; describe(`/oauth`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - - beforeEach(async () => { - await dbUtils.reset(); - await apiUtils.adminSetup(); + beforeAll(async () => { + await utils.resetDatabase(); + await utils.adminSetup(); }); describe('POST /oauth/authorize', () => { diff --git a/e2e/src/api/specs/partner.e2e-spec.ts b/e2e/src/api/specs/partner.e2e-spec.ts index 2c88391bd..b2fb7f410 100644 --- a/e2e/src/api/specs/partner.e2e-spec.ts +++ b/e2e/src/api/specs/partner.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, createPartner } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -12,15 +12,14 @@ describe('/partner', () => { let user3: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); [user1, user2, user3] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); await Promise.all([ diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts index 77a10b343..54fbfa9be 100644 --- a/e2e/src/api/specs/person.e2e-spec.ts +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -1,39 +1,49 @@ import { LoginResponseDto, PersonResponseDto } from '@immich/sdk'; import { uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -describe('/activity', () => { +const invalidBirthday = [ + { birthDate: 'false', response: 'birthDate must be a date string' }, + { birthDate: '123567', response: 'birthDate must be a date string' }, + { birthDate: 123_567, response: 'birthDate must be a date string' }, + { birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] }, +]; + +describe('/person', () => { let admin: LoginResponseDto; let visiblePerson: PersonResponseDto; let hiddenPerson: PersonResponseDto; + let multipleAssetsPerson: PersonResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - }); + await utils.resetDatabase(); + admin = await utils.adminSetup(); - beforeEach(async () => { - await dbUtils.reset(['person']); - - [visiblePerson, hiddenPerson] = await Promise.all([ - apiUtils.createPerson(admin.accessToken, { + [visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([ + utils.createPerson(admin.accessToken, { name: 'visible_person', }), - apiUtils.createPerson(admin.accessToken, { + utils.createPerson(admin.accessToken, { name: 'hidden_person', isHidden: true, }), + utils.createPerson(admin.accessToken, { + name: 'multiple_assets_person', + }), ]); - const asset = await apiUtils.createAsset(admin.accessToken); + const asset1 = await utils.createAsset(admin.accessToken); + const asset2 = await utils.createAsset(admin.accessToken); await Promise.all([ - dbUtils.createFace({ assetId: asset.id, personId: visiblePerson.id }), - dbUtils.createFace({ assetId: asset.id, personId: hiddenPerson.id }), + utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }), + utils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }), + utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }), + utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }), ]); }); @@ -55,9 +65,10 @@ describe('/activity', () => { expect(status).toBe(200); expect(body).toEqual({ - total: 2, + total: 3, hidden: 1, people: [ + expect.objectContaining({ name: 'multiple_assets_person' }), expect.objectContaining({ name: 'visible_person' }), expect.objectContaining({ name: 'hidden_person' }), ], @@ -69,9 +80,12 @@ describe('/activity', () => { expect(status).toBe(200); expect(body).toEqual({ - total: 2, + total: 3, hidden: 1, - people: [expect.objectContaining({ name: 'visible_person' })], + people: [ + expect.objectContaining({ name: 'multiple_assets_person' }), + expect.objectContaining({ name: 'visible_person' }), + ], }); }); }); @@ -103,6 +117,68 @@ describe('/activity', () => { }); }); + describe('GET /person/:id/statistics', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should throw error if person with id does not exist', async () => { + const { status, body } = await request(app) + .get(`/person/${uuidDto.notFound}/statistics`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should return the correct number of assets', async () => { + const { status, body } = await request(app) + .get(`/person/${multipleAssetsPerson.id}/statistics`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ assets: 2 })); + }); + }); + + describe('POST /person', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post(`/person`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + for (const { birthDate, response } of invalidBirthday) { + it(`should not accept an invalid birth date [${birthDate}]`, async () => { + const { status, body } = await request(app) + .post(`/person`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(response)); + }); + } + + it('should create a person', async () => { + const { status, body } = await request(app) + .post(`/person`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ + name: 'New Person', + birthDate: '1990-01-01T05:00:00.000Z', + }); + expect(status).toBe(201); + expect(body).toMatchObject({ + id: expect.any(String), + name: 'New Person', + birthDate: '1990-01-01T05:00:00.000Z', + }); + }); + }); + describe('PUT /person/:id', () => { it('should require authentication', async () => { const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`); @@ -125,24 +201,16 @@ describe('/activity', () => { }); } - it('should not accept invalid birth dates', async () => { - for (const { birthDate, response } of [ - { birthDate: false, response: 'Not found or no person.write access' }, - { birthDate: 'false', response: ['birthDate must be a Date instance'] }, - { - birthDate: '123567', - response: 'Not found or no person.write access', - }, - { birthDate: 123_567, response: 'Not found or no person.write access' }, - ]) { + for (const { birthDate, response } of invalidBirthday) { + it(`should not accept an invalid birth date [${birthDate}]`, async () => { const { status, body } = await request(app) - .put(`/person/${uuidDto.notFound}`) + .put(`/person/${visiblePerson.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ birthDate }); expect(status).toBe(400); expect(body).toEqual(errorDto.badRequest(response)); - } - }); + }); + } it('should update a date of birth', async () => { const { status, body } = await request(app) @@ -154,15 +222,8 @@ describe('/activity', () => { }); it('should clear a date of birth', async () => { - // TODO ironically this uses the update endpoint to create the person - const person = await apiUtils.createPerson(admin.accessToken, { - birthDate: new Date('1990-01-01').toISOString(), - }); - - expect(person.birthDate).toBeDefined(); - const { status, body } = await request(app) - .put(`/person/${person.id}`) + .put(`/person/${visiblePerson.id}`) .set('Authorization', `Bearer ${admin.accessToken}`) .send({ birthDate: null }); expect(status).toBe(200); diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts new file mode 100644 index 000000000..de7d9ef4c --- /dev/null +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -0,0 +1,224 @@ +import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { Socket } from 'socket.io-client'; +import { errorDto } from 'src/responses'; +import { app, testAssetDir, utils } from 'src/utils'; +import request from 'supertest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const albums = { total: 0, count: 0, items: [], facets: [] }; + +describe('/search', () => { + let admin: LoginResponseDto; + let assetFalcon: AssetFileUploadResponseDto; + let assetDenali: AssetFileUploadResponseDto; + let websocket: Socket; + + beforeAll(async () => { + await utils.resetDatabase(); + admin = await utils.adminSetup(); + websocket = await utils.connectWebsocket(admin.accessToken); + + const files: string[] = [ + '/albums/nature/prairie_falcon.jpg', + '/formats/webp/denali.webp', + '/formats/raw/Nikon/D700/philadelphia.nef', + '/albums/nature/orychophragmus_violaceus.jpg', + '/albums/nature/notocactus_minimus.jpg', + '/albums/nature/silver_fir.jpg', + '/albums/nature/tanners_ridge.jpg', + '/albums/nature/cyclamen_persicum.jpg', + '/albums/nature/polemonium_reptans.jpg', + '/albums/nature/wood_anemones.jpg', + '/formats/heic/IMG_2682.heic', + '/formats/jpg/el_torcal_rocks.jpg', + '/formats/png/density_plot.png', + '/formats/motionphoto/Samsung One UI 6.jpg', + '/formats/motionphoto/Samsung One UI 6.heic', + '/formats/motionphoto/Samsung One UI 5.jpg', + '/formats/raw/Nikon/D80/glarus.nef', + '/metadata/gps-position/thompson-springs.jpg', + ]; + const assets: AssetFileUploadResponseDto[] = []; + for (const filename of files) { + const bytes = await readFile(join(testAssetDir, filename)); + assets.push( + await utils.createAsset(admin.accessToken, { + deviceAssetId: `test-${filename}`, + assetData: { bytes, filename }, + }), + ); + } + + for (const asset of assets) { + await utils.waitForWebsocketEvent({ event: 'upload', assetId: asset.id }); + } + + [assetFalcon, assetDenali] = assets; + }); + + afterAll(async () => { + await utils.disconnectWebsocket(websocket); + }); + + describe('POST /search/metadata', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/search/metadata'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should search by camera make', async () => { + const { status, body } = await request(app) + .post('/search/metadata') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ make: 'Canon' }); + expect(status).toBe(200); + expect(body).toEqual({ + albums, + assets: { + count: 2, + items: expect.arrayContaining([ + expect.objectContaining({ id: assetDenali.id }), + expect.objectContaining({ id: assetFalcon.id }), + ]), + facets: [], + nextPage: null, + total: 2, + }, + }); + }); + + it('should search by camera model', async () => { + const { status, body } = await request(app) + .post('/search/metadata') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ model: 'Canon EOS 7D' }); + expect(status).toBe(200); + expect(body).toEqual({ + albums, + assets: { + count: 1, + items: [expect.objectContaining({ id: assetDenali.id })], + facets: [], + nextPage: null, + total: 1, + }, + }); + }); + }); + + describe('POST /search/smart', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/search/smart'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + }); + + describe('GET /search/explore', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/search/explore'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get explore data', async () => { + const { status, body } = await request(app) + .get('/search/explore') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual([ + { fieldName: 'exifInfo.city', items: [] }, + { fieldName: 'smartInfo.tags', items: [] }, + ]); + }); + }); + + describe('GET /search/places', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/search/places'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get places', async () => { + const { status, body } = await request(app) + .get('/search/places?name=Paris') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(200); + expect(Array.isArray(body)).toBe(true); + expect(body.length).toBeGreaterThan(10); + }); + }); + + describe('GET /search/suggestions', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/search/suggestions'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should get suggestions for country', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=country') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual(['United States of America']); + expect(status).toBe(200); + }); + + it('should get suggestions for state', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=state') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']); + expect(status).toBe(200); + }); + + it('should get suggestions for city', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=city') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual(['Palisade', 'Ralston']); + expect(status).toBe(200); + }); + + it('should get suggestions for camera make', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=camera-make') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Apple', + 'Canon', + 'FUJIFILM', + 'NIKON CORPORATION', + 'PENTAX Corporation', + 'samsung', + 'SONY', + ]); + expect(status).toBe(200); + }); + + it('should get suggestions for camera model', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=camera-model') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Canon EOS 7D', + 'Canon EOS R5', + 'DSLR-A550', + 'FinePix S3Pro', + 'iPhone 7', + 'NIKON D700', + 'NIKON D750', + 'NIKON D80', + 'PENTAX K10D', + 'SM-F711N', + 'SM-S906U', + 'SM-T970', + ]); + expect(status).toBe(200); + }); + }); +}); diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts index 7c8c45709..5cfd6a8b9 100644 --- a/e2e/src/api/specs/server-info.e2e-spec.ts +++ b/e2e/src/api/specs/server-info.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, getServerConfig } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,10 +10,9 @@ describe('/server-info', () => { let nonAdmin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); - nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); }); describe('GET /server-info', () => { @@ -88,6 +87,7 @@ describe('/server-info', () => { loginPageMessage: '', oauthButtonText: 'Login with OAuth', trashDays: 30, + userDeleteDelay: 7, isInitialized: true, externalDomain: '', isOnboarded: false, diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index f2e5b0186..8b854eda0 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -9,7 +9,7 @@ import { } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -30,20 +30,16 @@ describe('/shared-link', () => { let linkWithoutMetadata: SharedLinkResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); + await utils.resetDatabase(); - admin = await apiUtils.adminSetup(); + admin = await utils.adminSetup(); [user1, user2] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), ]); - [asset1, asset2] = await Promise.all([ - apiUtils.createAsset(user1.accessToken), - apiUtils.createAsset(user1.accessToken), - ]); + [asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]); [album, deletedAlbum, metadataAlbum] = await Promise.all([ createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }), @@ -61,29 +57,29 @@ describe('/shared-link', () => { [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = await Promise.all([ - apiUtils.createSharedLink(user2.accessToken, { + utils.createSharedLink(user2.accessToken, { type: SharedLinkType.Album, albumId: deletedAlbum.id, }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: album.id, }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, assetIds: [asset1.id], }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: album.id, password: 'foo', }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: true, }), - apiUtils.createSharedLink(user1.accessToken, { + utils.createSharedLink(user1.accessToken, { type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: false, @@ -194,7 +190,7 @@ describe('/shared-link', () => { expect(body.assets).toHaveLength(1); expect(body.assets[0]).toEqual( expect.objectContaining({ - originalFileName: 'example', + originalFileName: 'example.png', localDateTime: expect.any(String), fileCreatedAt: expect.any(String), exifInfo: expect.any(Object), diff --git a/e2e/src/api/specs/system-config.e2e-spec.ts b/e2e/src/api/specs/system-config.e2e-spec.ts index 6d8880d3f..c223df487 100644 --- a/e2e/src/api/specs/system-config.e2e-spec.ts +++ b/e2e/src/api/specs/system-config.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto } from '@immich/sdk'; import { createUserDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { app, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -10,10 +10,9 @@ describe('/system-config', () => { let nonAdmin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); }); describe('GET /system-config/map/style.json', () => { diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index 60ed75f11..3e6c2f1fc 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, getAllAssets } from '@immich/sdk'; import { Socket } from 'socket.io-client'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils, wsUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -10,14 +10,13 @@ describe('/trash', () => { let ws: Socket; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); - ws = await wsUtils.connect(admin.accessToken); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); + ws = await utils.connectWebsocket(admin.accessToken); }); afterAll(() => { - wsUtils.disconnect(ws); + utils.disconnectWebsocket(ws); }); describe('POST /trash/empty', () => { @@ -29,8 +28,8 @@ describe('/trash', () => { }); it('should empty the trash', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); - await apiUtils.deleteAssets(admin.accessToken, [assetId]); + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.deleteAssets(admin.accessToken, [assetId]); const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); @@ -39,7 +38,7 @@ describe('/trash', () => { const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - await wsUtils.waitForEvent({ event: 'delete', assetId }); + await utils.waitForWebsocketEvent({ event: 'delete', assetId }); const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); expect(after.length).toBe(0); @@ -55,16 +54,16 @@ describe('/trash', () => { }); it('should restore all trashed assets', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); - await apiUtils.deleteAssets(admin.accessToken, [assetId]); + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.deleteAssets(admin.accessToken, [assetId]); - const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const before = await utils.getAssetInfo(admin.accessToken, assetId); expect(before.isTrashed).toBe(true); const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(false); }); }); @@ -78,10 +77,10 @@ describe('/trash', () => { }); it('should restore a trashed asset by id', async () => { - const { id: assetId } = await apiUtils.createAsset(admin.accessToken); - await apiUtils.deleteAssets(admin.accessToken, [assetId]); + const { id: assetId } = await utils.createAsset(admin.accessToken); + await utils.deleteAssets(admin.accessToken, [assetId]); - const before = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const before = await utils.getAssetInfo(admin.accessToken, assetId); expect(before.isTrashed).toBe(true); const { status } = await request(app) @@ -90,7 +89,7 @@ describe('/trash', () => { .send({ ids: [assetId] }); expect(status).toBe(204); - const after = await apiUtils.getAssetInfo(admin.accessToken, assetId); + const after = await utils.getAssetInfo(admin.accessToken, assetId); expect(after.isTrashed).toBe(false); }); }); diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index e47e1d531..d448a605c 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -1,7 +1,7 @@ import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk'; import { createUserDto, userDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; +import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; @@ -12,14 +12,13 @@ describe('/server-info', () => { let nonAdmin: LoginResponseDto; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup({ onboarding: false }); + await utils.resetDatabase(); + admin = await utils.adminSetup({ onboarding: false }); [deletedUser, nonAdmin, userToDelete] = await Promise.all([ - apiUtils.userSetup(admin.accessToken, createUserDto.user1), - apiUtils.userSetup(admin.accessToken, createUserDto.user2), - apiUtils.userSetup(admin.accessToken, createUserDto.user3), + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + utils.userSetup(admin.accessToken, createUserDto.user3), ]); await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) }); diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index e3140ecea..61702769c 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -1,14 +1,10 @@ import { stat } from 'node:fs/promises'; -import { apiUtils, app, dbUtils, immichCli } from 'src/utils'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { app, immichCli, utils } from 'src/utils'; +import { beforeEach, describe, expect, it } from 'vitest'; describe(`immich login-key`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - beforeEach(async () => { - await dbUtils.reset(); + await utils.resetDatabase(); }); it('should require a url', async () => { @@ -29,12 +25,12 @@ describe(`immich login-key`, () => { expect(exitCode).toBe(1); }); - it('should login', async () => { - const admin = await apiUtils.adminSetup(); - const key = await apiUtils.createApiKey(admin.accessToken); + it('should login and save auth.yml with 600', async () => { + const admin = await utils.adminSetup(); + const key = await utils.createApiKey(admin.accessToken); const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]); expect(stdout.split('\n')).toEqual([ - 'Logging in...', + 'Logging in to http://127.0.0.1:2283/api', 'Logged in as admin@immich.cloud', 'Wrote auth info to /tmp/immich/auth.yml', ]); @@ -45,4 +41,18 @@ describe(`immich login-key`, () => { const mode = (stats.mode & 0o777).toString(8); expect(mode).toEqual('600'); }); + + it('should login without /api in the url', async () => { + const admin = await utils.adminSetup(); + const key = await utils.createApiKey(admin.accessToken); + const { stdout, stderr, exitCode } = await immichCli(['login-key', app.replaceAll('/api', ''), `${key.secret}`]); + expect(stdout.split('\n')).toEqual([ + 'Logging in to http://127.0.0.1:2283', + 'Discovered API at http://127.0.0.1:2283/api', + 'Logged in as admin@immich.cloud', + 'Wrote auth info to /tmp/immich/auth.yml', + ]); + expect(stderr).toBe(''); + expect(exitCode).toBe(0); + }); }); diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts index 038a2c2ca..6efe002b8 100644 --- a/e2e/src/cli/specs/server-info.e2e-spec.ts +++ b/e2e/src/cli/specs/server-info.e2e-spec.ts @@ -1,11 +1,10 @@ -import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils'; +import { immichCli, utils } from 'src/utils'; import { beforeAll, describe, expect, it } from 'vitest'; describe(`immich server-info`, () => { beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - await cliUtils.login(); + await utils.resetDatabase(); + await utils.cliLogin(); }); it('should return the server info', async () => { diff --git a/e2e/src/cli/specs/upload.e2e-spec.ts b/e2e/src/cli/specs/upload.e2e-spec.ts index bda625241..27362ef23 100644 --- a/e2e/src/cli/specs/upload.e2e-spec.ts +++ b/e2e/src/cli/specs/upload.e2e-spec.ts @@ -1,19 +1,18 @@ import { getAllAlbums, getAllAssets } from '@immich/sdk'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; -import { apiUtils, asKeyAuth, cliUtils, dbUtils, immichCli, testAssetDir } from 'src/utils'; +import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; describe(`immich upload`, () => { let key: string; beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - key = await cliUtils.login(); + await utils.resetDatabase(); + key = await utils.cliLogin(); }); beforeEach(async () => { - await dbUtils.reset(['assets', 'albums']); + await utils.resetDatabase(['assets', 'albums']); }); describe('immich upload --recursive', () => { diff --git a/e2e/src/cli/specs/version.e2e-spec.ts b/e2e/src/cli/specs/version.e2e-spec.ts index e94ccf214..56a0d8b0b 100644 --- a/e2e/src/cli/specs/version.e2e-spec.ts +++ b/e2e/src/cli/specs/version.e2e-spec.ts @@ -1,14 +1,10 @@ import { readFileSync } from 'node:fs'; -import { apiUtils, immichCli } from 'src/utils'; -import { beforeAll, describe, expect, it } from 'vitest'; +import { immichCli } from 'src/utils'; +import { describe, expect, it } from 'vitest'; const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8')); describe(`immich --version`, () => { - beforeAll(() => { - apiUtils.setup(); - }); - describe('immich --version', () => { it('should print the cli version', async () => { const { stdout, stderr, exitCode } = await immichCli(['--version']); diff --git a/e2e/src/setup.ts b/e2e/src/setup.ts index e0ff44356..a3d96ac17 100644 --- a/e2e/src/setup.ts +++ b/e2e/src/setup.ts @@ -1,8 +1,16 @@ import { exec, spawn } from 'node:child_process'; +import { setTimeout } from 'node:timers'; export default async () => { let _resolve: () => unknown; - const ready = new Promise((resolve) => (_resolve = resolve)); + let _reject: (error: Error) => unknown; + + const ready = new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }); + + const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000); const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' }); @@ -17,6 +25,7 @@ export default async () => { child.stderr.on('data', (data) => console.log(data.toString())); await ready; + clearTimeout(timeout); return async () => { await new Promise((resolve) => exec('docker compose down', () => resolve())); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 34f25e396..d62497b8e 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -5,7 +5,7 @@ import { CreateAssetDto, CreateLibraryDto, CreateUserDto, - PersonUpdateDto, + PersonCreateDto, SharedLinkCreateDto, ValidateLibraryDto, createAlbum, @@ -20,12 +20,12 @@ import { login, setAdminOnboarding, signUpAdmin, - updatePerson, validate, } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; import { exec, spawn } from 'node:child_process'; -import { access } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import { existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import path from 'node:path'; import { promisify } from 'node:util'; @@ -35,75 +35,71 @@ import { loginDto, signupDto } from 'src/fixtures'; import { makeRandomImage } from 'src/generators'; import request from 'supertest'; -const execPromise = promisify(exec); +type CliResponse = { stdout: string; stderr: string; exitCode: number | null }; +type EventType = 'upload' | 'delete'; +type WaitOptions = { event: EventType; assetId: string; timeout?: number }; +type AdminSetupOptions = { onboarding?: boolean }; +type AssetData = { bytes?: Buffer; filename: string }; -export const app = 'http://127.0.0.1:2283/api'; - -const directoryExists = (directory: string) => - access(directory) - .then(() => true) - .catch(() => false); +const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich'; +const baseUrl = 'http://127.0.0.1:2283'; +export const app = `${baseUrl}/api`; // TODO move test assets into e2e/assets export const testAssetDir = path.resolve(`./../server/test/assets/`); export const testAssetDirInternal = '/data/assets'; export const tempDir = tmpdir(); - -const serverContainerName = 'immich-e2e-server'; -const mediaDir = '/usr/src/app/upload'; -const dirs = [ - `"${mediaDir}/thumbs"`, - `"${mediaDir}/upload"`, - `"${mediaDir}/library"`, - `"${mediaDir}/encoded-video"`, -].join(' '); - -if (!(await directoryExists(`${testAssetDir}/albums`))) { - throw new Error( - `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`, - ); -} - -export const asBearerAuth = (accessToken: string) => ({ - Authorization: `Bearer ${accessToken}`, -}); - +export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` }); export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); +export const immichCli = async (args: string[]) => { + let _resolve: (value: CliResponse) => void; + const deferred = new Promise((resolve) => (_resolve = resolve)); + const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]; + const child = spawn('node', _args, { + stdio: 'pipe', + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (data) => (stdout += data.toString())); + child.stderr.on('data', (data) => (stderr += data.toString())); + child.on('exit', (exitCode) => { + _resolve({ + stdout: stdout.trim(), + stderr: stderr.trim(), + exitCode, + }); + }); + + return deferred; +}; let client: pg.Client | null = null; -export const fileUtils = { - reset: async () => { - await execPromise(`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`); - }, +const events: Record> = { + upload: new Set(), + delete: new Set(), }; -export const dbUtils = { - createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => { - if (!client) { - return; - } +const callbacks: Record void> = {}; - const vector = Array.from({ length: 512 }, Math.random); - const embedding = `[${vector.join(',')}]`; +const execPromise = promisify(exec); - await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [ - assetId, - personId, - embedding, - ]); - }, - setPersonThumbnail: async (personId: string) => { - if (!client) { - return; - } +const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { + events[event].add(assetId); + const callback = callbacks[assetId]; + if (callback) { + callback(); + delete callbacks[assetId]; + } +}; - await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]); - }, - reset: async (tables?: string[]) => { +export const utils = { + resetDatabase: async (tables?: string[]) => { try { if (!client) { - client = new pg.Client('postgres://postgres:postgres@127.0.0.1:5433/immich'); + client = new pg.Client(dbUrl); await client.connect(); } @@ -129,83 +125,27 @@ export const dbUtils = { throw error; } }, - teardown: async () => { - try { - if (client) { - await client.end(); - client = null; - } - } catch (error) { - console.error('Failed to teardown database', error); - throw error; - } + + resetFilesystem: async () => { + const mediaInternal = '/usr/src/app/upload'; + const dirs = [ + `"${mediaInternal}/thumbs"`, + `"${mediaInternal}/upload"`, + `"${mediaInternal}/library"`, + `"${mediaInternal}/encoded-video"`, + ].join(' '); + + await execPromise(`docker exec -i "immich-e2e-server" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`); }, -}; -export interface CliResponse { - stdout: string; - stderr: string; - exitCode: number | null; -} -export const immichCli = async (args: string[]) => { - let _resolve: (value: CliResponse) => void; - const deferred = new Promise((resolve) => (_resolve = resolve)); - const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args]; - const child = spawn('node', _args, { - stdio: 'pipe', - }); + unzip: async (input: string, output: string) => { + await execPromise(`unzip -o -d "${output}" "${input}"`); + }, - let stdout = ''; - let stderr = ''; + sha1: (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'), - child.stdout.on('data', (data) => (stdout += data.toString())); - child.stderr.on('data', (data) => (stderr += data.toString())); - child.on('exit', (exitCode) => { - _resolve({ - stdout: stdout.trim(), - stderr: stderr.trim(), - exitCode, - }); - }); - - return deferred; -}; - -export interface AdminSetupOptions { - onboarding?: boolean; -} - -export enum SocketEvent { - UPLOAD = 'upload', - DELETE = 'delete', -} - -export type EventType = 'upload' | 'delete'; -export interface WaitOptions { - event: EventType; - assetId: string; - timeout?: number; -} - -const events: Record> = { - upload: new Set(), - delete: new Set(), -}; - -const callbacks: Record void> = {}; - -const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { - events[event].add(assetId); - const callback = callbacks[assetId]; - if (callback) { - callback(); - delete callbacks[assetId]; - } -}; - -export const wsUtils = { - connect: async (accessToken: string) => { - const websocket = io('http://127.0.0.1:2283', { + connectWebsocket: async (accessToken: string) => { + const websocket = io(baseUrl, { path: '/api/socket.io', transports: ['websocket'], extraHeaders: { Authorization: `Bearer ${accessToken}` }, @@ -221,7 +161,8 @@ export const wsUtils = { .connect(); }); }, - disconnect: (ws: Socket) => { + + disconnectWebsocket: (ws: Socket) => { if (ws?.connected) { ws.disconnect(); } @@ -230,14 +171,16 @@ export const wsUtils = { set.clear(); } }, - waitForEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise => { + + waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise => { + console.log(`Waiting for ${event} [${assetId}]`); const set = events[event]; if (set.has(assetId)) { return; } return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 5000); + const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000); callbacks[assetId] = () => { clearTimeout(timeout); @@ -245,12 +188,8 @@ export const wsUtils = { }; }); }, -}; -type AssetData = { bytes?: Buffer; filename: string }; - -export const apiUtils = { - setup: () => { + setApiEndpoint: () => { defaults.baseUrl = app; }, @@ -264,17 +203,21 @@ export const apiUtils = { } return response; }, + userSetup: async (accessToken: string, dto: CreateUserDto) => { await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) }); return login({ loginCredentialDto: { email: dto.email, password: dto.password }, }); }, + createApiKey: (accessToken: string) => { return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) }); }, + createAlbum: (accessToken: string, dto: CreateAlbumDto) => createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }), + createAsset: async ( accessToken: string, dto?: Partial> & { assetData?: AssetData }, @@ -290,6 +233,10 @@ export const apiUtils = { const assetData = dto?.assetData?.bytes || makeRandomImage(); const filename = dto?.assetData?.filename || 'example.png'; + if (dto?.assetData?.bytes) { + console.log(`Uploading ${filename}`); + } + const builder = request(app) .post(`/asset/upload`) .attach('assetData', assetData, filename) @@ -303,38 +250,51 @@ export const apiUtils = { return body as AssetFileUploadResponseDto; }, + getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), + deleteAssets: (accessToken: string, ids: string[]) => deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }), - createPerson: async (accessToken: string, dto?: PersonUpdateDto) => { - // TODO fix createPerson to accept a body - const person = await createPerson({ headers: asBearerAuth(accessToken) }); - await dbUtils.setPersonThumbnail(person.id); - if (!dto) { - return person; + createPerson: async (accessToken: string, dto?: PersonCreateDto) => { + const person = await createPerson({ personCreateDto: dto || {} }, { headers: asBearerAuth(accessToken) }); + await utils.setPersonThumbnail(person.id); + + return person; + }, + + createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => { + if (!client) { + return; } - return updatePerson({ id: person.id, personUpdateDto: dto }, { headers: asBearerAuth(accessToken) }); + const vector = Array.from({ length: 512 }, Math.random); + const embedding = `[${vector.join(',')}]`; + + await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [ + assetId, + personId, + embedding, + ]); }, + + setPersonThumbnail: async (personId: string) => { + if (!client) { + return; + } + + await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]); + }, + createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }), + createLibrary: (accessToken: string, dto: CreateLibraryDto) => createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), + validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) => validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), -}; -export const cliUtils = { - login: async () => { - const admin = await apiUtils.adminSetup(); - const key = await apiUtils.createApiKey(admin.accessToken); - await immichCli(['login-key', app, `${key.secret}`]); - return key.secret; - }, -}; - -export const webUtils = { setAuthCookies: async (context: BrowserContext, accessToken: string) => await context.addCookies([ { @@ -368,4 +328,19 @@ export const webUtils = { sameSite: 'Lax', }, ]), + + cliLogin: async () => { + const admin = await utils.adminSetup(); + const key = await utils.createApiKey(admin.accessToken); + await immichCli(['login-key', app, `${key.secret}`]); + return key.secret; + }, }; + +utils.setApiEndpoint(); + +if (!existsSync(`${testAssetDir}/albums`)) { + throw new Error( + `Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`, + ); +} diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index 23210205a..73d62f1b1 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -1,17 +1,13 @@ import { expect, test } from '@playwright/test'; -import { apiUtils, dbUtils, webUtils } from 'src/utils'; +import { utils } from 'src/utils'; test.describe('Registration', () => { test.beforeAll(() => { - apiUtils.setup(); + utils.setApiEndpoint(); }); test.beforeEach(async () => { - await dbUtils.reset(); - }); - - test.afterAll(async () => { - await dbUtils.teardown(); + await utils.resetDatabase(); }); test('admin registration', async ({ page }) => { @@ -45,8 +41,8 @@ test.describe('Registration', () => { }); test('user registration', async ({ context, page }) => { - const admin = await apiUtils.adminSetup(); - await webUtils.setAuthCookies(context, admin.accessToken); + const admin = await utils.adminSetup(); + await utils.setAuthCookies(context, admin.accessToken); // create user await page.goto('/admin/user-management'); diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index 6b2dbad95..3540ed72e 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -7,7 +7,7 @@ import { createAlbum, } from '@immich/sdk'; import { test } from '@playwright/test'; -import { apiUtils, asBearerAuth, dbUtils } from 'src/utils'; +import { asBearerAuth, utils } from 'src/utils'; test.describe('Shared Links', () => { let admin: LoginResponseDto; @@ -17,10 +17,10 @@ test.describe('Shared Links', () => { let sharedLinkPassword: SharedLinkResponseDto; test.beforeAll(async () => { - apiUtils.setup(); - await dbUtils.reset(); - admin = await apiUtils.adminSetup(); - asset = await apiUtils.createAsset(admin.accessToken); + utils.setApiEndpoint(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + asset = await utils.createAsset(admin.accessToken); album = await createAlbum( { createAlbumDto: { @@ -30,21 +30,17 @@ test.describe('Shared Links', () => { }, { headers: asBearerAuth(admin.accessToken) }, ); - sharedLink = await apiUtils.createSharedLink(admin.accessToken, { + sharedLink = await utils.createSharedLink(admin.accessToken, { type: SharedLinkType.Album, albumId: album.id, }); - sharedLinkPassword = await apiUtils.createSharedLink(admin.accessToken, { + sharedLinkPassword = await utils.createSharedLink(admin.accessToken, { type: SharedLinkType.Album, albumId: album.id, password: 'test-password', }); }); - test.afterAll(async () => { - await dbUtils.teardown(); - }); - test('download from a shared link', async ({ page }) => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index b8cc098dd..d7dcde4c3 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ test: { include: ['src/{api,cli}/specs/*.e2e-spec.ts'], globalSetup, + testTimeout: 10_000, poolOptions: { threads: { singleThread: true, diff --git a/machine-learning/.gitignore b/machine-learning/.gitignore index e31c7773e..a259b9f5d 100644 --- a/machine-learning/.gitignore +++ b/machine-learning/.gitignore @@ -167,6 +167,10 @@ cython_debug/ # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +# VS Code +.vscode *.onnx -*.zip \ No newline at end of file +*.zip + +core \ No newline at end of file diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index c0a1a2030..561d2af41 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -39,7 +39,7 @@ FROM python:3.11-slim-bookworm@sha256:ce81dc539f0aedc9114cae640f8352fad83d37461c FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino USER root -FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:8b51b1fe922964d73c482a267b5b519e990d90bf744ec7a40419923737caff6d as prod-cuda +FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda 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/config.py b/machine-learning/app/config.py index c48b3278d..a911659db 100644 --- a/machine-learning/app/config.py +++ b/machine-learning/app/config.py @@ -6,7 +6,7 @@ from pathlib import Path from socket import socket from gunicorn.arbiter import Arbiter -from pydantic import BaseSettings +from pydantic import BaseModel, BaseSettings from rich.console import Console from rich.logging import RichHandler from uvicorn import Server @@ -15,6 +15,11 @@ from uvicorn.workers import UvicornWorker from .schemas import ModelType +class PreloadModelData(BaseModel): + clip: str | None + facial_recognition: str | None + + class Settings(BaseSettings): cache_folder: str = "/cache" model_ttl: int = 300 @@ -27,10 +32,12 @@ class Settings(BaseSettings): model_inter_op_threads: int = 0 model_intra_op_threads: int = 0 ann: bool = True + preload: PreloadModelData | None = None class Config: env_prefix = "MACHINE_LEARNING_" case_sensitive = False + env_nested_delimiter = "__" class LogSettings(BaseSettings): diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index bde40f36e..277ad7689 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -17,7 +17,7 @@ from starlette.formparsers import MultiPartParser from app.models.base import InferenceModel -from .config import log, settings +from .config import PreloadModelData, log, settings from .models.cache import ModelCache from .schemas import ( MessageResponse, @@ -27,7 +27,7 @@ from .schemas import ( MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger -model_cache = ModelCache(ttl=settings.model_ttl, revalidate=settings.model_ttl > 0) +model_cache = ModelCache(revalidate=settings.model_ttl > 0) thread_pool: ThreadPoolExecutor | None = None lock = threading.Lock() active_requests = 0 @@ -51,6 +51,8 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: log.info(f"Initialized request thread pool with {settings.request_threads} threads.") if settings.model_ttl > 0 and settings.model_ttl_poll_s > 0: asyncio.ensure_future(idle_shutdown_task()) + if settings.preload is not None: + await preload_models(settings.preload) yield finally: log.handlers.clear() @@ -61,6 +63,14 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]: gc.collect() +async def preload_models(preload_models: PreloadModelData) -> None: + log.info(f"Preloading models: {preload_models}") + if preload_models.clip is not None: + await load(await model_cache.get(preload_models.clip, ModelType.CLIP)) + if preload_models.facial_recognition is not None: + await load(await model_cache.get(preload_models.facial_recognition, ModelType.FACIAL_RECOGNITION)) + + def update_state() -> Iterator[None]: global active_requests, last_called active_requests += 1 @@ -103,7 +113,7 @@ async def predict( except orjson.JSONDecodeError: raise HTTPException(400, f"Invalid options JSON: {options}") - model = await load(await model_cache.get(model_name, model_type, **kwargs)) + model = await load(await model_cache.get(model_name, model_type, ttl=settings.model_ttl, **kwargs)) model.configure(**kwargs) outputs = await run(model.predict, inputs) return ORJSONResponse(outputs) diff --git a/machine-learning/app/models/cache.py b/machine-learning/app/models/cache.py index 62afd05a0..781a9caea 100644 --- a/machine-learning/app/models/cache.py +++ b/machine-learning/app/models/cache.py @@ -2,7 +2,7 @@ from typing import Any from aiocache.backends.memory import SimpleMemoryCache from aiocache.lock import OptimisticLock -from aiocache.plugins import BasePlugin, TimingPlugin +from aiocache.plugins import TimingPlugin from app.models import from_model_type @@ -15,28 +15,25 @@ class ModelCache: def __init__( self, - ttl: float | None = None, revalidate: bool = False, timeout: int | None = None, profiling: bool = False, ) -> None: """ Args: - ttl: Unloads model after this duration. Disabled if None. Defaults to None. revalidate: Resets TTL on cache hit. Useful to keep models in memory while active. Defaults to False. timeout: Maximum allowed time for model to load. Disabled if None. Defaults to None. profiling: Collects metrics for cache operations, adding slight overhead. Defaults to False. """ - self.ttl = ttl plugins = [] - if revalidate: - plugins.append(RevalidationPlugin()) if profiling: plugins.append(TimingPlugin()) - self.cache = SimpleMemoryCache(ttl=ttl, timeout=timeout, plugins=plugins, namespace=None) + self.revalidate_enable = revalidate + + self.cache = SimpleMemoryCache(timeout=timeout, plugins=plugins, namespace=None) async def get(self, model_name: str, model_type: ModelType, **model_kwargs: Any) -> InferenceModel: """ @@ -49,11 +46,14 @@ class ModelCache: """ key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}" + async with OptimisticLock(self.cache, key) as lock: model: InferenceModel | None = await self.cache.get(key) if model is None: model = from_model_type(model_type, model_name, **model_kwargs) - await lock.cas(model, ttl=self.ttl) + await lock.cas(model, ttl=model_kwargs.get("ttl", None)) + elif self.revalidate_enable: + await self.revalidate(key, model_kwargs.get("ttl", None)) return model async def get_profiling(self) -> dict[str, float] | None: @@ -62,21 +62,6 @@ class ModelCache: return self.cache.profiling - -class RevalidationPlugin(BasePlugin): # type: ignore[misc] - """Revalidates cache item's TTL after cache hit.""" - - async def post_get( - self, - client: SimpleMemoryCache, - key: str, - ret: Any | None = None, - namespace: str | None = None, - **kwargs: Any, - ) -> None: - if ret is None: - return - if namespace is not None: - key = client.build_key(key, namespace) - if key in client._handlers: - await client.expire(key, client.ttl) + async def revalidate(self, key: str, ttl: int | None) -> None: + if ttl is not None and key in self.cache._handlers: + await self.cache.expire(key, ttl) diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index 0f802997f..72cd020ff 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -13,11 +13,12 @@ import onnxruntime as ort import pytest from fastapi.testclient import TestClient from PIL import Image +from pytest import MonkeyPatch from pytest_mock import MockerFixture -from app.main import load +from app.main import load, preload_models -from .config import log, settings +from .config import Settings, log, settings from .models.base import InferenceModel from .models.cache import ModelCache from .models.clip import MCLIPEncoder, OpenCLIPEncoder @@ -509,20 +510,20 @@ class TestCache: @mock.patch("app.models.cache.OptimisticLock", autospec=True) async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None: - model_cache = ModelCache(ttl=100) - await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) + model_cache = ModelCache() + await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100) @mock.patch("app.models.cache.SimpleMemoryCache.expire") async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None: - model_cache = ModelCache(ttl=100, revalidate=True) - await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) - await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) + model_cache = ModelCache(revalidate=True) + await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) + await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) mock_cache_expire.assert_called_once_with(mock.ANY, 100) async def test_profiling(self, mock_get_model: mock.Mock) -> None: - model_cache = ModelCache(ttl=100, profiling=True) - await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION) + model_cache = ModelCache(profiling=True) + await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100) profiling = await model_cache.get_profiling() assert isinstance(profiling, dict) assert profiling == model_cache.cache.profiling @@ -548,6 +549,25 @@ class TestCache: with pytest.raises(ValueError): await model_cache.get("test_model_name", ModelType.CLIP, mode="text") + async def test_preloads_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None: + os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai" + os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s" + + settings = Settings() + assert settings.preload is not None + assert settings.preload.clip == "ViT-B-32__openai" + assert settings.preload.facial_recognition == "buffalo_s" + + model_cache = ModelCache() + monkeypatch.setattr("app.main.model_cache", model_cache) + + await preload_models(settings.preload) + assert len(model_cache.cache._cache) == 2 + assert mock_get_model.call_count == 2 + await model_cache.get("ViT-B-32__openai", ModelType.CLIP, ttl=100) + await model_cache.get("buffalo_s", ModelType.FACIAL_RECOGNITION, ttl=100) + assert mock_get_model.call_count == 2 + @pytest.mark.asyncio class TestLoad: diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index c22668380..1016b330c 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:96586e238e2fed914b839e50cf91943b5655262348d141466b34ced2e0b5b155 as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index d3bb8578d..ac5dfd2bf 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -680,13 +680,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi" -version = "0.109.2" +version = "0.110.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, - {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, + {file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"}, + {file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"}, ] [package.dependencies] @@ -1274,13 +1274,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "huggingface-hub" -version = "0.20.3" +version = "0.21.3" 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.20.3-py3-none-any.whl", hash = "sha256:d988ae4f00d3e307b0c80c6a05ca6dbb7edba8bba3079f74cda7d9c2e562a7b6"}, - {file = "huggingface_hub-0.20.3.tar.gz", hash = "sha256:94e7f8e074475fbc67d6a71957b678e1b4a74ff1b64a644fd6cbb83da962d05d"}, + {file = "huggingface_hub-0.21.3-py3-none-any.whl", hash = "sha256:b183144336fdf2810a8c109822e0bb6ef1fd61c65da6fb60e8c3f658b7144016"}, + {file = "huggingface_hub-0.21.3.tar.gz", hash = "sha256:26a15b604e4fc7bad37c467b76456543ec849386cbca9cd7e1e135f53e500423"}, ] [package.dependencies] @@ -1297,11 +1297,12 @@ all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", cli = ["InquirerPy (==0.3.4)"] dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"] quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"] tensorflow = ["graphviz", "pydot", "tensorflow"] testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["torch"] +torch = ["safetensors", "torch"] typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] [[package]] @@ -1566,13 +1567,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.23.1" +version = "2.24.0" description = "Developer friendly load testing framework" optional = false python-versions = ">=3.8" files = [ - {file = "locust-2.23.1-py3-none-any.whl", hash = "sha256:96013a460a4b4d6d4fd46c70e6ff1fd2b6e03b48ddb1b48d1513d3134ba2cecf"}, - {file = "locust-2.23.1.tar.gz", hash = "sha256:6cc729729e5ebf5852fc9d845302cfcf0ab0132f198e68b3eb0c88b438b6a863"}, + {file = "locust-2.24.0-py3-none-any.whl", hash = "sha256:1b6b878b4fd0108fec956120815e69775d2616c8f4d1e9f365c222a7a5c17d9a"}, + {file = "locust-2.24.0.tar.gz", hash = "sha256:6cffa378d995244a7472af6be1d6139331f19aee44e907deee73e0281252804d"}, ] [package.dependencies] @@ -1588,6 +1589,7 @@ pywin32 = {version = "*", markers = "platform_system == \"Windows\""} pyzmq = ">=25.0.0" requests = ">=2.26.0" roundrobin = ">=0.0.2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} Werkzeug = ">=2.0.0" [[package]] @@ -1988,36 +1990,36 @@ reference = ["Pillow", "google-re2"] [[package]] name = "onnxruntime" -version = "1.17.0" +version = "1.17.1" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime-1.17.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d2b22a25a94109cc983443116da8d9805ced0256eb215c5e6bc6dcbabefeab96"}, - {file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c87d83c6f58d1af2675fc99e3dc810f2dbdb844bcefd0c1b7573632661f6fc"}, - {file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dba55723bf9b835e358f48c98a814b41692c393eb11f51e02ece0625c756b797"}, - {file = "onnxruntime-1.17.0-cp310-cp310-win32.whl", hash = "sha256:ee48422349cc500273beea7607e33c2237909f58468ae1d6cccfc4aecd158565"}, - {file = "onnxruntime-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f34cc46553359293854e38bdae2ab1be59543aad78a6317e7746d30e311110c3"}, - {file = "onnxruntime-1.17.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:16d26badd092c8c257fa57c458bb600d96dc15282c647ccad0ed7b2732e6c03b"}, - {file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f1273bebcdb47ed932d076c85eb9488bc4768fcea16d5f2747ca692fad4f9d3"}, - {file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb60fd3c2c1acd684752eb9680e89ae223e9801a9b0e0dc7b28adabe45a2e380"}, - {file = "onnxruntime-1.17.0-cp311-cp311-win32.whl", hash = "sha256:4b038324586bc905299e435f7c00007e6242389c856b82fe9357fdc3b1ef2bdc"}, - {file = "onnxruntime-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:93d39b3fa1ee01f034f098e1c7769a811a21365b4883f05f96c14a2b60c6028b"}, - {file = "onnxruntime-1.17.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:90c0890e36f880281c6c698d9bc3de2afbeee2f76512725ec043665c25c67d21"}, - {file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7466724e809a40e986b1637cba156ad9fc0d1952468bc00f79ef340bc0199552"}, - {file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d47bee7557a8b99c8681b6882657a515a4199778d6d5e24e924d2aafcef55b0a"}, - {file = "onnxruntime-1.17.0-cp312-cp312-win32.whl", hash = "sha256:bb1bf1ee575c665b8bbc3813ab906e091a645a24ccc210be7932154b8260eca1"}, - {file = "onnxruntime-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac2f286da3494b29b4186ca193c7d4e6a2c1f770c4184c7192c5da142c3dec28"}, - {file = "onnxruntime-1.17.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1ec485643b93e0a3896c655eb2426decd63e18a278bb7ccebc133b340723624f"}, - {file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c35809cda898c5a11911c69ceac8a2ac3925911854c526f73bad884582f911"}, - {file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa464aa4d81df818375239e481887b656e261377d5b6b9a4692466f5f3261edc"}, - {file = "onnxruntime-1.17.0-cp38-cp38-win32.whl", hash = "sha256:b7b337cd0586f7836601623cbd30a443df9528ef23965860d11c753ceeb009f2"}, - {file = "onnxruntime-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:fbb9faaf51d01aa2c147ef52524d9326744c852116d8005b9041809a71838878"}, - {file = "onnxruntime-1.17.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:5a06ab84eaa350bf64b1d747b33ccf10da64221ed1f38f7287f15eccbec81603"}, - {file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d3d11db2c8242766212a68d0b139745157da7ce53bd96ba349a5c65e5a02357"}, - {file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5632077c3ab8b0cd4f74b0af9c4e924be012b1a7bcd7daa845763c6c6bf14b7d"}, - {file = "onnxruntime-1.17.0-cp39-cp39-win32.whl", hash = "sha256:61a12732cba869b3ad2d4e29ab6cb62c7a96f61b8c213f7fcb961ba412b70b37"}, - {file = "onnxruntime-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:461fa0fc7d9c392c352b6cccdedf44d818430f3d6eacd924bb804fdea2dcfd02"}, + {file = "onnxruntime-1.17.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d43ac17ac4fa3c9096ad3c0e5255bb41fd134560212dc124e7f52c3159af5d21"}, + {file = "onnxruntime-1.17.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55b5e92a4c76a23981c998078b9bf6145e4fb0b016321a8274b1607bd3c6bd35"}, + {file = "onnxruntime-1.17.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebbcd2bc3a066cf54e6f18c75708eb4d309ef42be54606d22e5bdd78afc5b0d7"}, + {file = "onnxruntime-1.17.1-cp310-cp310-win32.whl", hash = "sha256:5e3716b5eec9092e29a8d17aab55e737480487deabfca7eac3cd3ed952b6ada9"}, + {file = "onnxruntime-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:fbb98cced6782ae1bb799cc74ddcbbeeae8819f3ad1d942a74d88e72b6511337"}, + {file = "onnxruntime-1.17.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:36fd6f87a1ecad87e9c652e42407a50fb305374f9a31d71293eb231caae18784"}, + {file = "onnxruntime-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99a8bddeb538edabc524d468edb60ad4722cff8a49d66f4e280c39eace70500b"}, + {file = "onnxruntime-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd7fddb4311deb5a7d3390cd8e9b3912d4d963efbe4dfe075edbaf18d01c024e"}, + {file = "onnxruntime-1.17.1-cp311-cp311-win32.whl", hash = "sha256:606a7cbfb6680202b0e4f1890881041ffc3ac6e41760a25763bd9fe146f0b335"}, + {file = "onnxruntime-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:53e4e06c0a541696ebdf96085fd9390304b7b04b748a19e02cf3b35c869a1e76"}, + {file = "onnxruntime-1.17.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:40f08e378e0f85929712a2b2c9b9a9cc400a90c8a8ca741d1d92c00abec60843"}, + {file = "onnxruntime-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac79da6d3e1bb4590f1dad4bb3c2979d7228555f92bb39820889af8b8e6bd472"}, + {file = "onnxruntime-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae9ba47dc099004e3781f2d0814ad710a13c868c739ab086fc697524061695ea"}, + {file = "onnxruntime-1.17.1-cp312-cp312-win32.whl", hash = "sha256:2dff1a24354220ac30e4a4ce2fb1df38cb1ea59f7dac2c116238d63fe7f4c5ff"}, + {file = "onnxruntime-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:6226a5201ab8cafb15e12e72ff2a4fc8f50654e8fa5737c6f0bd57c5ff66827e"}, + {file = "onnxruntime-1.17.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:cd0c07c0d1dfb8629e820b05fda5739e4835b3b82faf43753d2998edf2cf00aa"}, + {file = "onnxruntime-1.17.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:617ebdf49184efa1ba6e4467e602fbfa029ed52c92f13ce3c9f417d303006381"}, + {file = "onnxruntime-1.17.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dae9071e3facdf2920769dceee03b71c684b6439021defa45b830d05e148924"}, + {file = "onnxruntime-1.17.1-cp38-cp38-win32.whl", hash = "sha256:835d38fa1064841679433b1aa8138b5e1218ddf0cfa7a3ae0d056d8fd9cec713"}, + {file = "onnxruntime-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:96621e0c555c2453bf607606d08af3f70fbf6f315230c28ddea91754e17ad4e6"}, + {file = "onnxruntime-1.17.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:7a9539935fb2d78ebf2cf2693cad02d9930b0fb23cdd5cf37a7df813e977674d"}, + {file = "onnxruntime-1.17.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45c6a384e9d9a29c78afff62032a46a993c477b280247a7e335df09372aedbe9"}, + {file = "onnxruntime-1.17.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e19f966450f16863a1d6182a685ca33ae04d7772a76132303852d05b95411ea"}, + {file = "onnxruntime-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e2ae712d64a42aac29ed7a40a426cb1e624a08cfe9273dcfe681614aa65b07dc"}, + {file = "onnxruntime-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7e9f7fb049825cdddf4a923cfc7c649d84d63c0134315f8e0aa9e0c3004672c"}, ] [package.dependencies] @@ -2633,6 +2635,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -2812,13 +2815,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.0" +version = "13.7.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.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [package.dependencies] @@ -2840,28 +2843,28 @@ files = [ [[package]] name = "ruff" -version = "0.2.2" +version = "0.3.0" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, - {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, - {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, - {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, - {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, - {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, - {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, - {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"}, + {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"}, + {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"}, + {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"}, + {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"}, + {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"}, + {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"}, + {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"}, ] [[package]] diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 0fffde12b..fc881d29a 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.97.0" +version = "1.98.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/misc/release/pump-version.sh b/misc/release/pump-version.sh index 7d3f268f3..45d32ebfc 100755 --- a/misc/release/pump-version.sh +++ b/misc/release/pump-version.sh @@ -62,9 +62,12 @@ fi if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER" npm --prefix server version "$SERVER_PUMP" - npm --prefix web version "$SERVER_PUMP" - npm --prefix open-api/typescript-sdk version "$SERVER_PUMP" make open-api + npm --prefix open-api/typescript-sdk version "$SERVER_PUMP" + npm --prefix web version "$SERVER_PUMP" + npm --prefix web i --package-lock-only + npm --prefix cli i --package-lock-only + npm --prefix e2e i --package-lock-only poetry --directory machine-learning version "$SERVER_PUMP" fi diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 6e051103d..a656dbc4c 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" => 125, - "android.injected.version.name" => "1.97.0", + "android.injected.version.code" => 126, + "android.injected.version.name" => "1.98.0", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/assets/i18n/ca.json b/mobile/assets/i18n/ca.json index 306025e23..2db42ad85 100644 --- a/mobile/assets/i18n/ca.json +++ b/mobile/assets/i18n/ca.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALLS", "exif_bottom_sheet_location": "UBICACIร“", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json index 68e37feb1..7f1a38078 100644 --- a/mobile/assets/i18n/cs-CZ.json +++ b/mobile/assets/i18n/cs-CZ.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "PODROBNOSTI", "exif_bottom_sheet_location": "LOKALITA", "exif_bottom_sheet_location_add": "Pล™idat polohu", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Zpracovรกvรกm", "experimental_settings_new_asset_list_title": "Povolenรญ experimentรกlnรญ mล™รญลพky fotografiรญ", "experimental_settings_subtitle": "Pouลพรญvejte na vlastnรญ riziko!", diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json index a21044b37..bb59fddf8 100644 --- a/mobile/assets/i18n/da-DK.json +++ b/mobile/assets/i18n/da-DK.json @@ -35,8 +35,8 @@ "app_bar_signout_dialog_title": "Log ud", "archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet", "archive_page_title": "Arkivรฉr ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "Kan ikke slette kun lรฆselige elementer. Springer over", + "asset_action_share_err_offline": "Kan ikke hente offline element(er). Springer over", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout", "asset_list_layout_settings_group_automatically": "Automatisk", "asset_list_layout_settings_group_by": "Gruppรฉr elementer pr. ", @@ -150,7 +150,7 @@ "control_bottom_app_bar_share": "Del", "control_bottom_app_bar_share_to": "Del til", "control_bottom_app_bar_stack": "Stak", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Flyt til papirkurv", "control_bottom_app_bar_unarchive": "Afakivรฉr", "control_bottom_app_bar_unfavorite": "Fjern favorit", "control_bottom_app_bar_upload": "Upload", @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALJER", "exif_bottom_sheet_location": "LOKATION", "exif_bottom_sheet_location_add": "Tilfรธj en placering", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Under udarbejdelse", "experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter", "experimental_settings_subtitle": "Brug pรฅ eget ansvar!", @@ -199,7 +200,7 @@ "home_page_archive_err_partner": "Kan endnu ikke arkivere partners elementer. Springer over", "home_page_building_timeline": "Bygger tidslinjen", "home_page_delete_err_partner": "Kan endnu ikke slette partners elementer. Springer over", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_remote_err_local": "Lokale elementer i fjernsletningssektion. Springer over", "home_page_favorite_err_local": "Kan endnu ikke gรธre lokale elementer til favoritter. Springer over..", "home_page_favorite_err_partner": "Kan endnu ikke tilfรธje partners elementer som favoritter. Springer over", "home_page_first_time_notice": "Hvis det er din fรธrste gang i appen, bedes du vรฆlge en sikkerhedskopi af albummer sรฅ tidlinjen kan blive fyldt med billeder og videoer fra albummerne.", @@ -279,8 +280,8 @@ "map_zoom_to_see_photos": "Zoom ud for at vise billeder", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Bevรฆgelsesbilleder", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen pรฅ kun lรฆselige elementer. Springer over", + "multiselect_grid_edit_gps_err_read_only": "Kan ikke redigere lokation af kun lรฆselige elementer. Springer over", "notification_permission_dialog_cancel": "Annuller", "notification_permission_dialog_content": "Gรฅ til indstillinger for at slรฅ notifikationer til.", "notification_permission_dialog_settings": "Indstillinger", diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index 4beb2f701..298847cd7 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "STANDORT", "exif_bottom_sheet_location_add": "Aufnahmeort hinzufรผgen", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "In Arbeit", "experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren", "experimental_settings_subtitle": "Benutzung auf eigene Gefahr!", diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index d855502ef..eda783891 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index d15b5c96e..7c0ae71c7 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -1,6 +1,6 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "Cancelar", + "action_common_update": "Actualizar", "add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "advanced_settings_log_level_title": "Nivel de log: {}", @@ -35,7 +35,7 @@ "app_bar_signout_dialog_title": "Cerrar sesiรณn", "archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_title": "Archivo ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", + "asset_action_delete_err_read_only": "No se pueden borrar los archivos de solo lectura. Saltando.", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_list_layout_settings_dynamic_layout_title": "Diseรฑo dinรกmico", "asset_list_layout_settings_group_automatically": "Automatico", @@ -142,15 +142,15 @@ "control_bottom_app_bar_archive": "Archivar", "control_bottom_app_bar_create_new_album": "Crear nuevo รกlbum", "control_bottom_app_bar_delete": "Eliminar", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_delete_from_immich": "Borrar de Immich", + "control_bottom_app_bar_delete_from_local": "Borrar del dispositivo", "control_bottom_app_bar_edit_location": "Editar ubicaciรณn", "control_bottom_app_bar_edit_time": "Editar fecha y hora", "control_bottom_app_bar_favorite": "Favorito", "control_bottom_app_bar_share": "Compartir", "control_bottom_app_bar_share_to": "Enviar", "control_bottom_app_bar_stack": "Apilar", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Mover a la papelera", "control_bottom_app_bar_unarchive": "Desarchivar", "control_bottom_app_bar_unfavorite": "Unfavorite", "control_bottom_app_bar_upload": "Subir", @@ -165,9 +165,9 @@ "daily_title_text_date_year": "E dd de MMM, yyyy", "date_format": "E d, LLL y โ€ข h:mm a", "delete_dialog_alert": "Estos elementos serรกn eliminados permanentemente de Immich y de tu dispositivo", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Estas imรกgenes van a ser borradas de tu dispositivo, pero seguirรกn disponibles en el servidor Immich", + "delete_dialog_alert_local_non_backed_up": "Algunas de las imรกgenes no tienen copia de seguridad y serรกn borradas de forma permanente de tu dispositivo", + "delete_dialog_alert_remote": "Estas imรกgenes van a ser borradas de forma permanente del servidor Immich", "delete_dialog_cancel": "Cancelar", "delete_dialog_ok": "Eliminar", "delete_dialog_ok_force": "Delete Anyway", @@ -178,13 +178,14 @@ "delete_shared_link_dialog_title": "Eliminar enlace compartido", "description_input_hint_text": "Agregar descripciรณn...", "description_input_submit_error": "Error al actualizar la descripciรณn, verifica el registro para obtener mรกs detalles", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "edit_date_time_dialog_date_time": "Fecha y Hora", + "edit_date_time_dialog_timezone": "Zona horaria", + "edit_location_dialog_title": "Ubicaciรณn", "exif_bottom_sheet_description": "Agregar Descripciรณn...", "exif_bottom_sheet_details": "DETALLES", "exif_bottom_sheet_location": "UBICACIร“N", "exif_bottom_sheet_location_add": "Aรฑadir ubicaciรณn", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrรญcula fotogrรกfica experimental", "experimental_settings_subtitle": "รšsalo bajo tu responsabilidad", @@ -220,13 +221,13 @@ "library_page_sort_most_oldest_photo": "Foto mรกs antigua", "library_page_sort_most_recent_photo": "Foto mรกs reciente", "library_page_sort_title": "Tรญtulo del รกlbum", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "location_picker_choose_on_map": "Elegir en el mapa", + "location_picker_latitude": "Latitud", + "location_picker_latitude_error": "Introduce una latitud vรกlida", + "location_picker_latitude_hint": "Introduce tu latitud aquรญ", + "location_picker_longitude": "Longitud", + "location_picker_longitude_error": "Introduce una longitud vรกlida", + "location_picker_longitude_hint": "Introduce tu longitud aquรญ", "login_disabled": "El inicio de sesiรณn ha sido desactivado", "login_form_api_exception": "Excepciรณn producida por API. Por favor, verifica el URL del servidor e intรฉntalo de nuevo.", "login_form_back_button_text": "Atrรกs", @@ -252,12 +253,12 @@ "login_form_server_error": "No se pudo conectar al servidor.", "login_password_changed_error": "Hubo un error actualizando la contraseรฑa", "login_password_changed_success": "Contraseรฑa cambiado con รฉxito", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bound": "{} foto", + "map_assets_in_bounds": "{} fotos", "map_cannot_get_user_location": "No se pudo obtener la posiciรณn del usuario", "map_location_dialog_cancel": "Cancelar", "map_location_dialog_yes": "Sรญ", - "map_location_picker_page_use_location": "Use this location", + "map_location_picker_page_use_location": "Usar esta ubicaciรณn", "map_location_service_disabled_content": "Los servicios de ubicaciรณn deben estar activados para mostrar elementos de tu ubicaciรณn actual. Deseas activarlos ahora?", "map_location_service_disabled_title": "Servicios de ubicaciรณn desactivados", "map_no_assets_in_bounds": "No hay fotos en esta zona", @@ -265,22 +266,22 @@ "map_no_location_permission_title": "Permisos de ubicaciรณn denegados", "map_settings_dark_mode": "Modo oscuro", "map_settings_date_range_option_all": "Todo", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_day": "รšltimas 24 horas", + "map_settings_date_range_option_days": "รšltimos {} dรญas", + "map_settings_date_range_option_year": "รšltimo aรฑo", + "map_settings_date_range_option_years": "รšltimos {} aรฑos", "map_settings_dialog_cancel": "Cancelar", "map_settings_dialog_save": "Guardar", "map_settings_dialog_title": "Ajustes mapa", "map_settings_include_show_archived": "Incluir archivados", "map_settings_only_relative_range": "Rango de fechas", "map_settings_only_show_favorites": "Mostrar solo favoritas", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Apariencia del Mapa", "map_zoom_to_see_photos": "Alejar para ver fotos", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Foto en Movimiento", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha de archivos de solo lectura. Saltando.", + "multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localizaciรณn de archivos de solo lectura. Saltando.", "notification_permission_dialog_cancel": "Cancelar", "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuraciรณn y selecciona permitir.", "notification_permission_dialog_settings": "Ajustes", @@ -318,7 +319,7 @@ "profile_drawer_sign_out": "Cerrar Sesiรณn", "profile_drawer_trash": "Papelera", "recently_added_page_title": "Reciรฉn Agregadas", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "Ha ocurrido un error", "search_bar_hint": "Busca tus fotos", "search_page_categories": "Categorรญas", "search_page_favorites": "Favoritos", @@ -330,9 +331,9 @@ "search_page_person_add_name_dialog_hint": "Nombre", "search_page_person_add_name_dialog_save": "Guardar", "search_page_person_add_name_dialog_title": "Aรฑadir nombre", - "search_page_person_add_name_subtitle": "Find them fast by name with search", - "search_page_person_add_name_title": "Add a name", - "search_page_person_edit_name": "Edit name", + "search_page_person_add_name_subtitle": "Encuรฉntralos rรกpido buscando por nombre", + "search_page_person_add_name_title": "Aรฑadir nombre", + "search_page_person_edit_name": "Cambiar nombre", "search_page_places": "Lugares", "search_page_recently_added": "Reciรฉn agregadas", "search_page_screenshots": "Capturas de pantalla", @@ -341,7 +342,7 @@ "search_page_videos": "Videos", "search_page_view_all_button": "Ver todo", "search_page_your_activity": "Tu actividad", - "search_page_your_map": "Your Map", + "search_page_your_map": "Tu Mapa", "search_result_page_new_search_hint": "Nueva Busqueda", "search_suggestion_list_smart_search_hint_1": "La bรบsqueda inteligente estรก habilitada por defecto, para buscar metadatos utiliza esta sintaxis ", "search_suggestion_list_smart_search_hint_2": "m:tu-tรฉrmino-de-bรบsqueda", @@ -381,17 +382,17 @@ "shared_album_activity_remove_title": "Eliminar Actividad", "shared_album_activity_setting_subtitle": "Permitir que otros respondan", "shared_album_activity_setting_title": "Comentarios y me gusta", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_owner_label": "Owner", - "shared_album_section_people_title": "PEOPLE", + "shared_album_section_people_action_error": "Error dejando/eliminando del album", + "shared_album_section_people_action_leave": "Eliminar usuario del album", + "shared_album_section_people_action_remove_user": "Eliminar usuario del album", + "shared_album_section_people_owner_label": "Propietario", + "shared_album_section_people_title": "GENTE", "share_dialog_preparing": "Preparando...", "shared_link_app_bar_title": "Enlaces compartidos", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_clipboard_copied_massage": "Copiado al portapapeles", + "shared_link_clipboard_text": "Enlace: {}\nContraseรฑa: {}", "shared_link_create_app_bar_title": "Crear enlace compartido", - "shared_link_create_error": "Error while creating shared link", + "shared_link_create_error": "Error creando el enlace compartido", "shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas", "shared_link_create_submit_button": "Crear enlace", "shared_link_edit_allow_download": "Permitir descargar a usuarios pรบblicos", @@ -401,32 +402,32 @@ "shared_link_edit_description": "Descripciรณn", "shared_link_edit_description_hint": "Introduce la descripciรณn del enlace", "shared_link_edit_expire_after": "Expirar despuรฉs de", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_expire_after_option_day": "1 dรญa", + "shared_link_edit_expire_after_option_days": "{} dรญas", + "shared_link_edit_expire_after_option_hour": "1 hora", + "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_minute": "1 minuto", + "shared_link_edit_expire_after_option_minutes": "{} minutos", + "shared_link_edit_expire_after_option_never": "Nunca", "shared_link_edit_password": "Contraseรฑa", "shared_link_edit_password_hint": "Introduce la contraseรฑa del enlace", "shared_link_edit_show_meta": "Mostrar metadatos", "shared_link_edit_submit_button": "Actualizar enlace", "shared_link_empty": "No tienes enlaces compartidos", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", + "shared_link_error_server_url_fetch": "No se puede adquirir la URL del servidor", + "shared_link_expired": "Caducado", + "shared_link_expires_day": "Caduca en {} dรญa", + "shared_link_expires_days": "Caduca en {} dรญas", + "shared_link_expires_hour": "Caduca en {} hora", + "shared_link_expires_hours": "Caduca en {} horas", + "shared_link_expires_minute": "Caduca en {} minuto", "shared_link_expires_minutes": "Caduca en {} minutos", - "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", + "shared_link_expires_never": "Caduca โˆž", + "shared_link_expires_second": "Caduca en {} segundo", "shared_link_expires_seconds": "Caduca en {} segundos", - "shared_link_info_chip_download": "Download", + "shared_link_info_chip_download": "Descargar", "shared_link_info_chip_metadata": "EXIF", - "shared_link_info_chip_upload": "Upload", + "shared_link_info_chip_upload": "Subir", "shared_link_manage_links": "Administrar enlaces compartidos", "share_done": "Hecho", "share_invite": "Invitar al รกlbum", diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json index b85515315..c96f1adac 100644 --- a/mobile/assets/i18n/es-MX.json +++ b/mobile/assets/i18n/es-MX.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALLES", "exif_bottom_sheet_location": "UBICACIร“N", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrรญcula fotogrรกfica experimental", "experimental_settings_subtitle": "รšsalo bajo tu responsabilidad", diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json index 76b1a5a00..753ee01ee 100644 --- a/mobile/assets/i18n/es-PE.json +++ b/mobile/assets/i18n/es-PE.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALLES", "exif_bottom_sheet_location": "UBICACIร“N", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Trabajo en progreso", "experimental_settings_new_asset_list_title": "Habilitar cuadrรญcula fotogrรกfica experimental", "experimental_settings_subtitle": "รšsalo bajo tu responsabilidad", diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json index e462bb347..b5fa0e263 100644 --- a/mobile/assets/i18n/fi-FI.json +++ b/mobile/assets/i18n/fi-FI.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "TIEDOT", "exif_bottom_sheet_location": "SIJAINTI", "exif_bottom_sheet_location_add": "Lisรครค sijainti", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Tyรถn alla", "experimental_settings_new_asset_list_title": "Ota kรคyttรถรถn kokeellinen kuvaruudukko", "experimental_settings_subtitle": "Kรคyttรถ omalla vastuulla!", @@ -199,7 +200,7 @@ "home_page_archive_err_partner": "Kumppanin kohteita ei voi arkistoida. Hypรคtรครคn yli", "home_page_building_timeline": "Rakennetaan aikajanaa", "home_page_delete_err_partner": "Kumppanin kohteita ei voi poistaa.Hypรคtรครคn yli", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_remote_err_local": "Paikallisia kohteita etรคkohdevalintojen joukossa, ohitetaan", "home_page_favorite_err_local": "Paikallisten kohteiden lisรครคminen suosikkeihin ei ole mahdollista, ohitetaan", "home_page_favorite_err_partner": "Kumppanin kohteita ei voi vielรค merkitรค suosikiksi. Hypรคtรครคn yli", "home_page_first_time_notice": "Jos kรคytรคt sovellusta ensimmรคistรค kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index 7e77774ec..5bc611123 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "Dร‰TAILS", "exif_bottom_sheet_location": "LOCALISATION", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "En cours de dรฉveloppement", "experimental_settings_new_asset_list_title": "Activer la grille de photos expรฉrimentale", "experimental_settings_subtitle": "Utilisez ร  vos dรฉpends !", diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json index 1017fce24..24763bd4f 100644 --- a/mobile/assets/i18n/hi-IN.json +++ b/mobile/assets/i18n/hi-IN.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json index 168340ad1..2d18c4b6a 100644 --- a/mobile/assets/i18n/hu-HU.json +++ b/mobile/assets/i18n/hu-HU.json @@ -1,74 +1,74 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "Mรฉgsem", + "action_common_update": "Frissรญt", "add_to_album_bottom_sheet_added": "Hozzรกadva a(z) {album} nevลฑ albumhoz", "add_to_album_bottom_sheet_already_exists": "Mรกr eleme a(z) {album} nevลฑ albumnak", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_log_level_title": "Naplรณzรกs szintje: {}", + "advanced_settings_prefer_remote_subtitle": "Nรฉhรกny eszkรถz fรกjdalmasan lassan tรถlti be az eszkรถzรถn lรฉvล‘ elemeket. Ezzel a beรกllรญtรกssal inkรกbb a tรกvoli kรฉpeket tรถltjรผk be helyette.", + "advanced_settings_prefer_remote_title": "Tรกvoli kรฉpek preferรกlรกsa", + "advanced_settings_self_signed_ssl_subtitle": "SSL tanรบsรญtvรกny ellenล‘rzรฉsรฉnek kihagyรกsa a szerver vรฉgponthoz. Ehhez sajรกt alรกรญrt tanรบsรญtvรกnyok szรผksรฉgesek.", + "advanced_settings_self_signed_ssl_title": "Sajรกt alรกรญrt SSL tanรบsรญtvรกnyok engedรฉlyezรฉse", "advanced_settings_tile_subtitle": "Haladรณ felhasznรกlรณi beรกllรญtรกsok", "advanced_settings_tile_title": "Haladรณ", "advanced_settings_troubleshooting_subtitle": "Tovรกbbi funkciรณk engedรฉlyezรฉse hibaelhรกrรญtรกs cรฉljรกbรณl", "advanced_settings_troubleshooting_title": "Hibaelhรกrรญtรกs", - "album_info_card_backup_album_excluded": "EXCLUDED", - "album_info_card_backup_album_included": "INCLUDED", + "album_info_card_backup_album_excluded": "KIZรRVA", + "album_info_card_backup_album_included": "BELEร‰RTVE", "album_thumbnail_card_item": "1 elem", "album_thumbnail_card_items": "{} elem", "album_thumbnail_card_shared": "ยท Megosztott", "album_thumbnail_owned": "Tulajdonos", "album_thumbnail_shared_by": "Megosztotta: {}", "album_viewer_appbar_share_delete": "Album tรถrlรฉse", - "album_viewer_appbar_share_err_delete": "Hiba az album tรถrlรฉse kรถzben", - "album_viewer_appbar_share_err_leave": "Hiba az albumbรณl valรณ kilรฉpรฉs kรถzben", - "album_viewer_appbar_share_err_remove": "Hiba az elemek tรถrlรฉse kรถzben", - "album_viewer_appbar_share_err_title": "Hiba az album รกtnevezรฉse kรถzben", + "album_viewer_appbar_share_err_delete": "Nem sikerรผlt tรถrรถlni az albumot", + "album_viewer_appbar_share_err_leave": "Nem sikerรผlt kilรฉpni az albumbรณl", + "album_viewer_appbar_share_err_remove": "Nรฉhรกny elemet nem sikerรผlt tรถrรถlni az albumbรณl", + "album_viewer_appbar_share_err_title": "Nem sikerรผlt รกtnevezni az albumot", "album_viewer_appbar_share_leave": "Kilรฉpรฉs az albumbรณl", - "album_viewer_appbar_share_remove": "Tรถrlรฉs az albumbรณl", - "album_viewer_appbar_share_to": "Share To", + "album_viewer_appbar_share_remove": "Eltรกvolรญtรกs az albumbรณl", + "album_viewer_appbar_share_to": "Megosztรกs Ide", "album_viewer_page_share_add_users": "Felhasznรกlรณk hozzรกadรกsa", "all_people_page_title": "Emberek", "all_videos_page_title": "Videรณk", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", - "archive_page_no_archived_assets": "Nem talรกlhatรณ archivรกlt mรฉdia", + "app_bar_signout_dialog_content": "Biztos, hogy ki szeretnรฉl jelentkezni?", + "app_bar_signout_dialog_ok": "Igen", + "app_bar_signout_dialog_title": "Kijelentkezรฉs", + "archive_page_no_archived_assets": "Nem talรกlhatรณ archivรกlt elem", "archive_page_title": "Archรญvum ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", + "asset_action_delete_err_read_only": "Nem sikerรผlt tรถrรถlni a csak-olvashatรณ elem(ek)et, รญgy ezeket รกtugorjuk", + "asset_action_share_err_offline": "Nem sikerรผlt betรถlteni az offline elem(ek)et, รญgy ezeket kihagyjuk", + "asset_list_layout_settings_dynamic_layout_title": "Dinamikus elrendezรฉs", "asset_list_layout_settings_group_automatically": "Automatikus", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month": "Hรณnap", - "asset_list_layout_settings_group_by_month_day": "Hรณnap + nap", - "asset_list_settings_subtitle": "Photo grid layout settings", - "asset_list_settings_title": "Photo Grid", - "backup_album_selection_page_albums_device": "Az eszkรถzรถn lรฉvล‘ albumok ({})", - "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", - "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", - "backup_album_selection_page_select_albums": "Albumok kivรกlasztรกsa", - "backup_album_selection_page_selection_info": "Selection Info", + "asset_list_layout_settings_group_by": "Elemek csoportosรญtรกsa", + "asset_list_layout_settings_group_by_month": "hรณnapok szerint", + "asset_list_layout_settings_group_by_month_day": "hรณnap รฉs nap szerint", + "asset_list_settings_subtitle": "Fotรณrรกcs elrendezรฉse", + "asset_list_settings_title": "Fotรณrรกcs", + "backup_album_selection_page_albums_device": "Ezen az eszkรถzรถn lรฉvล‘ albumok ({})", + "backup_album_selection_page_albums_tap": "Koppincs a hozzรกadรกshoz, duplรกn koppincs az eltรกvolรญtรกshoz", + "backup_album_selection_page_assets_scatter": "Egy elem tรถbb albumban is lehet. Ezรฉrt a mentรฉshez albumokat lehet hozzรกadni vagy azokat a mentรฉsbล‘l kihagyni.", + "backup_album_selection_page_select_albums": "Vรกlassz albumokat", + "backup_album_selection_page_selection_info": "ร–sszegzรฉs", "backup_album_selection_page_total_assets": "ร–sszes egyedi elem", "backup_all": "ร–sszes", "backup_background_service_backup_failed_message": "HIba a mentรฉs kรถzben. รšjraprรณbรกlkozรกs...", "backup_background_service_connection_failed_message": "HIba a szerverhez valรณ csatlakozรกs kรถzben. รšjraprรณbรกlkozรกs...", "backup_background_service_current_upload_notification": "Feltรถltรฉs {}", - "backup_background_service_default_notification": "Keresรฉs รบj elemek utรกn...", + "backup_background_service_default_notification": "รšj elemek keresรฉse...", "backup_background_service_error_title": "Hiba mentรฉs kรถzben", "backup_background_service_in_progress_notification": "Elemek mentรฉs alatt..", "backup_background_service_upload_failure_notification": "Hiba feltรถltรฉs kรถzben {}", - "backup_controller_page_albums": "Albumok mentรฉse", - "backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.", - "backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled", + "backup_controller_page_albums": "Albumok Mentรฉse", + "backup_controller_page_background_app_refresh_disabled_content": "Engedรฉlyezd a hรกttรฉrben tรถrtรฉnล‘ frissรญtรฉst a Beรกllรญtรกsok > รltalรกnos > Hรกttรฉrben Frissรญtรฉs menรผpontban.", + "backup_controller_page_background_app_refresh_disabled_title": "Hรกttรฉrben frissรญtรฉs kikapcsolva", "backup_controller_page_background_app_refresh_enable_button_text": "Beรกllรญtรกsok megnyitรกsa", "backup_controller_page_background_battery_info_link": "Mutasd meg hogyan", - "backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.", + "backup_controller_page_background_battery_info_message": "A sikeres hรกttรฉrben tรถrtรฉnล‘ mentรฉshez kรฉrjรผk, tiltsd le az Immich akkumulรกtor optimalizรกlรกsรกt.\n\nMivel ezt a kรผlรถnfรฉle eszkรถzรถkรถn mรกshogy kell, ezรฉrt kรฉrjรผk, az eszkรถzรถd gyรกrtรณjรกtรณl tudd meg, hogyan kell.", "backup_controller_page_background_battery_info_ok": "OK", - "backup_controller_page_background_battery_info_title": "Akkumulรกtoroptimalizรกlรกs", + "backup_controller_page_background_battery_info_title": "Akkumulรกtor optimalizรกlรกs", "backup_controller_page_background_charging": "Csak tรถltรฉs kรถzben", - "backup_controller_page_background_configure_error": "Failed to configure the background service", - "backup_controller_page_background_delay": "Delay new assets backup: {}", + "backup_controller_page_background_configure_error": "Nem sikerรผlt beรกllรญtani a hรกttรฉr szolgรกltatรกst", + "backup_controller_page_background_delay": "รšj elemek mentรฉsรฉnek kรฉsleltetรฉse: {}", "backup_controller_page_background_description": "Kapcsold be a hรกttรฉrfolyamatot, hogy automatikusan mentsen elemeket az applikรกciรณ megnyitรกsa nรฉlkรผl", "backup_controller_page_background_is_off": "Automatikus mentรฉs a hรกttรฉrben ki van kapcsolva", "backup_controller_page_background_is_on": "Automatikus mentรฉs a hรกttรฉrben bekapcsolva", @@ -78,63 +78,63 @@ "backup_controller_page_backup": "Mentรฉs", "backup_controller_page_backup_selected": "Kivรกlasztva:", "backup_controller_page_backup_sub": "Mentett fotรณk รฉs videรณk", - "backup_controller_page_cancel": "Megszakรญt", + "backup_controller_page_cancel": "Mรฉgsem", "backup_controller_page_created": "Lรฉtrehozva: {}", - "backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.", + "backup_controller_page_desc_backup": "Ha engedรฉlyezed az elล‘tรฉrben mentรฉst, akkor az รบj elemek automatikusan feltรถltล‘dnek a szerverre, amikor megyitod az alkalmazรกst.", "backup_controller_page_excluded": "Kivรฉve:", "backup_controller_page_failed": "Sikertelen ({})", "backup_controller_page_filename": "Fรกjlnรฉv: {}[{}]", "backup_controller_page_id": "Azonosรญtรณ: {}", "backup_controller_page_info": "Mentรฉsinformรกciรณk", "backup_controller_page_none_selected": "Egy sincs kivรกlasztva", - "backup_controller_page_remainder": "Maradรฉk", + "backup_controller_page_remainder": "Hรกtralรฉvล‘", "backup_controller_page_remainder_sub": "Hรกtralรฉvล‘ fotรณk รฉs videรณk a kijelรถltek kรถzรผl", "backup_controller_page_select": "Kivรกlaszt", - "backup_controller_page_server_storage": "Szerver tรกrhely", - "backup_controller_page_start_backup": "Mentรฉs elindรญtรกsa", - "backup_controller_page_status_off": "Autoatikus mentรฉs az elล‘tรฉrben kikapcsolva", - "backup_controller_page_status_on": "Autoatikus mentรฉs az elล‘tรฉrben bekapcsolva", + "backup_controller_page_server_storage": "Szerver Tรกrhely", + "backup_controller_page_start_backup": "Mentรฉs Elindรญtรกsa", + "backup_controller_page_status_off": "Automatikus mentรฉs az elล‘tรฉrben kikapcsolva", + "backup_controller_page_status_on": "Automatikus mentรฉs az elล‘tรฉrben bekapcsolva", "backup_controller_page_storage_format": "{} / {} felhasznรกlva", - "backup_controller_page_to_backup": "Albumok amiket mentesz", - "backup_controller_page_total": "ร–sszes", + "backup_controller_page_to_backup": "Mentรฉsre kijelรถlt albumok", + "backup_controller_page_total": "ร–sszesen", "backup_controller_page_total_sub": "Minden egyedi fotรณ รฉs videรณ a kijelรถlt albumokbรณl", - "backup_controller_page_turn_off": "Turn off foreground backup", - "backup_controller_page_turn_on": "Turn on foreground backup", - "backup_controller_page_uploading_file_info": "Uploading file info", + "backup_controller_page_turn_off": "Elล‘tรฉrben mentรฉs kikapcsolรกsa", + "backup_controller_page_turn_on": "Elล‘tรฉrben mentรฉs bekapcsolรกsa", + "backup_controller_page_uploading_file_info": "Fรกjl informรกciรณk feltรถltรฉse", "backup_err_only_album": "Az utolsรณ albumot nem tudod tรถrรถlni", "backup_info_card_assets": "elemek", "backup_manual_cancelled": "Megszakรญtva", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", + "backup_manual_failed": "Sikertelen", + "backup_manual_in_progress": "Feltรถltรฉs mรกr folyamatban. Prรณbรกld meg kรฉsล‘bb", "backup_manual_success": "Sikeres", - "backup_manual_title": "Upload status", + "backup_manual_title": "Feltรถltรฉs รกllapota", "cache_settings_album_thumbnails": "Library page thumbnails ({} assets)", - "cache_settings_clear_cache_button": "Gyorsรญtรณtรกr tรถrlรฉse", + "cache_settings_clear_cache_button": "Gyorsรญtรณtรกr kiรผrรญtรฉse", "cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", - "cache_settings_image_cache_size": "Image cache size ({} assets)", - "cache_settings_statistics_album": "Library thumbnails", - "cache_settings_statistics_assets": "{} assets ({})", - "cache_settings_statistics_full": "Teljes kรฉpek", - "cache_settings_statistics_shared": "Shared album thumbnails", - "cache_settings_statistics_thumbnail": "Elล‘nรฉzeti kรฉpek", - "cache_settings_statistics_title": "Gyorsรญtรณtรกr รกltal hasznรกlt terรผlet", - "cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application", - "cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", - "cache_settings_title": "Gyorsรญtรณtรกr beรกllรญtรกsok", + "cache_settings_duplicated_assets_clear_button": "KIรœRรT", + "cache_settings_duplicated_assets_subtitle": "Fotรณk รฉs videรณk, amiket az alkalmazรกs fekete listรกra tett", + "cache_settings_duplicated_assets_title": "Duplikรกlt Elemek ({})", + "cache_settings_image_cache_size": "Kรฉp gyorsรญtรณtรกr mรฉrete ({} elem)", + "cache_settings_statistics_album": "Mappa bรฉlyegkรฉpei", + "cache_settings_statistics_assets": "{} elem ({})", + "cache_settings_statistics_full": "Teljes mรฉretลฑ kรฉpek", + "cache_settings_statistics_shared": "Megosztott album bรฉlyegkรฉpei", + "cache_settings_statistics_thumbnail": "Bรฉlyegkรฉpek", + "cache_settings_statistics_title": "Gyorsรญtรณtรกr hasznรกlata", + "cache_settings_subtitle": "Az Immich mobilalkalmazรกs gyorsรญtรณtรกr viselkedรฉsรฉnek beรกllรญtรกsa", + "cache_settings_thumbnail_size": "Bรฉlyegkรฉp gyorsรญtรณtรกr mรฉrete ({} elem)", + "cache_settings_tile_subtitle": "Helyi tรกrhely viselkedรฉsรฉnek beรกllรญtรกsa", + "cache_settings_tile_title": "Helyi Tรกrhely", + "cache_settings_title": "Gyorsรญtรณtรกr Beรกllรญtรกsok", "change_password_form_confirm_password": "Jelszรณ Megerล‘sรญtรฉse", - "change_password_form_description": "Kedves {name}!\n\nMost jelentkezel be elล‘szรถr a rendszerbe vagy mรกs okbรณl szรผksรฉfes a jelszavad mevรกltoztatรกsa. Kรฉrjรผk, add meg รบj jelszavad.", + "change_password_form_description": "Kedves {name}!\n\nMost jelentkezel be elล‘szรถr a rendszerbe vagy mรกs okbรณl szรผksรฉges a jelszavad mevรกltoztatรกsa. Kรฉrjรผk, add meg รบj jelszavad.", "change_password_form_new_password": "รšj Jelszรณ", "change_password_form_password_mismatch": "A kรฉt beรญrt jelszรณ nem egyezik", "change_password_form_reenter_new_password": "Jelszรณ (mรฉg egyszer)", "common_add_to_album": "Albumhoz ad", "common_change_password": "Jelszรณcsere", "common_create_new_album": "รšj album lรฉtrehozรกsa", - "common_server_error": "Kรฉrjรผk, ellenล‘rizd a hรกlรณzati kapcsolatot, gondoskodj rรณla, hogy a szerver elรฉrhetล‘ legyen, valamint az app รฉs a szerver kompatibilis verziรณjรบ legyen.", + "common_server_error": "Kรฉrjรผk, ellenล‘rizd a hรกlรณzati kapcsolatot, gondoskodj rรณla, hogy a szerver elรฉrhetล‘ legyen, valamint az alkalmazรกs รฉs a szerver kompatibilis verziรณjรบ legyen.", "common_shared": "Megosztva", "control_bottom_app_bar_add_to_album": "Hozzรกadรกs az albumhoz", "control_bottom_app_bar_album_info": "{} elem", @@ -142,23 +142,23 @@ "control_bottom_app_bar_archive": "Archivรกl", "control_bottom_app_bar_create_new_album": "Album lรฉtrehozรกsa", "control_bottom_app_bar_delete": "Tรถrlรฉs", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", + "control_bottom_app_bar_delete_from_immich": "Tรถrlรฉs az Immich-bล‘l", + "control_bottom_app_bar_delete_from_local": "Tรถrlรฉs az eszkรถzrล‘l", + "control_bottom_app_bar_edit_location": "Hely Mรณdosรญtรกsa", + "control_bottom_app_bar_edit_time": "Dรกtum รฉs Idล‘ Mรณdosรญtรกsa", "control_bottom_app_bar_favorite": "Kedvenc", "control_bottom_app_bar_share": "Megosztรกs", - "control_bottom_app_bar_share_to": "Share To", + "control_bottom_app_bar_share_to": "Megosztรกs Ide", "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", - "control_bottom_app_bar_unarchive": "Archivรกlรกs megszรผntetรฉse", - "control_bottom_app_bar_unfavorite": "Unfavorite", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_trash_from_immich": "Lomtรกrba Helyez", + "control_bottom_app_bar_unarchive": "Nem Archivรกlt", + "control_bottom_app_bar_unfavorite": "Nem Kedvenc", + "control_bottom_app_bar_upload": "Feltรถltรฉs", "create_album_page_untitled": "Nรฉvtelen", "create_shared_album_page_create": "Lรฉtrehoz", "create_shared_album_page_share": "Megosztรกs", "create_shared_album_page_share_add_assets": "ELEMEK HOZZรADรSA", - "create_shared_album_page_share_select_photos": "Fotรณk kivรกlasztรกsa", + "create_shared_album_page_share_select_photos": "Fotรณk vรกlasztรกsa", "curated_location_page_title": "Helyek", "curated_object_page_title": "Dolgok", "daily_title_text_date": "E, MMM dd", @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "Rร‰SZLETEK", "exif_bottom_sheet_location": "HELYSZรN", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Fejlesztรฉs alatt", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Csak sajรกt felelล‘ssรฉgre hasznรกld", @@ -230,10 +231,10 @@ "login_disabled": "A bejelentkezรฉs letiltva", "login_form_api_exception": "API hiba. Kรฉrljรผk, ellenล‘rid a szerver cรญmรฉt, majd prรณbรกld รบjra.", "login_form_back_button_text": "Back", - "login_form_button_text": "Belรฉpรฉs", - "login_form_email_hint": "teemailed@email.com", + "login_form_button_text": "Bejelentkezรฉs", + "login_form_email_hint": "email@cimed.hu", "login_form_endpoint_hint": "http://szerver-cรญme:port/api", - "login_form_endpoint_url": "Kiszolgรกlรณ vรฉgpont cรญme", + "login_form_endpoint_url": "Szerver cรญme", "login_form_err_http": "Kรฉrem, adjon meg egy http:// vagy https:// cรญmet", "login_form_err_invalid_email": "ร‰rvรฉnytelen email cรญm", "login_form_err_invalid_url": "ร‰rvรฉnytelen cรญm", @@ -346,7 +347,7 @@ "search_suggestion_list_smart_search_hint_1": "Az intelligens keresรฉs alapรฉrtelmezetten be van kapcsolva, metaadatokat รญgy kereshetsz", "search_suggestion_list_smart_search_hint_2": "m:keresรฉsi-kifejezรฉs", "select_additional_user_for_sharing_page_suggestions": "Javaslatok", - "select_user_for_sharing_page_err_album": "Hiba az album lรฉtrehozรกsa kรถzben", + "select_user_for_sharing_page_err_album": "Nem sikerรผlt lรฉtrehozni az albumot", "select_user_for_sharing_page_share_suggestions": "Javaslatok", "server_info_box_app_version": "Alkalmazรกs Verziรณ", "server_info_box_latest_release": "Latest Version", @@ -373,7 +374,7 @@ "settings_require_restart": "Kรฉrlek indรญtsd รบjra az Immich-et hogy alkalmazd ezt a beรกllรญtรกst", "share_add": "Hozzรกadรกs", "share_add_photos": "Fotรณk hozzรกadรกsa", - "share_add_title": "Cรญm hozzรกadรกsa", + "share_add_title": "Album neve", "share_create_album": "Album lรฉtrehozรกsa", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_hint": "Say something", @@ -431,11 +432,11 @@ "share_done": "Done", "share_invite": "Meghรญvรกs az albumba", "sharing_page_album": "Megosztott albumok", - "sharing_page_description": "Hozzon lรฉtre megosztott albumokat, hogy megoszthasson fรฉnykรฉpeket รฉs videรณkat a hรกlรณzatรกban lรฉvล‘ emberekkel.", + "sharing_page_description": "Megosztott albumok lรฉtrehozรกsรกval fรฉnykรฉpeket รฉs videรณkatoszthatsz meg a hรกlรณzatodban lรฉvล‘ emberekkel.", "sharing_page_empty_list": "รœRES LISTA", "sharing_silver_appbar_create_shared_album": "Megosztott album lรฉtrehozรกsa", "sharing_silver_appbar_shared_links": "Shared links", - "sharing_silver_appbar_share_partner": "Megosztรกs mรกsokkal", + "sharing_silver_appbar_share_partner": "Megosztรกs partnerrel", "tab_controller_nav_library": "Kรถnyvtรกr", "tab_controller_nav_photos": "Kรฉpek", "tab_controller_nav_search": "Keresรฉs", diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json index 1d08ccf13..0d109f6d1 100644 --- a/mobile/assets/i18n/it-IT.json +++ b/mobile/assets/i18n/it-IT.json @@ -1,13 +1,13 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "Annulla", + "action_common_update": "Aggiorna", "add_to_album_bottom_sheet_added": "Aggiunto in {album}", "add_to_album_bottom_sheet_already_exists": "Giร  presente in {album}", - "advanced_settings_log_level_title": "Log level: {}", + "advanced_settings_log_level_title": "Livello log: {}", "advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.", "advanced_settings_prefer_remote_title": "Preferisci immagini remote.", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.", + "advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed", "advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti", "advanced_settings_tile_title": "Avanzato", "advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi", @@ -35,7 +35,7 @@ "app_bar_signout_dialog_title": "Disconnetti", "archive_page_no_archived_assets": "Nessuna oggetto archiviato", "archive_page_title": "Archivia ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", + "asset_action_delete_err_read_only": "Non posso eliminare degli elementi in sola lettura, ignorato", "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", "asset_list_layout_settings_dynamic_layout_title": "Layout dinamico", "asset_list_layout_settings_group_automatically": "Automatico", @@ -111,9 +111,9 @@ "cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)", "cache_settings_clear_cache_button": "Cancella cache", "cache_settings_clear_cache_button_title": "Cancella la cache dell'app. Questo impatterร  significativamente le prestazioni dell''app fino a quando la cache non sarร  rigenerata.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", + "cache_settings_duplicated_assets_clear_button": "ELIMINA", + "cache_settings_duplicated_assets_subtitle": "Foto e video che sono nella black list dell'applicazione", + "cache_settings_duplicated_assets_title": "Elementi duplicati ({})", "cache_settings_image_cache_size": "Dimensione cache delle foto ({} assets)", "cache_settings_statistics_album": "Anteprime librerie", "cache_settings_statistics_assets": "{} contenuti ({})", @@ -142,15 +142,15 @@ "control_bottom_app_bar_archive": "Archivia", "control_bottom_app_bar_create_new_album": "Crea nuovo album", "control_bottom_app_bar_delete": "Elimina", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", + "control_bottom_app_bar_delete_from_immich": "Elimina da Immich", + "control_bottom_app_bar_delete_from_local": "Elimina dal dispositivo", + "control_bottom_app_bar_edit_location": "Modifica posizione", + "control_bottom_app_bar_edit_time": "Modifica data e ora", "control_bottom_app_bar_favorite": "Preferiti", "control_bottom_app_bar_share": "Condividi", "control_bottom_app_bar_share_to": "Share To", "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Sposta nel cestino", "control_bottom_app_bar_unarchive": "Rimuovi dagli archivi", "control_bottom_app_bar_unfavorite": "Unfavorite", "control_bottom_app_bar_upload": "Upload", @@ -165,26 +165,27 @@ "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E, d LLL, y โ€ข hh:mm", "delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Questi elementi verranno eliminati definitivamente dal dispositivo, ma saranno ancora disponibili sul server Immich", + "delete_dialog_alert_local_non_backed_up": "Alcuni degli elementi non sono stati caricati su Immich e saranno rimossi definitivamente dal tuo dispositivo", + "delete_dialog_alert_remote": "Questi elementi verranno eliminati permanentemente dal server Immich", "delete_dialog_cancel": "Annulla", "delete_dialog_ok": "Elimina", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Elimina comunque", "delete_dialog_title": "Cancella definitivamente", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Elimina solo quelli con backup", + "delete_local_dialog_ok_force": "Elimina comunque", "delete_shared_link_dialog_content": "Sei sicuro di voler eliminare questo link condiviso?", "delete_shared_link_dialog_title": "Elimina link condiviso", "description_input_hint_text": "Aggiungi descrizione...", "description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "edit_date_time_dialog_date_time": "Data e ora", + "edit_date_time_dialog_timezone": "Fuso orario", + "edit_location_dialog_title": "Posizione", "exif_bottom_sheet_description": "Aggiungi una descrizione...", "exif_bottom_sheet_details": "DETTAGLI", "exif_bottom_sheet_location": "POSIZIONE", - "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_location_add": "Aggiungi una posizione", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Attiva griglia di foto sperimentale", "experimental_settings_subtitle": "Usalo a tuo rischio!", @@ -199,7 +200,7 @@ "home_page_archive_err_partner": "Can not archive partner assets, skipping", "home_page_building_timeline": "Costruendo il Timeline", "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_remote_err_local": "Immagini sul disco locale presenti pure nella selezione degli elementi remoti, skippando", "home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate", "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", "home_page_first_time_notice": "Se รจ la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video", @@ -214,22 +215,22 @@ "library_page_favorites": "Preferiti", "library_page_new_album": "Nuovo Album", "library_page_sharing": "Condividendo", - "library_page_sort_asset_count": "Number of assets", + "library_page_sort_asset_count": "Numero di elementi", "library_page_sort_created": "Creato il piรน recente", "library_page_sort_last_modified": "Ultima modifica", - "library_page_sort_most_oldest_photo": "Oldest photo", + "library_page_sort_most_oldest_photo": "Foto piรน vecchia", "library_page_sort_most_recent_photo": "Piรน recente", "library_page_sort_title": "Titolo album", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "location_picker_choose_on_map": "Scegli una mappa", + "location_picker_latitude": "Latitudine", + "location_picker_latitude_error": "Inserisci una latitudine valida", + "location_picker_latitude_hint": "Inserisci la tua latitudine qui", + "location_picker_longitude": "Longitudine", + "location_picker_longitude_error": "Inserisci una longitudine valida", + "location_picker_longitude_hint": "Inserisci la longitudine qui", "login_disabled": "L'accesso รจ stato disattivato", "login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi", - "login_form_back_button_text": "Back", + "login_form_back_button_text": "Indietro", "login_form_button_text": "Login", "login_form_email_hint": "tuaemail@email.com", "login_form_endpoint_hint": "http://ip-del-tuo-server:port/api", @@ -252,35 +253,35 @@ "login_form_server_error": "Non รจ possibile connettersi al server", "login_password_changed_error": "C'รจ stato un errore durante l'aggiornamento della password", "login_password_changed_success": "Password aggiornata con successo", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bound": "{} foto", + "map_assets_in_bounds": "{} foto", "map_cannot_get_user_location": "Cannot get user's location", "map_location_dialog_cancel": "Annulla", "map_location_dialog_yes": "Si", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", + "map_location_picker_page_use_location": "Usa questa posizione", + "map_location_service_disabled_content": "I servizi di geolocalizzazione devono essere attivati per visualizzare gli elementi per la tua posizione attuale. Vuoi attivarli adesso?", "map_location_service_disabled_title": "Location Service disabled", "map_no_assets_in_bounds": "Nessuna foto in questa zona", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", + "map_no_location_permission_content": "L'accesso alla posizione รจ necessario per visualizzare gli elementi per la tua posizione attuale. Vuoi consentirlo adesso?", "map_no_location_permission_title": "Location Permission denied", "map_settings_dark_mode": "Modalitร  scura", "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_day": "Ultime 24 ore", + "map_settings_date_range_option_days": "Ultimi {} giorni", + "map_settings_date_range_option_year": "Ultimo anno", + "map_settings_date_range_option_years": "Ultimi {} anni", "map_settings_dialog_cancel": "Cancel", "map_settings_dialog_save": "Salva", "map_settings_dialog_title": "Map Settings", "map_settings_include_show_archived": "Include Archived", "map_settings_only_relative_range": "Date range", "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Tema della mappa", "map_zoom_to_see_photos": "Zoom out to see photos", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Motion Foto", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "Non posso modificare la data degli elementi in sola lettura, ignorato", + "multiselect_grid_edit_gps_err_read_only": "Non posso modificare la posizione degli elementi in sola lettura, ignorato", "notification_permission_dialog_cancel": "Annulla", "notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi", "notification_permission_dialog_settings": "Impostazioni", @@ -307,18 +308,18 @@ "permission_onboarding_permission_limited": "Permessi limitati. Perchรฉ Immich possa controllare e fare i backup di tutte le foto, concedere i permessi all'intera galleria dalle impostazioni ", "permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video", "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_out_of_date_major": "L'applicazione non รจ aggiornata. Per favore aggiorna all'ultima versione principale.", + "profile_drawer_client_out_of_date_minor": "L'applicazione non รจ aggiornata. Per favore aggiorna all'ultima versione minore.", "profile_drawer_client_server_up_to_date": "Client e server sono aggiornati", "profile_drawer_documentation": "Documentazione", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_server_out_of_date_major": "Il server non รจ aggiornato. Per favore aggiorna all'ultima versione principale.", + "profile_drawer_server_out_of_date_minor": "Il server non รจ aggiornato. Per favore aggiorna all'ultima versione minore.", "profile_drawer_settings": "Impostazioni ", "profile_drawer_sign_out": "Logout", "profile_drawer_trash": "Trash", "recently_added_page_title": "Aggiunti di recente", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "Si รจ verificato un errore.", "search_bar_hint": "Cerca le tue foto", "search_page_categories": "Categoria", "search_page_favorites": "Preferiti", @@ -326,13 +327,13 @@ "search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile", "search_page_no_places": "Nessun informazione sul luogo disponibile", "search_page_people": "Persone", - "search_page_person_add_name_dialog_cancel": "Cancel", - "search_page_person_add_name_dialog_hint": "Name", - "search_page_person_add_name_dialog_save": "Save", - "search_page_person_add_name_dialog_title": "Add a name", + "search_page_person_add_name_dialog_cancel": "Annulla", + "search_page_person_add_name_dialog_hint": "Nome", + "search_page_person_add_name_dialog_save": "Salva", + "search_page_person_add_name_dialog_title": "Aggiungi un nome", "search_page_person_add_name_subtitle": "Find them fast by name with search", - "search_page_person_add_name_title": "Add a name", - "search_page_person_edit_name": "Edit name", + "search_page_person_add_name_title": "Aggiungi un nome", + "search_page_person_edit_name": "Modifica nome", "search_page_places": "Luoghi", "search_page_recently_added": "Aggiunte di recente", "search_page_screenshots": "Screenshot", @@ -341,7 +342,7 @@ "search_page_videos": "Video", "search_page_view_all_button": "Guarda tutto", "search_page_your_activity": "Tua attivitร  ", - "search_page_your_map": "Your Map", + "search_page_your_map": "La tua mappa", "search_result_page_new_search_hint": "Nuova ricerca ", "search_suggestion_list_smart_search_hint_1": "\nRicerca Smart รจ attiva di default, per usare la ricerca con i metadata usare la seguente sintassi", "search_suggestion_list_smart_search_hint_2": "m:your-search-term", @@ -381,17 +382,17 @@ "shared_album_activity_remove_title": "Elimina attivitร ", "shared_album_activity_setting_subtitle": "Let others respond", "shared_album_activity_setting_title": "Commenti e Mi piace", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_owner_label": "Owner", - "shared_album_section_people_title": "PEOPLE", + "shared_album_section_people_action_error": "Errore durante la rimozione/uscita dell'album", + "shared_album_section_people_action_leave": "Rimuovi utente dall'album", + "shared_album_section_people_action_remove_user": "Rimuovi utente dall'album", + "shared_album_section_people_owner_label": "Proprietario", + "shared_album_section_people_title": "PERSONE", "share_dialog_preparing": "Preparoโ€ฆ", "shared_link_app_bar_title": "Link condivisi", - "shared_link_clipboard_copied_massage": "Copied to clipboard", + "shared_link_clipboard_copied_massage": "Copiato negli appunti", "shared_link_clipboard_text": "Link: {}\nPassword: {}", "shared_link_create_app_bar_title": "Crea link di condivisione", - "shared_link_create_error": "Error while creating shared link", + "shared_link_create_error": "Si รจ verificato un errore durante la creazione del link condiviso", "shared_link_create_info": "Consenti a chiunque abbia il link di vedere le foto selezionate", "shared_link_create_submit_button": "Crea link di condivisione", "shared_link_edit_allow_download": "Consenti ad utenti pubblici di scaricare i contenuti", @@ -401,32 +402,32 @@ "shared_link_edit_description": "Descrizione", "shared_link_edit_description_hint": "Inserisci la descrizione della condivisione", "shared_link_edit_expire_after": "Scade dopo", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_expire_after_option_day": "1 giorno", + "shared_link_edit_expire_after_option_days": "{} giorni", + "shared_link_edit_expire_after_option_hour": "1 ora", + "shared_link_edit_expire_after_option_hours": "{} ore", + "shared_link_edit_expire_after_option_minute": "1 minuto", + "shared_link_edit_expire_after_option_minutes": "{} minuti", + "shared_link_edit_expire_after_option_never": "Mai", "shared_link_edit_password": "Password", "shared_link_edit_password_hint": "Inserire la password di condivisione", "shared_link_edit_show_meta": "Visualizza metadati", "shared_link_edit_submit_button": "Aggiorna link", "shared_link_empty": "Non hai alcun link condiviso", "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_info_chip_download": "Download", + "shared_link_expired": "Scaduto", + "shared_link_expires_day": "Scade tra {} giorno", + "shared_link_expires_days": "Scade tra {} giorni", + "shared_link_expires_hour": "Scade tra {} ora", + "shared_link_expires_hours": "Scade tra {} ore", + "shared_link_expires_minute": "Scade tra {} minuto", + "shared_link_expires_minutes": "Scade tra {} minuti", + "shared_link_expires_never": "Scadenza โˆž", + "shared_link_expires_second": "Scade tra {} secondo", + "shared_link_expires_seconds": "Scade tra {} secondi", + "shared_link_info_chip_download": "Scarica", "shared_link_info_chip_metadata": "EXIF", - "shared_link_info_chip_upload": "Upload", + "shared_link_info_chip_upload": "Carica", "shared_link_manage_links": "Gestisci link condivisi", "share_done": "Done", "share_invite": "Invita nell'album ", @@ -454,7 +455,7 @@ "trash_page_delete": "Elimina", "trash_page_delete_all": "Elimina tutti", "trash_page_empty_trash_btn": "Svuota cestino", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", + "trash_page_empty_trash_dialog_content": "Vuoi eliminare gli elementi nel cestino? Questi elementi saranno eliminati definitivamente da Immich", "trash_page_empty_trash_dialog_ok": "Ok", "trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {} giorni", "trash_page_no_assets": "Nessun elemento cestinato", diff --git a/mobile/assets/i18n/ja-JP.json b/mobile/assets/i18n/ja-JP.json index dee773843..b4b686c74 100644 --- a/mobile/assets/i18n/ja-JP.json +++ b/mobile/assets/i18n/ja-JP.json @@ -1,13 +1,13 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", + "action_common_update": "ๆ›ดๆ–ฐ", "add_to_album_bottom_sheet_added": "{album}ใซ่ฟฝๅŠ ", "add_to_album_bottom_sheet_already_exists": "{album}ใซ่ฟฝๅŠ ๆธˆใฟ", - "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", - "advanced_settings_prefer_remote_title": "Prefer remote images", - "advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.", - "advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates", + "advanced_settings_log_level_title": "ใƒญใ‚ฐใƒฌใƒ™ใƒซ: {}", + "advanced_settings_prefer_remote_subtitle": "็ซฏๆœซใซใ‚ˆใฃใฆใฏ็ซฏๆœซไธŠใซๅญ˜ๅœจใ™ใ‚‹ใ‚ตใƒ ใƒใ‚คใƒซใฎใƒญใƒผใƒ‰ใซ้žๅธธใซๆ™‚้–“ใŒใ‹ใ‹ใ‚Šใพใ™ใ€‚ใ“ใฎใ‚ชใƒ—ใ‚ทใƒงใƒณใ‚’ใซๆœ‰ๅŠนใซใ™ใ‚‹ไบ‹ใซใ‚ˆใฃใฆใ‚ตใƒผใƒใƒผใ‹ใ‚‰็›ดๆŽฅ็”ปๅƒใ‚’ใƒญใƒผใƒ‰ใ™ใ‚‹ใ“ใจใŒๅฏ่ƒฝใงใ™", + "advanced_settings_prefer_remote_title": "ใƒชใƒขใƒผใƒˆใ‚’ๅ„ชๅ…ˆใ™ใ‚‹", + "advanced_settings_self_signed_ssl_subtitle": "SSLใฎใƒใ‚งใƒƒใ‚ฏใ‚’ใ‚นใ‚ญใƒƒใƒ—ใ™ใ‚‹ใ€‚Self-signedใช็ฝฒๅใงๅฟ…่ฆใงใ™", + "advanced_settings_self_signed_ssl_title": "Self-signed็ฝฒๅใ‚’่จฑๅฏใ™ใ‚‹", "advanced_settings_tile_subtitle": "่ฟฝๅŠ ใƒฆใƒผใ‚ถใƒผ่จญๅฎš", "advanced_settings_tile_title": "่ฉณ็ดฐ่จญๅฎš", "advanced_settings_troubleshooting_subtitle": "ใƒˆใƒฉใƒ–ใƒซใ‚ทใƒฅใƒผใƒ†ใ‚ฃใƒณใ‚ฐ็”จใฎ่ฉณ็ดฐ่จญๅฎšใ‚’ใ‚ชใƒณใซใ™ใ‚‹", @@ -26,17 +26,17 @@ "album_viewer_appbar_share_err_title": "ใ‚ฟใ‚คใƒˆใƒซๅค‰ๆ›ดใฎๅคฑๆ•—", "album_viewer_appbar_share_leave": "ใ‚ขใƒซใƒใƒ ใ‹ใ‚‰่„ฑ้€€", "album_viewer_appbar_share_remove": "ใ‚ขใƒซใƒใƒ ใ‹ใ‚‰ๅ‰Š้™ค", - "album_viewer_appbar_share_to": "Share To", + "album_viewer_appbar_share_to": "ๆฌกใฎๆ–นใ€…ใจๅ…ฑๆœ‰ใ—ใพใ™", "album_viewer_page_share_add_users": "ใƒฆใƒผใ‚ถใƒผใ‚’่ฟฝๅŠ ", - "all_people_page_title": "People", + "all_people_page_title": "ใƒ”ใƒผใƒ—ใƒซ", "all_videos_page_title": "ใƒ“ใƒ‡ใ‚ช", "app_bar_signout_dialog_content": " ใ‚ตใ‚คใƒณใ‚ขใ‚ฆใƒˆใ—ใพใ™ใ‹?", "app_bar_signout_dialog_ok": "ใฏใ„", "app_bar_signout_dialog_title": " ใ‚ตใ‚คใƒณใ‚ขใ‚ฆใƒˆ", "archive_page_no_archived_assets": "ใ‚ขใƒผใ‚ซใ‚คใƒ–ๆธˆใฟใฎๅ†™็œŸใพใŸใฏใƒ“ใƒ‡ใ‚ชใŒใ‚ใ‚Šใพใ›ใ‚“", "archive_page_title": "ใ‚ขใƒผใ‚ซใ‚คใƒ–({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "่ชญใฟๅ–ใ‚Šๅฐ‚็”จใฎ้ …็›ฎใฏๅ‰Š้™คใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", + "asset_action_share_err_offline": "ใ‚ชใƒ•ใƒฉใ‚คใƒณใฎ้ …็›ฎใ‚’ใ‚ฒใƒƒใƒˆใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", "asset_list_layout_settings_dynamic_layout_title": "ใƒ€ใ‚คใƒŠใƒŸใƒƒใ‚ฏใƒฌใ‚คใ‚ขใ‚ฆใƒˆ", "asset_list_layout_settings_group_automatically": "่‡ชๅ‹•", "asset_list_layout_settings_group_by": "ๅ†™็œŸใฎใ‚ฐใƒซใƒผใƒ—ๅˆ†ใ‘", @@ -103,17 +103,17 @@ "backup_controller_page_uploading_file_info": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ไธญใฎใƒ•ใ‚กใ‚คใƒซ", "backup_err_only_album": "ๆœ€ไฝŽ1ใคใฎใ‚ขใƒซใƒใƒ ใ‚’้ธๆŠžใ—ใฆใใ ใ•ใ„", "backup_info_card_assets": "ๅ†™็œŸใจๅ‹•็”ป", - "backup_manual_cancelled": "Cancelled", - "backup_manual_failed": "Failed", - "backup_manual_in_progress": "Upload already in progress. Try after sometime", - "backup_manual_success": "Success", - "backup_manual_title": "Upload status", + "backup_manual_cancelled": "ใ‚ญใƒฃใƒณใ‚ปใƒซใ•ใ‚Œใพใ—ใŸ", + "backup_manual_failed": "ๅคฑๆ•—", + "backup_manual_in_progress": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใŒ้€ฒ่กŒไธญใงใ™ใ€‚ๅพŒใงใ‚‚ใ†ไธ€ๅบฆ่ฉฆใ—ใฆใใ ใ•ใ„", + "backup_manual_success": "ๆˆๅŠŸ", + "backup_manual_title": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰็Šถๆณ", "cache_settings_album_thumbnails": "ใƒฉใ‚คใƒ–ใƒฉใƒชใฎใ‚ตใƒ ใƒใ‚คใƒซ ({}ๆžš)", "cache_settings_clear_cache_button": "ใ‚ญใƒฃใƒƒใ‚ทใƒฅใ‚’ใ‚ฏใƒชใ‚ข", "cache_settings_clear_cache_button_title": "ใ‚ญใƒฃใƒƒใ‚ทใƒฅใ‚’ๅ‰Š้™ค๏ผˆใ‚ญใƒฃใƒƒใ‚ทใƒฅๅ†็”Ÿๆˆใพใงใ‚ขใƒ—ใƒชใฎใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚นใŒ่‘—ใ—ใไฝŽไธ‹๏ผ‰", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", + "cache_settings_duplicated_assets_clear_button": "ใ‚ฏใƒชใ‚ข", + "cache_settings_duplicated_assets_subtitle": "ใ‚ขใƒ—ใƒชใŒใƒ–ใƒฉใƒƒใ‚ฏใƒชใ‚นใƒˆใซ่ฟฝๅŠ ใ—ใฆใ„ใ‚‹้ …็›ฎ", + "cache_settings_duplicated_assets_title": "{}้ …็›ฎใŒ้‡่ค‡", "cache_settings_image_cache_size": "ใ‚ญใƒฃใƒƒใ‚ทใƒฅใฎใ‚ตใ‚คใ‚บ ({}ๆžš) ", "cache_settings_statistics_album": "ใƒฉใ‚คใƒ–ใƒฉใƒชใฎใ‚ตใƒ ใƒใ‚คใƒซ", "cache_settings_statistics_assets": "{} ๆžš ({}ๆžšไธญ)", @@ -123,8 +123,8 @@ "cache_settings_statistics_title": "ใ‚ญใƒฃใƒƒใ‚ทใƒฅ", "cache_settings_subtitle": "ใ‚ญใƒฃใƒƒใ‚ทใƒฅใฎๅ‹•ไฝœใ‚’ๅค‰ๆ›ดใ™ใ‚‹", "cache_settings_thumbnail_size": "ใ‚ตใƒ ใƒใ‚คใƒซใฎใ‚ญใƒฃใƒƒใ‚ทใƒฅใฎใ‚ตใ‚คใ‚บ ({}ๆžš)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", + "cache_settings_tile_subtitle": "ใƒญใƒผใ‚ซใƒซใ‚นใƒˆใƒฌใƒผใ‚ธใฎๆŒ™ๅ‹•ใ‚’็ขบ่ชใ™ใ‚‹", + "cache_settings_tile_title": "ใƒญใƒผใ‚ซใƒซใ‚นใƒˆใƒฌใƒผใ‚ธ", "cache_settings_title": "ใ‚ญใƒฃใƒƒใ‚ทใƒฅใฎ่จญๅฎš", "change_password_form_confirm_password": "็ขบๅฎš", "change_password_form_description": "{name}ใ•ใ‚“ ใ“ใ‚“ใซใกใฏ\n\nใ‚ตใƒผใƒใƒผใซใ‚ขใ‚ฏใ‚ปใ‚นใ™ใ‚‹ใฎใŒๅˆใ‚ใฆใ‹ใ€ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใƒชใ‚ปใƒƒใƒˆใฎใƒชใ‚ฏใ‚จใ‚นใƒˆใŒใ•ใ‚Œใพใ—ใŸใ€‚ๆ–ฐใ—ใ„ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", @@ -142,18 +142,18 @@ "control_bottom_app_bar_archive": "ใ‚ขใƒผใ‚ซใ‚คใƒ–", "control_bottom_app_bar_create_new_album": "ใ‚ขใƒซใƒใƒ ใ‚’ไฝœๆˆ", "control_bottom_app_bar_delete": "ๅ‰Š้™ค", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", + "control_bottom_app_bar_delete_from_immich": "Immichใ‹ใ‚‰ๅ‰Š้™ค", + "control_bottom_app_bar_delete_from_local": "็ซฏๆœซใ‹ใ‚‰ๅ‰Š้™ค", + "control_bottom_app_bar_edit_location": "ไฝ็ฝฎๆƒ…ๅ ฑใ‚’็ทจ้›†", + "control_bottom_app_bar_edit_time": "ๆ—ฅๆ™‚ใ‚’ๅค‰ๆ›ด", "control_bottom_app_bar_favorite": "ใŠๆฐ—ใซๅ…ฅใ‚Š", "control_bottom_app_bar_share": "ๅ…ฑๆœ‰", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_share_to": "ๆฌกใฎใƒฆใƒผใ‚ถใƒผใซๅ…ฑๆœ‰: ", + "control_bottom_app_bar_stack": "ใ‚นใ‚ฟใƒƒใ‚ฏ", + "control_bottom_app_bar_trash_from_immich": "ใ‚ดใƒŸ็ฎฑใซๆจใฆใ‚‹", "control_bottom_app_bar_unarchive": "ใ‚ขใƒผใ‚ซใ‚คใƒ–ใ‚’่งฃ้™ค", - "control_bottom_app_bar_unfavorite": "Unfavorite", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_unfavorite": "ใŠๆฐ—ใซๅ…ฅใ‚Šใ‹ใ‚‰ๅค–ใ™", + "control_bottom_app_bar_upload": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰", "create_album_page_untitled": "ใ‚ฟใ‚คใƒˆใƒซใชใ—", "create_shared_album_page_create": "ไฝœๆˆ", "create_shared_album_page_share": "ๅ…ฑๆœ‰", @@ -165,26 +165,27 @@ "daily_title_text_date_year": "yyyyๅนด MMๆœˆ DDๆ—ฅ, EE", "date_format": "MMๆœˆ DDๆ—ฅ, EE โ€ข hhๆ™‚mmๅˆ†", "delete_dialog_alert": "ใ‚ตใƒผใƒใƒผใจใƒ‡ใƒใ‚คใ‚นใฎไธกๆ–นใ‹ใ‚‰ๆฐธไน…็š„ใซๅ‰Š้™คใ•ใ‚Œใพใ™๏ผ", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "้ธๆŠžใ•ใ‚ŒใŸ้ …็›ฎใฏ็ซฏๆœซใ‹ใ‚‰ๅ‰Š้™คใ•ใ‚Œใพใ™ใŒImmichใซใฏๆฎ‹ใ‚Šใพใ™", + "delete_dialog_alert_local_non_backed_up": "Immichใซใƒใƒƒใ‚ฏใ‚ขใƒƒใƒ—ใ•ใ‚Œใฆใ„ใชใ„้ …็›ฎใŒใ‚ใ‚Šใพใ™ใ€‚ใใ‚Œใ‚‰ใฎ้ …็›ฎใฏใƒ‡ใƒใ‚คใ‚นใ‹ใ‚‰ใ‚‚ๆฐธไน…ใซๅ‰Š้™คใ•ใ‚Œใพใ™", + "delete_dialog_alert_remote": "้ธๆŠžใ•ใ‚ŒใŸ้ …็›ฎใฏImmichใ‹ใ‚‰ๆฐธไน…ใซๅ‰Š้™คใ•ใ‚Œใพใ™", "delete_dialog_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", "delete_dialog_ok": "ๅ‰Š้™ค", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "ๅ‰Š้™คใ—ใพใ™", "delete_dialog_title": "ๆฐธไน…็š„ใซๅ‰Š้™ค", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "ใƒใƒƒใ‚ฏใ‚ขใƒƒใƒ—ๆธˆใฟใฎใฟใ‚’ๅ‰Š้™ค", + "delete_local_dialog_ok_force": "ๅ‰Š้™คใ—ใพใ™", "delete_shared_link_dialog_content": "ๆœฌๅฝ“ใซใ“ใฎๅ…ฑๆœ‰ใƒชใƒณใ‚ฏใ‚’ๆถˆใ—ใพใ™ใ‹?", "delete_shared_link_dialog_title": "ๅ…ฑๆœ‰ใƒชใƒณใ‚ฏใ‚’ๆถˆใ™", "description_input_hint_text": "่ชฌๆ˜Žใ‚’่ฟฝๅŠ ", "description_input_submit_error": "่ชฌๆ˜Žใฎ็ทจ้›†ใซๅคฑๆ•—ใ€่ฉณ็ดฐใฎ็ขบ่ชใฏใƒญใ‚ฐใง่กŒใฃใฆใใ ใ•ใ„", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "edit_date_time_dialog_date_time": "ๆ—ฅไป˜ใจๆ™‚้–“", + "edit_date_time_dialog_timezone": "ใ‚ฟใ‚คใƒ ใ‚พใƒผใƒณ", + "edit_location_dialog_title": "ไฝ็ฝฎๆƒ…ๅ ฑ", "exif_bottom_sheet_description": "่ชฌๆ˜Žใ‚’่ฟฝๅŠ ", "exif_bottom_sheet_details": "่ฉณ็ดฐ", "exif_bottom_sheet_location": "ๆ’ฎๅฝฑๅ ดๆ‰€", - "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_location_add": "ไฝ็ฝฎๆƒ…ๅ ฑใ‚’่ฟฝๅŠ ", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "่ฃฝไฝœ้€”ไธญ(WIP)", "experimental_settings_new_asset_list_title": "่ฉฆ้จ“็š„ใชใ‚ฐใƒชใƒƒใƒ‰ใ‚’ๆœ‰ๅŠนๅŒ–", "experimental_settings_subtitle": "่ฉฆ้จ“็š„ๆฉŸ่ƒฝใซใคใ่‡ชๅทฑ่ฒฌไปปใง๏ผ", @@ -194,42 +195,42 @@ "home_page_add_to_album_conflicts": "{album}ใซ{added}ๆžšๅ†™็œŸใ‚’่ฟฝๅŠ ใ—ใพใ—ใŸใ€‚่ฟฝๅŠ ๆธˆใฟใฎ{failed}ๆžšใฏใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ—ใŸใ€‚", "home_page_add_to_album_err_local": "ใพใ ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ•ใ‚Œใฆใชใ„้ …็›ฎใฏใ‚ขใƒซใƒใƒ ใซ็™ป้Œฒใงใใพใ›ใ‚“", "home_page_add_to_album_success": "{album}ใซ{added}ๆžšๅ†™็œŸใ‚’่ฟฝๅŠ ใ—ใพใ—ใŸ", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", + "home_page_album_err_partner": "ใพใ ใƒ‘ใƒผใƒˆใƒŠใƒผใฎๅ†™็œŸใฏใ‚ขใƒซใƒใƒ ใซ่ฟฝๅŠ ใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™(ใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆๅพ…ใฃใฆใญ)", "home_page_archive_err_local": "ใพใ ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ•ใ‚Œใฆใชใ„้ …็›ฎใฏใ‚ขใƒผใ‚ซใ‚คใƒ–ใงใใพใ›ใ‚“", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", + "home_page_archive_err_partner": "ใƒ‘ใƒผใƒˆใƒŠใƒผใฎๅ†™็œŸใฏใ‚ขใƒผใ‚ซใ‚คใƒ–ใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", "home_page_building_timeline": "ใ‚ฟใ‚คใƒ ใƒฉใ‚คใƒณๆง‹็ฏ‰ไธญ", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_err_partner": "ใƒ‘ใƒผใƒˆใƒŠใƒผใฎๅ†™็œŸใฏๅ‰Š้™คใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", + "home_page_delete_remote_err_local": "ใƒชใƒขใƒผใƒˆๅ‰Š้™คใฎ้ธๆŠžใซใƒญใƒผใ‚ซใƒซใชใ‚ขใ‚คใƒ†ใƒ ใŒๅซใพใ‚Œใฆใ„ใพใ™ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", "home_page_favorite_err_local": "ใพใ ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ•ใ‚Œใฆใชใ„้ …็›ฎใฏใŠๆฐ—ใซๅ…ฅใ‚Š็™ป้Œฒใงใใพใ›ใ‚“", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", + "home_page_favorite_err_partner": "ใพใ ใƒ‘ใƒผใƒˆใƒŠใƒผใฎๅ†™็œŸใ‚’ใŠๆฐ—ใซๅ…ฅใ‚Š็™ป้Œฒใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™(ใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆๅพ…ใฃใฆใญ)", "home_page_first_time_notice": "ใฏใ˜ใ‚ใฆใ‚ขใƒ—ใƒชใ‚’ไฝฟใ†ๅ ดๅˆใ€ใ‚ฟใ‚คใƒ ใƒฉใ‚คใƒณใซๅ†™็œŸใ‚’่กจ็คบใ™ใ‚‹ใŸใ‚ใซใ‚ขใƒซใƒใƒ ใ‚’้ธๆŠžใ—ใฆใใ ใ•ใ„", - "home_page_share_err_local": "Can not share local assets via link, skipping", - "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_share_err_local": "ใƒญใƒผใ‚ซใƒซใฎใฟใฎ้ …็›ฎใ‚’ใƒชใƒณใ‚ฏใงๅ…ฑๆœ‰ใฏใงใใพใ›ใ‚“ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", + "home_page_upload_err_limit": "ไธ€ๅ›žใงใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใงใใ‚‹ๅ†™็œŸใฎๆ•ฐใฏ30ๆžšใงใ™ใ€‚ใ‚นใ‚ญใƒƒใƒ—ใ—ใพใ™", "image_viewer_page_state_provider_download_error": "ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ๅคฑๆ•—", "image_viewer_page_state_provider_download_success": "ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ๆˆๅŠŸ", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "ๅ…ฑๆœ‰ใ‚จใƒฉใƒผ", "library_page_albums": "ใ‚ขใƒซใƒใƒ ", "library_page_archive": "ใ‚ขใƒผใ‚ซใ‚คใƒ–", "library_page_device_albums": "ใƒ‡ใƒใ‚คใ‚นไธŠใฎใ‚ขใƒซใƒใƒ ", "library_page_favorites": "ใŠๆฐ—ใซๅ…ฅใ‚Š", "library_page_new_album": "ๆ–ฐใ—ใ„ใ‚ขใƒซใƒใƒ ", "library_page_sharing": "ๅ…ฑๆœ‰ไธญ", - "library_page_sort_asset_count": "Number of assets", + "library_page_sort_asset_count": "้ …็›ฎใฎๆ•ฐ", "library_page_sort_created": "ไฝœๆˆๆ—ฅๆ™‚", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_oldest_photo": "Oldest photo", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "ๆœ€็ต‚ๅค‰ๆ›ด", + "library_page_sort_most_oldest_photo": "ไธ€็•ชๅคใ„้ …็›ฎ", + "library_page_sort_most_recent_photo": "ๆœ€่ฟ‘ใฎ้ …็›ฎ", "library_page_sort_title": "ใ‚ขใƒซใƒใƒ ๅ", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", - "login_disabled": "Login has been disabled", + "location_picker_choose_on_map": "ใƒžใƒƒใƒ—ใ‚’้ธๆŠž", + "location_picker_latitude": "็ทฏๅบฆ", + "location_picker_latitude_error": "ๆœ‰ๅŠนใช็ทฏๅบฆใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", + "location_picker_latitude_hint": "็ทฏๅบฆใ‚’ใ“ใ“ใซๅ…ฅๅŠ›", + "location_picker_longitude": "็ตŒๅบฆ", + "location_picker_longitude_error": "ๆœ‰ๅŠนใช็ตŒๅบฆใ‚’ๅ…ฅๅŠ›ใ—ใฆใใ ใ•ใ„", + "location_picker_longitude_hint": "็ตŒๅบฆใ‚’ใ“ใ“ใซๅ…ฅๅŠ›", + "login_disabled": "ใƒญใ‚ฐใ‚คใƒณใฏ็„กๅŠนๅŒ–ใ•ใ‚Œใพใ—ใŸ", "login_form_api_exception": "APIใ‚จใƒฉใƒผใ€‚URLใ‚’ใƒใ‚งใƒƒใ‚ฏใ—ใฆใ‚‚ใ†ไธ€ๅบฆ่ฉฆใ—ใฆใใ ใ•ใ„", - "login_form_back_button_text": "Back", + "login_form_back_button_text": "ๆˆปใ‚‹", "login_form_button_text": "ใƒญใ‚ฐใ‚คใƒณ", "login_form_email_hint": "hoge@email.com", "login_form_endpoint_hint": "https://example.com:port/api", @@ -242,7 +243,7 @@ "login_form_failed_get_oauth_server_config": "OAuthใƒญใ‚ฐใ‚คใƒณใซๅคฑๆ•—ใ—ใพใ—ใŸใ€‚ใ‚ตใƒผใƒใƒผใฎURLใ‚’็ขบ่ชใ—ใฆใใ ใ•ใ„ใ€‚", "login_form_failed_get_oauth_server_disable": "ใ“ใฎใ‚ตใƒผใƒใƒผใงใฏOAuthใŒไฝฟใˆใพใ›ใ‚“", "login_form_failed_login": "ใƒญใ‚ฐใ‚คใƒณใ‚จใƒฉใƒผใ€‚ใ‚ตใƒผใƒใƒผใฎURLใƒปใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚นใƒปใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’ๅ†็ขบ่ชใ—ใฆใใ ใ•ใ„ใ€‚", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "Handshake Exceptionใ‚จใƒฉใƒผใ€‚self-signed็ฝฒๅใ‚’่จญๅฎšใงๆœ‰ๅŠนใซใ—ใฆใใ ใ•ใ„", "login_form_label_email": "ใƒกใƒผใƒซใ‚ขใƒ‰ใƒฌใ‚น", "login_form_label_password": "ใƒ‘ใ‚นใƒฏใƒผใƒ‰", "login_form_next_button": "ๆฌก", @@ -250,53 +251,53 @@ "login_form_save_login": "ใƒญใ‚ฐใ‚คใƒณใ‚’ไฟๆŒ", "login_form_server_empty": "URLใ‚’ๅ…ฅๅŠ›", "login_form_server_error": "ใ‚ตใƒผใƒใƒผใซๆŽฅ็ถšใงใใพใ›ใ‚“", - "login_password_changed_error": "There was an error updating your password", - "login_password_changed_success": "Password updated successfully", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "Cannot get user's location", - "map_location_dialog_cancel": "Cancel", - "map_location_dialog_yes": "Yes", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?", - "map_location_service_disabled_title": "Location Service disabled", - "map_no_assets_in_bounds": "No photos in this area", - "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", - "map_no_location_permission_title": "Location Permission denied", - "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", - "map_settings_dialog_cancel": "Cancel", - "map_settings_dialog_save": "Save", - "map_settings_dialog_title": "Map Settings", - "map_settings_include_show_archived": "Include Archived", + "login_password_changed_error": "ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใฎๅค‰ๆ›ดใงใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸ", + "login_password_changed_success": "ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใฎๅค‰ๆ›ดใซๆˆๅŠŸ", + "map_assets_in_bound": "{}้ …็›ฎ", + "map_assets_in_bounds": "{}้ …็›ฎ", + "map_cannot_get_user_location": "ไฝ็ฝฎๆƒ…ๅ ฑใŒใ‚ฒใƒƒใƒˆใงใใพใ›ใ‚“", + "map_location_dialog_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", + "map_location_dialog_yes": "ใฏใ„", + "map_location_picker_page_use_location": "ใ“ใฎไฝ็ฝฎๆƒ…ๅ ฑใ‚’ไฝฟใ†", + "map_location_service_disabled_content": "็พๅœจๅœฐใฎ้ …็›ฎใ‚’่กจ็คบใ™ใ‚‹ใซใฏไฝ็ฝฎๆƒ…ๅ ฑใŒใ‚ชใƒณใงใ‚ใ‚‹ๅฟ…่ฆใŒใ‚ใ‚Šใพใ™ใ€‚ๆœ‰ๅŠนๅŒ–ใ—ใพใ™ใ‹๏ผŸ", + "map_location_service_disabled_title": "ไฝ็ฝฎๆƒ…ๅ ฑใŒใ‚ชใƒ•ใงใ™", + "map_no_assets_in_bounds": "ใ“ใฎใ‚จใƒชใ‚ขใซๅ†™็œŸใฏใ‚ใ‚Šใพใ›ใ‚“", + "map_no_location_permission_content": "็พๅœจๅœฐใฎ้ …็›ฎใ‚’่กจ็คบใ™ใ‚‹ใซใฏไฝ็ฝฎๆƒ…ๅ ฑใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใŒๅฟ…่ฆใงใ™ใ€‚่จฑๅฏใ—ใพใ™ใ‹๏ผŸ", + "map_no_location_permission_title": "ไฝ็ฝฎๆƒ…ๅ ฑใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใŒๆ‹’ๅฆใ•ใ‚Œใพใ—ใŸ", + "map_settings_dark_mode": "ใƒ€ใƒผใ‚ฏใƒขใƒผใƒ‰", + "map_settings_date_range_option_all": "ๅ…จใฆ", + "map_settings_date_range_option_day": "้ŽๅŽป24ๆ™‚้–“", + "map_settings_date_range_option_days": "้ŽๅŽป{}ๆ—ฅ้–“", + "map_settings_date_range_option_year": "้ŽๅŽป1ๅนด", + "map_settings_date_range_option_years": "้ŽๅŽป{}ๅนด้–“", + "map_settings_dialog_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", + "map_settings_dialog_save": "ใ‚ปใƒผใƒ–", + "map_settings_dialog_title": "ใƒžใƒƒใƒ—ใฎ่จญๅฎš", + "map_settings_include_show_archived": "ใ‚ขใƒผใ‚ซใ‚คใƒ–ๆธˆใฟใ‚’ๅซใ‚ใ‚‹", "map_settings_only_relative_range": "Date range", - "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", - "map_zoom_to_see_photos": "Zoom out to see photos", + "map_settings_only_show_favorites": "ใŠๆฐ—ใซๅ…ฅใ‚Šใฎใฟใ‚’่กจ็คบ", + "map_settings_theme_settings": "ใƒžใƒƒใƒ—ใฎ่ฆ‹ใŸ็›ฎ", + "map_zoom_to_see_photos": "ๅ†™็œŸใ‚’่ฆ‹ใ‚‹ใซใฏใ‚บใƒผใƒ ใ‚ขใ‚ฆใƒˆ", "monthly_title_text_date_format": "yyyyๅนด MMๆœˆ", "motion_photos_page_title": "ใƒขใƒผใ‚ทใƒงใƒณใƒ•ใ‚ฉใƒˆ", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "่ชญใฟๅ–ใ‚Šๅฐ‚็”จใฎ้ …็›ฎใฎๆ—ฅไป˜ใ‚’ๅค‰ๆ›ดใงใใพใ›ใ‚“", + "multiselect_grid_edit_gps_err_read_only": "่ชญใฟๅ–ใ‚Šๅฐ‚็”จใฎ้ …็›ฎใฎไฝ็ฝฎๆƒ…ๅ ฑใ‚’ๅค‰ๆ›ดใงใใพใ›ใ‚“", "notification_permission_dialog_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", "notification_permission_dialog_content": "้€š็Ÿฅใ‚’่จฑๅฏใ™ใ‚‹ใซใฏ่จญๅฎšใ‚’้–‹ใ„ใฆใ‚ชใƒณใซใ—ใฆใใ ใ•ใ„", "notification_permission_dialog_settings": "่จญๅฎš", "notification_permission_list_tile_content": "้€š็Ÿฅใฎ่จฑๅฏ ใ‚’ใ‚ชใƒณใซใ—ใฆใใ ใ•ใ„", "notification_permission_list_tile_enable_button": "้€š็Ÿฅใ‚’ใ‚ชใƒณใซใ™ใ‚‹", "notification_permission_list_tile_title": "้€š็Ÿฅใฎ่จฑๅฏ", - "partner_page_add_partner": "Add partner", - "partner_page_empty_message": "Your photos are not yet shared with any partner.", - "partner_page_no_more_users": "No more users to add", - "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", - "partner_page_shared_to_title": "Shared to", - "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", - "partner_page_stop_sharing_title": "Stop sharing your photos?", - "partner_page_title": "Partner", - "permission_onboarding_back": "Back", + "partner_page_add_partner": "ใƒ‘ใƒผใƒˆใƒŠใƒผใ‚’่ฟฝๅŠ ", + "partner_page_empty_message": "ใพใ ใฉใฎใƒ‘ใƒผใƒˆใƒŠใƒผใจใ‚‚ๅ†™็œŸใ‚’ๅ…ฑๆœ‰ใ—ใฆใพใ›ใ‚“", + "partner_page_no_more_users": "่ฟฝๅŠ ใงใใ‚‹ใƒฆใƒผใ‚ถใƒผใŒใ‚‚ใ†ใ„ใพใ›ใ‚“", + "partner_page_partner_add_failed": "ใƒ‘ใƒผใƒˆใƒŠใƒผใฎ่ฟฝๅŠ ใซๅคฑๆ•—", + "partner_page_select_partner": "ใƒ‘ใƒผใƒˆใƒŠใƒผใ‚’้ธๆŠž", + "partner_page_shared_to_title": "ๆฌกใฎใƒฆใƒผใ‚ถใƒผใจๅ…ฑๆœ‰ใ—ใ™: ", + "partner_page_stop_sharing_content": "{}ใฏๅ†™็œŸใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใŒใงใใชใใชใ‚Šใพใ™", + "partner_page_stop_sharing_title": "ๅ†™็œŸใฎๅ…ฑๆœ‰ใ‚’็„กๅŠนๅŒ–ใ—ใพใ™ใ‹๏ผŸ", + "partner_page_title": "ใƒ‘ใƒผใƒˆใƒŠใƒผ", + "permission_onboarding_back": "ๆˆปใ‚‹", "permission_onboarding_continue_anyway": "็„ก่ฆ–ใ—ใฆ็ถš่กŒ", "permission_onboarding_get_started": "ใฏใ˜ใ‚ใ‚‹", "permission_onboarding_go_to_settings": "ใ‚ทใ‚นใƒ†ใƒ ่จญๅฎš", @@ -307,32 +308,32 @@ "permission_onboarding_permission_limited": "ๅ†™็œŸใธใฎใ‚ขใ‚ฏใ‚ปใ‚นใŒๅˆถ้™ใ•ใ‚Œใฆใ„ใพใ™ใ€‚Immichใซๅ†™็œŸใฎใƒใƒƒใ‚ฏใ‚ขใƒƒใƒ—ใจ็ฎก็†ใ‚’่กŒใ‚ใ›ใ‚‹ใซใฏใ‚ทใ‚นใƒ†ใƒ ่จญๅฎšใ‹ใ‚‰ๅ†™็œŸใจๅ‹•็”ปใฎใ‚ขใ‚ฏใ‚ปใ‚นๆจฉ้™ใ‚’ๅค‰ๆ›ดใ—ใฆใใ ใ•ใ„ใ€‚", "permission_onboarding_request": "Immichใฏๅ†™็œŸใธใฎใ‚ขใ‚ฏใ‚ปใ‚น่จฑๅฏใŒๅฟ…่ฆใงใ™", "profile_drawer_app_logs": "ใƒญใ‚ฐ", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_out_of_date_major": "ใ‚ขใƒ—ใƒชใŒๆ›ดๆ–ฐใ•ใ‚Œใฆใพใ›ใ‚“ใ€‚ๆœ€ๆ–ฐใฎใƒใƒผใ‚ธใƒงใƒณใซๆ›ดๆ–ฐใ—ใฆใใ ใ•ใ„", + "profile_drawer_client_out_of_date_minor": "ใ‚ขใƒ—ใƒชใŒๆ›ดๆ–ฐใ•ใ‚Œใฆใพใ›ใ‚“ใ€‚ๆœ€ๆ–ฐใฎใƒžใ‚คใƒŠใƒผใƒใƒผใ‚ธใƒงใƒณใซๆ›ดๆ–ฐใ—ใฆใใ ใ•ใ„", "profile_drawer_client_server_up_to_date": "ใ™ในใฆๆœ€ๆ–ฐใงใ™", - "profile_drawer_documentation": "Documentation", + "profile_drawer_documentation": "Immcihใฎ่ชฌๆ˜Žๆ›ธ", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_server_out_of_date_major": "ใ‚ตใƒผใƒใƒผใŒๆ›ดๆ–ฐใ•ใ‚Œใฆใพใ›ใ‚“ใ€‚ๆœ€ๆ–ฐใฎใƒใƒผใ‚ธใƒงใƒณใซๆ›ดๆ–ฐใ—ใฆใใ ใ•ใ„", + "profile_drawer_server_out_of_date_minor": "ใ‚ตใƒผใƒใƒผใŒๆ›ดๆ–ฐใ•ใ‚Œใฆใพใ›ใ‚“ใ€‚ๆœ€ๆ–ฐใฎใƒžใ‚คใƒŠใƒผใƒใƒผใ‚ธใƒงใƒณใซๆ›ดๆ–ฐใ—ใฆใใ ใ•ใ„", "profile_drawer_settings": "่จญๅฎš", "profile_drawer_sign_out": "ใ‚ตใ‚คใƒณใ‚ขใ‚ฆใƒˆ", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "ใ‚ดใƒŸ็ฎฑ", "recently_added_page_title": "ๆœ€่ฟ‘", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "ใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸ", "search_bar_hint": "ๅ†™็œŸใ‚’ๆคœ็ดข", "search_page_categories": "ใ‚ซใƒ†ใ‚ดใƒช", "search_page_favorites": "ใŠๆฐ—ใซๅ…ฅใ‚Š", "search_page_motion_photos": "ใƒขใƒผใ‚ทใƒงใƒณใƒ•ใ‚ฉใƒˆ", "search_page_no_objects": "่ขซๅ†™ไฝ“ใซ้–ขใ™ใ‚‹ใƒ‡ใƒผใ‚ฟใŒใชใ—", "search_page_no_places": "ๅ ดๆ‰€ใซ้–ขใ™ใ‚‹ใƒ‡ใƒผใ‚ฟใชใ—", - "search_page_people": "People", - "search_page_person_add_name_dialog_cancel": "Cancel", - "search_page_person_add_name_dialog_hint": "Name", - "search_page_person_add_name_dialog_save": "Save", - "search_page_person_add_name_dialog_title": "Add a name", - "search_page_person_add_name_subtitle": "Find them fast by name with search", - "search_page_person_add_name_title": "Add a name", - "search_page_person_edit_name": "Edit name", + "search_page_people": "ใƒ”ใƒผใƒ—ใƒซ", + "search_page_person_add_name_dialog_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", + "search_page_person_add_name_dialog_hint": "ๅๅ‰", + "search_page_person_add_name_dialog_save": "ใ‚ปใƒผใƒ–", + "search_page_person_add_name_dialog_title": "ๅๅ‰ใ‚’่ฟฝๅŠ ", + "search_page_person_add_name_subtitle": "ๅๅ‰ใงๆคœ็ดขใ—ใฆ้ซ˜้€ŸใซๆŽขใ™", + "search_page_person_add_name_title": "ๅๅ‰ใ‚’่ฟฝๅŠ ", + "search_page_person_edit_name": "ๅๅ‰ใ‚’ๅค‰ๆ›ด", "search_page_places": "ๆ’ฎๅฝฑๅœฐ", "search_page_recently_added": "ๆœ€่ฟ‘่ฟฝๅŠ ", "search_page_screenshots": "ใ‚นใ‚ฏใƒชใƒผใƒณใ‚ทใƒงใƒƒใƒˆ", @@ -341,7 +342,7 @@ "search_page_videos": "ใƒ“ใƒ‡ใ‚ช", "search_page_view_all_button": "ใ™ในใฆ่กจ็คบ", "search_page_your_activity": "ใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃ", - "search_page_your_map": "Your Map", + "search_page_your_map": "ใ‚ใชใŸใฎใƒžใƒƒใƒ—", "search_result_page_new_search_hint": "ๆคœ็ดข", "search_suggestion_list_smart_search_hint_1": "ใ‚นใƒžใƒผใƒˆๆคœ็ดขใฏใƒ‡ใƒ•ใ‚ฉใƒซใƒˆใงใ‚ชใƒณใซใชใฃใฆใ„ใพใ™ใ€‚ใƒกใ‚ฟใƒ‡ใƒผใ‚ฟใงๆคœ็ดขใ‚’่กŒใ†ๅ ดๅˆ:", "search_suggestion_list_smart_search_hint_2": "m:ๅ˜่ชž", @@ -349,7 +350,7 @@ "select_user_for_sharing_page_err_album": "ใ‚ขใƒซใƒใƒ ไฝœๆˆใซๅคฑๆ•—", "select_user_for_sharing_page_share_suggestions": "ใƒฆใƒผใ‚ถไธ€่ฆง", "server_info_box_app_version": "ใ‚ขใƒ—ใƒชVer.", - "server_info_box_latest_release": "Latest Version", + "server_info_box_latest_release": "ๆœ€ๆ–ฐใƒใƒผใ‚ธใƒงใƒณ", "server_info_box_server_url": " ใ‚ตใƒผใƒใฎURL", "server_info_box_server_version": "ใ‚ตใƒผใƒใƒผVer.", "setting_image_viewer_help": "ๅ†™็œŸใ‚’ใ‚ฟใƒƒใƒ—ใ™ใ‚‹ใจใ‚ตใƒ ใƒใ‚คใƒซใƒปไธญ็”ป่ณช(่ฆ่จญๅฎš)ใƒปใ‚ชใƒชใ‚ธใƒŠใƒซ(่ฆ่จญๅฎš)ใฎ้ †ใซ่ชญใฟ่พผใฟใพใ™", @@ -375,66 +376,66 @@ "share_add_photos": "ๅ†™็œŸใ‚’่ฟฝๅŠ ", "share_add_title": "ใ‚ฟใ‚คใƒˆใƒซใ‚’่ฟฝๅŠ ", "share_create_album": "ใ‚ขใƒซใƒใƒ ใ‚’ไฝœๆˆ", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activities_input_hint": "Say something", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_activity_setting_subtitle": "Let others respond", - "shared_album_activity_setting_title": "Comments & likes", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_owner_label": "Owner", - "shared_album_section_people_title": "PEOPLE", + "shared_album_activities_input_disable": "ใ‚ณใƒกใƒณใƒˆใฏใ‚ชใƒ•ใซใชใฃใฆใพใ™", + "shared_album_activities_input_hint": "ไฝ•ใ‹ๆ›ธใ่พผใฟใพใ—ใ‚‡ใ†", + "shared_album_activity_remove_content": "ใ“ใฎใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃใ‚’ๅ‰Š้™คใ—ใพใ™ใ‹", + "shared_album_activity_remove_title": "ใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ“ใƒ†ใ‚ฃใ‚’ๅ‰Š้™คใ—ใพใ™", + "shared_album_activity_setting_subtitle": "ไป–ใฎใƒฆใƒผใ‚ถใƒผใฎ่ฟ”ไฟกใ‚’่จฑๅฏใ™ใ‚‹", + "shared_album_activity_setting_title": "ใŠๆฐ—ใซๅ…ฅใ‚Šใจใ‚ณใƒกใƒณใƒˆ", + "shared_album_section_people_action_error": "ใ‚ขใƒซใƒใƒ ใ‹ใ‚‰ใฎ้€€ๅ‡บใซๅคฑๆ•—", + "shared_album_section_people_action_leave": "ใƒฆใƒผใ‚ถใƒผใ‚’ใ‚ขใƒซใƒใƒ ใ‹ใ‚‰้€€ๅ‡บ", + "shared_album_section_people_action_remove_user": "ใƒฆใƒผใ‚ถใƒผใ‚’ใ‚ขใƒซใƒใƒ ใ‹ใ‚‰้€€ๅ‡บ", + "shared_album_section_people_owner_label": "ใ‚ชใƒผใƒŠใƒผ", + "shared_album_section_people_title": "ใƒ”ใƒผใƒ—ใƒซ", "share_dialog_preparing": "ๆบ–ๅ‚™ไธญ", "shared_link_app_bar_title": "ๅ…ฑๆœ‰ใƒชใƒณใ‚ฏ", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_clipboard_copied_massage": "ใ‚ฏใƒชใƒƒใƒ—ใƒœใƒผใƒ‰ใซใ‚ณใƒ”ใƒผใ—ใพใ—ใŸ", + "shared_link_clipboard_text": "ใƒชใƒณใ‚ฏ: {}\nใƒ‘ใ‚นใƒฏใƒผใƒ‰: {}", "shared_link_create_app_bar_title": "ๅ…ฑๆœ‰ใƒชใƒณใ‚ฏใ‚’ไฝœใ‚‹", - "shared_link_create_error": "Error while creating shared link", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", + "shared_link_create_error": "ๅ…ฑๆœ‰็”จใฎใƒชใƒณใ‚ฏไฝœๆˆๆ™‚ใซใ‚จใƒฉใƒผใŒ็™บ็”Ÿใ—ใพใ—ใŸ", + "shared_link_create_info": "่ชฐใงใ‚‚ๅ†™็œŸใ‚’่ฆ‹ใ‚Œใ‚‹ใ‚ˆใ†ใซใ™ใ‚‹", "shared_link_create_submit_button": "ใƒชใƒณใ‚ฏใ‚’ไฝœใ‚‹", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", + "shared_link_edit_allow_download": "ๅ†™็œŸใฎใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰ใฎ่จฑๅฏ", + "shared_link_edit_allow_upload": "ๅ†™็œŸใฎใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰ใ‚’่จฑๅฏ", "shared_link_edit_app_bar_title": " ใƒชใƒณใ‚ฏใ‚’็ทจ้›†ใ™ใ‚‹", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": " ใƒ‡ใ‚นใ‚ฏใƒชใƒ—ใ‚ทใƒงใƒณ ", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_expire_after": "Expire after", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_change_expiry": "ๆœ‰ๅŠนๆœŸ้™ใ‚’ๅค‰ๆ›ด", + "shared_link_edit_description": "ๆฆ‚่ฆๆฌ„", + "shared_link_edit_description_hint": "ๆฆ‚่ฆใ‚’่ฟฝๅŠ ", + "shared_link_edit_expire_after": "ๆœ‰ๅŠนๆœŸ้™ใฏ", + "shared_link_edit_expire_after_option_day": "1ๆ—ฅ", + "shared_link_edit_expire_after_option_days": "{}ๆ—ฅ", + "shared_link_edit_expire_after_option_hour": "๏ผ‘ๆ™‚้–“", + "shared_link_edit_expire_after_option_hours": "{}ๆ™‚้–“", + "shared_link_edit_expire_after_option_minute": "1ๅˆ†", + "shared_link_edit_expire_after_option_minutes": "{}ๅˆ†", + "shared_link_edit_expire_after_option_never": "ๆœ‰ๅŠนๆœŸ้™ใชใ—", "shared_link_edit_password": " ใƒ‘ใ‚นใƒฏใƒผใƒ‰", "shared_link_edit_password_hint": "ๅ…ฑๆœ‰ใƒ‘ใ‚นใƒฏใƒผใƒ‰ใ‚’ๅ…ฅๅŠ›ใ™ใ‚‹", "shared_link_edit_show_meta": " ใƒกใ‚ฟใƒ‡ใƒผใ‚ฟใ‚’่ฆ‹ใ‚‹", "shared_link_edit_submit_button": "ใƒชใƒณใ‚ฏใ‚’ใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆใ™ใ‚‹", "shared_link_empty": "ๅ…ฑๆœ‰ใƒชใƒณใ‚ฏใฏใ‚ใ‚Šใพใ›ใ‚“ ", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_info_chip_download": "Download", + "shared_link_error_server_url_fetch": "ใ‚ตใƒผใƒใƒผใฎURLใŒใ‚ฒใƒƒใƒˆใงใใพใ›ใ‚“", + "shared_link_expired": "ๆœ‰ๅŠนๆœŸ้™ใŒๅˆ‡ใ‚Œใพใ—ใŸ", + "shared_link_expires_day": "{}ๆ—ฅ้–“ใงๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_days": "{}ๆ—ฅ้–“ใงๆœ‰ๅŠนๆœŸ้™ใŒๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_hour": "{}ๆ™‚้–“ใงๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_hours": "{}ๆ™‚้–“ใงๆœ‰ๅŠนๆœŸ้™ใŒๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_minute": "{}ๅˆ†ใงๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_minutes": "{}ๅˆ†ใงๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_never": "ๆœ‰ๅŠนๆœŸ้™ใฏใ‚ใ‚Šใพใ›ใ‚“", + "shared_link_expires_second": "{}็ง’ใงๅˆ‡ใ‚Œใพใ™", + "shared_link_expires_seconds": "{}็ง’ใงๅˆ‡ใ‚Œใพใ™", + "shared_link_info_chip_download": "ใƒ€ใ‚ฆใƒณใƒญใƒผใƒ‰", "shared_link_info_chip_metadata": "EXIF", - "shared_link_info_chip_upload": "Upload", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_info_chip_upload": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰", + "shared_link_manage_links": "ๅ…ฑๆœ‰ๆธˆใฟใฎใƒชใƒณใ‚ฏใ‚’็ฎก็†", + "share_done": "ๅฎŒไบ†", "share_invite": "ใ‚ขใƒซใƒใƒ ใซๆ‹›ๅพ…", "sharing_page_album": "ๅ…ฑๆœ‰ใ‚ขใƒซใƒใƒ ", "sharing_page_description": "ๅ…ฑๆœ‰ใ‚ขใƒซใƒใƒ ใ‚’ไฝœๆˆใ—ใฆๅŒใ˜ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏใซใ„ใ‚‹ไบบใŸใกใซๅ†™็œŸใ‚’ๅ…ฑๆœ‰", "sharing_page_empty_list": "ๅ…ฑๆœ‰ใ‚ขใƒซใƒใƒ ใชใ—", "sharing_silver_appbar_create_shared_album": "ๅ…ฑๆœ‰ใ‚ขใƒซใƒใƒ ใ‚’ไฝœๆˆ", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "ๅ…ฑๆœ‰ใƒชใƒณใ‚ฏ", "sharing_silver_appbar_share_partner": "ใƒ‘ใƒผใƒˆใƒŠใƒผใจๅ…ฑๆœ‰", "tab_controller_nav_library": "ใƒฉใ‚คใƒ–ใƒฉใƒช", "tab_controller_nav_photos": "ๅ†™็œŸ", @@ -450,30 +451,30 @@ "theme_setting_theme_title": "ใƒ†ใƒผใƒž", "theme_setting_three_stage_loading_subtitle": "ไธ‰ๆฎต้šŽ่ชญใฟ่พผใฟใ‚’ๆœ‰ๅŠนใซใ™ใ‚‹ใจใƒ‘ใƒ•ใ‚ฉใƒผใƒžใƒณใ‚นใŒๆ”นๅ–„ใ™ใ‚‹ๅฏ่ƒฝๆ€งใŒใ‚ใ‚Šใพใ™ใŒใ€ใƒใƒƒใƒˆใƒฏใƒผใ‚ฏ่ฒ ่ทใŒ่‘—ใ—ใๅข—ๅŠ ใ—ใพใ™", "theme_setting_three_stage_loading_title": "ไธ‰ๆฎต้šŽ่ชญใฟ่พผใฟใ‚’ใ‚ชใƒณใซใ™ใ‚‹", - "translated_text_options": "Options", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", - "upload_dialog_cancel": "Cancel", - "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", - "upload_dialog_ok": "Upload", - "upload_dialog_title": "Upload Asset", + "translated_text_options": "ใ‚ชใƒ—ใ‚ทใƒงใƒณ", + "trash_page_delete": "ๅ‰Š้™ค", + "trash_page_delete_all": "ๅ…จใฆๅ‰Š้™ค", + "trash_page_empty_trash_btn": "ใ‚ณใƒŸ็ฎฑใ‚’็ฉบใซใ™ใ‚‹", + "trash_page_empty_trash_dialog_content": "ใ‚ดใƒŸ็ฎฑใ‚’็ฉบใซใ—ใพใ™ใ‹๏ผŸ้ธๆŠžใ•ใ‚ŒใŸ้ …็›ฎใฏๅฎŒๅ…จใซๅ‰Š้™คใ•ใ‚Œใพใ™ใ€‚ใ“ใฎๆ“ไฝœใฏๅ–ใ‚Šๆถˆใ›ใพใ›ใ‚“", + "trash_page_empty_trash_dialog_ok": "ไบ†่งฃ", + "trash_page_info": "ใ‚ดใƒŸ็ฎฑใซ็งปๅ‹•ใ—ใŸใ‚ขใ‚คใƒ†ใƒ ใฏ{}ๆ—ฅๅพŒใซๅ‰Š้™คใ•ใ‚Œใพใ™", + "trash_page_no_assets": "ใ‚ดใƒŸ็ฎฑใฏ็ฉบใงใ™", + "trash_page_restore": "ๅพฉๅ…ƒ", + "trash_page_restore_all": "ๅ…จใฆๅพฉๅ…ƒ", + "trash_page_select_assets_btn": "้ …็›ฎใ‚’้ธๆŠž", + "trash_page_select_btn": "้ธๆŠž", + "trash_page_title": "ๅ‰Š้™ค({})", + "upload_dialog_cancel": "ใ‚ญใƒฃใƒณใ‚ปใƒซ", + "upload_dialog_info": "้ธๆŠžใ—ใŸ้ …็›ฎใฎใƒใƒƒใ‚ฏใ‚ขใƒƒใƒ—ใ‚’ใ—ใพใ™ใ‹๏ผŸ", + "upload_dialog_ok": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰", + "upload_dialog_title": "ใ‚ขใƒƒใƒ—ใƒญใƒผใƒ‰", "version_announcement_overlay_ack": "ไบ†่งฃ", "version_announcement_overlay_release_notes": "ๆ›ดๆ–ฐๆƒ…ๅ ฑ", "version_announcement_overlay_text_1": "ใ“ใ‚“ใซใกใฏใ€ใพใŸใฏใ“ใ‚“ใฐใ‚“ใฏ๏ผๆ–ฐใ—ใ„", "version_announcement_overlay_text_2": "ใฎใƒใƒผใ‚ธใƒงใƒณใŒๅ…ฌ้–‹ไธญใงใ™ใ€‚", "version_announcement_overlay_text_3": "ใ‚’็ขบ่ชใ—ใฆใฟใฆใใ ใ•ใ„ใ€‚docker-composeใ‚„.envใƒ•ใ‚กใ‚คใƒซใŒๆœ€ๆ–ฐใฎ็Šถๆ…‹ใซๆ›ดๆ–ฐใ•ใ‚Œใฆใ„ใ‚‹ใ‹ใ€็‰นใซWatchTowerใชใฉใฎใƒ„ใƒผใƒซใ‚’ไฝฟใฃใฆDockerใ‚คใƒกใƒผใ‚ธใ‚’่‡ชๅ‹•ใ‚ขใƒƒใƒ—ใƒ‡ใƒผใƒˆใ—ใฆใ‚‹ไบบใฏ็ขบ่ชใ—ใฆใใ ใ•ใ„ใ€‚", "version_announcement_overlay_title": "ใ‚ตใƒผใƒใƒผใฎๆ–ฐใƒใƒผใ‚ธใƒงใƒณใƒชใƒชใƒผใ‚น\uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "ใ‚นใ‚ฟใƒƒใ‚ฏใ‹ใ‚‰ๅค–ใ™", + "viewer_stack_use_as_main_asset": "ใƒกใ‚คใƒณใฎ็”ปๅƒใจใ—ใฆไฝฟ็”จใ™ใ‚‹", + "viewer_unstack": "ใ‚นใ‚ฟใƒƒใ‚ฏใ‚’่งฃ้™ค" } \ No newline at end of file diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index d87ba3b75..257a7ea9a 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "์ƒ์„ธ์ •๋ณด", "exif_bottom_sheet_location": "์œ„์น˜", "exif_bottom_sheet_location_add": "์œ„์น˜ ์ง€์ •", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "์ง„ํ–‰์ค‘", "experimental_settings_new_asset_list_title": "์‹คํ—˜์  ์‚ฌ์ง„ ๊ทธ๋ฆฌ๋“œ ์ ์šฉ", "experimental_settings_subtitle": "๋ฌธ์ œ์‹œ ์ฑ…์ž„์ง€์ง€ ์•Š์Šต๋‹ˆ๋‹ค!", diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json index cc1b52616..76b8e07d2 100644 --- a/mobile/assets/i18n/lv-LV.json +++ b/mobile/assets/i18n/lv-LV.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "INFORMฤ€CIJA", "exif_bottom_sheet_location": "ATRAล ANฤ€S VIETA", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Izstrฤdes posmฤ", "experimental_settings_new_asset_list_title": "Iespฤ“jot eksperimentฤlo fotoreลพฤฃi", "experimental_settings_subtitle": "Izmanto uzล†emoties risku!", diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn.json index 722ea2722..584be40aa 100644 --- a/mobile/assets/i18n/mn.json +++ b/mobile/assets/i18n/mn.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index ff835ba40..f2306e094 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -35,8 +35,8 @@ "app_bar_signout_dialog_title": "Logg ut", "archive_page_no_archived_assets": "Ingen arkiverte objekter funnet", "archive_page_title": "Arkiv ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "Kan ikke slette objekt(er) med kun lese-rettighet, hopper over", + "asset_action_share_err_offline": "Kan ikke hente offline objekt(er), hopper over", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk bildeorganisering", "asset_list_layout_settings_group_automatically": "Automatisk", "asset_list_layout_settings_group_by": "Grupper bilder etter", @@ -142,15 +142,15 @@ "control_bottom_app_bar_archive": "Arkiver", "control_bottom_app_bar_create_new_album": "Lag nytt album", "control_bottom_app_bar_delete": "Slett", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_delete_from_immich": "Slett fra Immich", + "control_bottom_app_bar_delete_from_local": "Slett fra enhet", "control_bottom_app_bar_edit_location": "Endre lokasjon", "control_bottom_app_bar_edit_time": "Endre Dato og tid", "control_bottom_app_bar_favorite": "Favoritt", "control_bottom_app_bar_share": "Del", "control_bottom_app_bar_share_to": "Del til", "control_bottom_app_bar_stack": "Stable", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Flytt til sรธppelkasse", "control_bottom_app_bar_unarchive": "Fjern fra arkiv", "control_bottom_app_bar_unfavorite": "Fjern favoritt", "control_bottom_app_bar_upload": "Last opp", @@ -165,15 +165,15 @@ "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y โ€ข h:mm a", "delete_dialog_alert": "Disse objektene vil bli slettet permanent fra Immich og fra enheten din", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Disse objektene vil bli permanent slettet fra enheten din, men vil fortsatt vรฆre tilgjengelige fra Immich serveren", + "delete_dialog_alert_local_non_backed_up": "Noen av objektene er ikke sikkerhetskopiert til Immich og vil bli permanent fjernet fra enheten din", + "delete_dialog_alert_remote": "Disse objektene vil bli permanent slettet fra Immich serveren", "delete_dialog_cancel": "Avbryt", "delete_dialog_ok": "Slett", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Slett uansett", "delete_dialog_title": "Slett permanent", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Slett kun sikkerhetskopierte objekter", + "delete_local_dialog_ok_force": "Slett uansett", "delete_shared_link_dialog_content": "Er du sikker pรฅ at du vil slette denne delte linken?", "delete_shared_link_dialog_title": "Slett delt link", "description_input_hint_text": "Legg til beskrivelse ...", @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALJER", "exif_bottom_sheet_location": "PLASSERING", "exif_bottom_sheet_location_add": "Legg til lokasjon", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Under utvikling", "experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning", "experimental_settings_subtitle": "Bruk pรฅ egen risiko!", @@ -199,7 +200,7 @@ "home_page_archive_err_partner": "Kan ikke arkivere partnerobjekter, hopper over", "home_page_building_timeline": "Genererer tidslinjen", "home_page_delete_err_partner": "Kan ikke slette partnerobjekter, hopper over", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_remote_err_local": "Lokale objekter i fjernslettingsvalgene, hopper over", "home_page_favorite_err_local": "Kan ikke sette favoritt pรฅ lokale objekter enda, hopper over", "home_page_favorite_err_partner": "Kan ikke merke partnerobjekter som favoritt enda, hopper over", "home_page_first_time_notice": "Hvis dette er fรธrste gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer.", @@ -275,12 +276,12 @@ "map_settings_include_show_archived": "Inkluder arkiverte", "map_settings_only_relative_range": "Datoomrรฅde", "map_settings_only_show_favorites": "Vis kun favoritter", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Karttema", "map_zoom_to_see_photos": "Zoom ut for รฅ se bilder", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Bevegelige bilder", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato pรฅ objekt(er) med kun lese-rettigheter, hopper over", + "multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon pรฅ objekt(er) med kun lese-rettigheter, hopper over", "notification_permission_dialog_cancel": "Avbryt", "notification_permission_dialog_content": "For รฅ aktivere notifikasjoner, gรฅ til Innstillinger og velg tillat.", "notification_permission_dialog_settings": "Innstillinger", diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index 557eadcb0..df505269a 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -1,6 +1,6 @@ { "action_common_cancel": "Annuleren", - "action_common_update": "Updaten", + "action_common_update": "Bijwerken", "add_to_album_bottom_sheet_added": "Toegevoegd aan {album}", "add_to_album_bottom_sheet_already_exists": "Staat al in {album}", "advanced_settings_log_level_title": "Log niveau: {}", @@ -26,17 +26,17 @@ "album_viewer_appbar_share_err_title": "Albumtitel wijzigen mislukt", "album_viewer_appbar_share_leave": "Verlaat album", "album_viewer_appbar_share_remove": "Verwijder uit album", - "album_viewer_appbar_share_to": "Deel Naar", + "album_viewer_appbar_share_to": "Delen met", "album_viewer_page_share_add_users": "Gebruikers toevoegen", "all_people_page_title": "Personen", "all_videos_page_title": "Video's", - "app_bar_signout_dialog_content": "Weet je zeker dat je je wilt afmelden?", + "app_bar_signout_dialog_content": "Weet je zeker dat je wilt uitloggen?", "app_bar_signout_dialog_ok": "Ja", "app_bar_signout_dialog_title": "Log uit", "archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden", "archive_page_title": "Archief ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "Kan alleen-lezen asset(s) niet verwijderen, overslaan", + "asset_action_share_err_offline": "Kan offline asset(s) niet ophalen, overslaan", "asset_list_layout_settings_dynamic_layout_title": "Dynamische layout", "asset_list_layout_settings_group_automatically": "Automatisch", "asset_list_layout_settings_group_by": "Groupeer assets per", @@ -104,7 +104,7 @@ "backup_err_only_album": "Kan het enige album niet verwijderen", "backup_info_card_assets": "assets", "backup_manual_cancelled": "Geannuleerd", - "backup_manual_failed": "Gefaald", + "backup_manual_failed": "Mislukt", "backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje", "backup_manual_success": "Succes", "backup_manual_title": "Uploadstatus", @@ -113,7 +113,7 @@ "cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beรฏnvloeden totdat de cache opnieuw is opgebouwd.", "cache_settings_duplicated_assets_clear_button": "MAAK VRIJ", "cache_settings_duplicated_assets_subtitle": "Foto's en video's op de zwarte lijst van de app", - "cache_settings_duplicated_assets_title": "Gedupliceerde Assets ({})", + "cache_settings_duplicated_assets_title": "Gedupliceerde assets ({})", "cache_settings_image_cache_size": "Grootte afbeeldingscache ({} assets)", "cache_settings_statistics_album": "Bibliotheekthumbnails", "cache_settings_statistics_assets": "{} assets ({})", @@ -124,7 +124,7 @@ "cache_settings_subtitle": "Beheer het cachegedrag van de Immich app", "cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({} assets)", "cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag", - "cache_settings_tile_title": "Lokale Opslag", + "cache_settings_tile_title": "Lokale opslag", "cache_settings_title": "Cache-instellingen", "change_password_form_confirm_password": "Bevestig wachtwoord", "change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.", @@ -142,15 +142,15 @@ "control_bottom_app_bar_archive": "Archiveren", "control_bottom_app_bar_create_new_album": "Nieuw album maken", "control_bottom_app_bar_delete": "Verwijderen", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Locatie Bewerken", - "control_bottom_app_bar_edit_time": "Datum & Tijd Bewerken", + "control_bottom_app_bar_delete_from_immich": "Verwijderen van Immich", + "control_bottom_app_bar_delete_from_local": "Verwijderen van apparaat", + "control_bottom_app_bar_edit_location": "Locatie bewerken", + "control_bottom_app_bar_edit_time": "Datum & tijd bewerken", "control_bottom_app_bar_favorite": "Favoriet", "control_bottom_app_bar_share": "Delen", - "control_bottom_app_bar_share_to": "Deel Naar", + "control_bottom_app_bar_share_to": "Delen met", "control_bottom_app_bar_stack": "Stapel", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Verplaatsen naar prullenbak", "control_bottom_app_bar_unarchive": "Herstellen", "control_bottom_app_bar_unfavorite": "Onfavoriet", "control_bottom_app_bar_upload": "Uploaden", @@ -165,26 +165,27 @@ "daily_title_text_date_year": "E dd MMM yyyy", "date_format": "E d LLL y โ€ข H:mm", "delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Deze items worden permanent verwijderd van je apparaat, maar blijven beschikbaar op de Immich server", + "delete_dialog_alert_local_non_backed_up": "Van sommige items is geen back-up gemaakt in Immich en zullen permanent van je apparaat worden verwijderd", + "delete_dialog_alert_remote": "Deze items worden permanent verwijderd van de Immich server", "delete_dialog_cancel": "Annuleren", "delete_dialog_ok": "Verwijderen", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Toch verwijderen", "delete_dialog_title": "Permanent verwijderen", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up", + "delete_local_dialog_ok_force": "Toch verwijderen", "delete_shared_link_dialog_content": "Weet je zeker dat je deze gedeelde link wilt verwijderen?", - "delete_shared_link_dialog_title": "Verwijder Gedeelde Link", + "delete_shared_link_dialog_title": "Verwijder gedeelde link", "description_input_hint_text": "Beschrijving toevoegen...", "description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details", - "edit_date_time_dialog_date_time": "Datum en Tijd", + "edit_date_time_dialog_date_time": "Datum en tijd", "edit_date_time_dialog_timezone": "Tijdzone", "edit_location_dialog_title": "Locatie", "exif_bottom_sheet_description": "Beschrijving toevoegen...", "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATIE", "exif_bottom_sheet_location_add": "Locatie toevoegen", + "exif_bottom_sheet_people": "MENSEN", "experimental_settings_new_asset_list_subtitle": "Werk in uitvoering", "experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen", "experimental_settings_subtitle": "Gebruik op eigen risico!", @@ -199,7 +200,7 @@ "home_page_archive_err_partner": "Partner assets kunnen niet gearchiveerd worden, overslaan", "home_page_building_timeline": "Tijdlijn opbouwen", "home_page_delete_err_partner": "Partner assets kunnen niet verwijderd worden, overslaan", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", + "home_page_delete_remote_err_local": "Lokale assets staan in verwijder selectie externe assets, overslaan", "home_page_favorite_err_local": "Lokale assets kunnen nog niet als favoriet worden aangemerkt, overslaan", "home_page_favorite_err_partner": "Partner assets kunnen nog niet ge-favoriet worden, overslaan", "home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.", @@ -259,10 +260,10 @@ "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Gebruik deze locatie", "map_location_service_disabled_content": "Locatie service moet ingeschakeld zijn om assets van je huidige locatie weer te geven. Wil je het nu inschakelen?", - "map_location_service_disabled_title": "Locatie Service uitgeschakeld", + "map_location_service_disabled_title": "Locatie service uitgeschakeld", "map_no_assets_in_bounds": "Geen foto's in dit gebied", "map_no_location_permission_content": "Locatie toestemming is nodig om assets van je huidige locatie weer te geven. Wil je het nu toestaan?", - "map_no_location_permission_title": "Locatie Toestemming geweigerd", + "map_no_location_permission_title": "Locatie toestemming geweigerd", "map_settings_dark_mode": "Donkere modus", "map_settings_date_range_option_all": "Alle", "map_settings_date_range_option_day": "Afgelopen 24 uur", @@ -270,17 +271,17 @@ "map_settings_date_range_option_year": "Afgelopen jaar", "map_settings_date_range_option_years": "Afgelopen {} jaar", "map_settings_dialog_cancel": "Annuleren", - "map_settings_dialog_save": "Sla op", + "map_settings_dialog_save": "Opslaan", "map_settings_dialog_title": "Kaart Instellingen", - "map_settings_include_show_archived": "Weergeef Gearchiveerden", + "map_settings_include_show_archived": "Toon gearchiveerde", "map_settings_only_relative_range": "Datum bereik", "map_settings_only_show_favorites": "Toon enkel favorieten", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Kaart thema", "map_zoom_to_see_photos": "Zoom uit om foto's te zien", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Bewegende foto's", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan", + "multiselect_grid_edit_gps_err_read_only": "Kan locatie van alleen-lezen asset(s) niet wijzigen, overslaan", "notification_permission_dialog_cancel": "Annuleren", "notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.", "notification_permission_dialog_settings": "Instellingen", @@ -390,7 +391,7 @@ "shared_link_app_bar_title": "Gedeelde links", "shared_link_clipboard_copied_massage": "Gekopieerd naar klembord", "shared_link_clipboard_text": "Link: {}\nWachtwoord: {}", - "shared_link_create_app_bar_title": "Link maken om te delen", + "shared_link_create_app_bar_title": "Gedeelde link maken", "shared_link_create_error": "Fout bij het maken van een gedeelde link", "shared_link_create_info": "Laat iedereen met de link de geselecteerde foto(s) zien", "shared_link_create_submit_button": "Link maken", @@ -399,7 +400,7 @@ "shared_link_edit_app_bar_title": "Bewerk link", "shared_link_edit_change_expiry": "Bewerk vervaltijd", "shared_link_edit_description": "Beschrijving", - "shared_link_edit_description_hint": "Geef de deel beschrijving", + "shared_link_edit_description_hint": "Voer beschrijving voor de gedeelde link in", "shared_link_edit_expire_after": "Verval na", "shared_link_edit_expire_after_option_day": "1 dag", "shared_link_edit_expire_after_option_days": "{} dagen", @@ -409,9 +410,9 @@ "shared_link_edit_expire_after_option_minutes": "{} minuten", "shared_link_edit_expire_after_option_never": "Nooit", "shared_link_edit_password": "Wachtwoord", - "shared_link_edit_password_hint": "Voer het deel wachtwoord in", + "shared_link_edit_password_hint": "Voer wachtwoord voor de gedeelde link in", "shared_link_edit_show_meta": "Toon metadata", - "shared_link_edit_submit_button": "Update link", + "shared_link_edit_submit_button": "Link bijwerken", "shared_link_empty": "Je hebt geen gedeelde links", "shared_link_error_server_url_fetch": "Kan de server url niet ophalen", "shared_link_expired": "Verlopen", @@ -452,19 +453,19 @@ "theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen", "translated_text_options": "Opties", "trash_page_delete": "Verwijderen", - "trash_page_delete_all": "Verwijder Alle", + "trash_page_delete_all": "Verwijder alle", "trash_page_empty_trash_btn": "Leeg prullenbak", - "trash_page_empty_trash_dialog_content": "Wil je je weggegooide assets leegmaken? Deze items worden permanent verwijderd van Immich", + "trash_page_empty_trash_dialog_content": "Wil je de prullenbak leegmaken? Deze items worden permanent verwijderd van Immich", "trash_page_empty_trash_dialog_ok": "Ok", "trash_page_info": "Verwijderde items worden permanent verwijderd na {} dagen", "trash_page_no_assets": "Geen verwijderde assets", "trash_page_restore": "Herstellen", - "trash_page_restore_all": "Herstel Alle", + "trash_page_restore_all": "Herstel alle", "trash_page_select_assets_btn": "Selecteer assets", "trash_page_select_btn": "Selecteren", "trash_page_title": "Prullenbak ({})", "upload_dialog_cancel": "Annuleren", - "upload_dialog_info": "Wilt u een backup maken van de geselecteerde Asset(s) op de server?", + "upload_dialog_info": "Wil je een backup maken van de geselecteerde asset(s) op de server?", "upload_dialog_ok": "Uploaden", "upload_dialog_title": "Asset uploaden", "version_announcement_overlay_ack": "Bevestig", diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index 920dd4f86..555e4dfba 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "SZCZEGร“ลY", "exif_bottom_sheet_location": "LOKALIZACJA", "exif_bottom_sheet_location_add": "Dodaj lokalizacjฤ™", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Praca w toku", "experimental_settings_new_asset_list_title": "Wล‚ฤ…cz eksperymentalnฤ… ukล‚ad zdjฤ™ฤ‡", "experimental_settings_subtitle": "Uลผywaj na wล‚asne ryzyko!", diff --git a/mobile/assets/i18n/pt-PT.json b/mobile/assets/i18n/pt-PT.json index a86a85373..67fdc1059 100644 --- a/mobile/assets/i18n/pt-PT.json +++ b/mobile/assets/i18n/pt-PT.json @@ -1,8 +1,8 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", - "add_to_album_bottom_sheet_added": "Added to {album}", - "add_to_album_bottom_sheet_already_exists": "Already in {album}", + "action_common_cancel": "Cancelar", + "action_common_update": "Atualizar", + "add_to_album_bottom_sheet_added": "Adicionar a {album}", + "add_to_album_bottom_sheet_already_exists": "Jรก pertence a {album}", "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.", "advanced_settings_prefer_remote_title": "Prefer remote images", @@ -16,7 +16,7 @@ "album_info_card_backup_album_included": "INCLUรDO", "album_thumbnail_card_item": "1 item", "album_thumbnail_card_items": "{} itens", - "album_thumbnail_card_shared": "Compartilhado", + "album_thumbnail_card_shared": " ยท Partilhado", "album_thumbnail_owned": "Owned", "album_thumbnail_shared_by": "Shared by {}", "album_viewer_appbar_share_delete": "Deletar รกlbum", @@ -29,21 +29,21 @@ "album_viewer_appbar_share_to": "Share To", "album_viewer_page_share_add_users": "Adicionar usuรกrios", "all_people_page_title": "People", - "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", + "all_videos_page_title": "Vรญdeos", + "app_bar_signout_dialog_content": "Tem a certeza que deseja sair?", "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", + "app_bar_signout_dialog_title": "Sair", "archive_page_no_archived_assets": "No archived assets found", "archive_page_title": "Archive ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", - "asset_list_layout_settings_dynamic_layout_title": "Dynamic layout", + "asset_action_delete_err_read_only": "Nรฃo รฉ possรญvel eliminar o(s) recurso(s) sรณ de leitura, ignorando", + "asset_action_share_err_offline": "Nรฃo รฉ possรญvel obter recurso(s) offline, ignorando", + "asset_list_layout_settings_dynamic_layout_title": "Disposiรงรฃo dinรขmica", "asset_list_layout_settings_group_automatically": "Automatic", - "asset_list_layout_settings_group_by": "Group assets by", - "asset_list_layout_settings_group_by_month": "Month", - "asset_list_layout_settings_group_by_month_day": "Month + day", - "asset_list_settings_subtitle": "Configuraรงรตes de layout da grade de fotos", - "asset_list_settings_title": "Grade de fotos", + "asset_list_layout_settings_group_by": "Agrupar recursos por", + "asset_list_layout_settings_group_by_month": "Mรชs", + "asset_list_layout_settings_group_by_month_day": "Mรชs + dia", + "asset_list_settings_subtitle": "Configuraรงรตes de layout da grelha de fotos", + "asset_list_settings_title": "Grelha de fotos", "backup_album_selection_page_albums_device": "รlbuns no dispositivo ({})", "backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para exluir", "backup_album_selection_page_assets_scatter": "Os itens podem estar espalhados por vรกrios รกlbuns. Assim, os รกlbuns podem ser incluรญdos ou excluรญdos durante o processo de backup.", @@ -90,11 +90,11 @@ "backup_controller_page_remainder": "Restante", "backup_controller_page_remainder_sub": "Fotos e vรญdeos restantes para fazer backup da seleรงรฃo", "backup_controller_page_select": "Selecione", - "backup_controller_page_server_storage": "Espaรงo no Servidor", + "backup_controller_page_server_storage": "Armazenamento no servidor", "backup_controller_page_start_backup": "Iniciar Backup", "backup_controller_page_status_off": "Backup estรก desligado", "backup_controller_page_status_on": "Backup estรก ligado", - "backup_controller_page_storage_format": "{} de {} usado", + "backup_controller_page_storage_format": "{} de {} usados", "backup_controller_page_to_backup": "รlbuns para fazer backup", "backup_controller_page_total": "Total", "backup_controller_page_total_sub": "Todas as fotos e vรญdeos dos รกlbuns selecionados", @@ -118,91 +118,92 @@ "cache_settings_statistics_album": "Miniaturas da biblioteca", "cache_settings_statistics_assets": "{} itens ({})", "cache_settings_statistics_full": "Imagens completas", - "cache_settings_statistics_shared": "Miniaturas de รกlbuns compartilhados", + "cache_settings_statistics_shared": "Miniaturas de รกlbuns partilhados", "cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_title": "Uso de cache", "cache_settings_subtitle": "Controle o comportamento de cache do aplicativo Immich", "cache_settings_thumbnail_size": "Tamanho do cache de miniaturas ({} itens)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", + "cache_settings_tile_subtitle": "Controlar o comportamento do armazenamento local", + "cache_settings_tile_title": "Armazenamento local", "cache_settings_title": "Configuraรงรตes de cache", - "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", - "change_password_form_new_password": "New Password", - "change_password_form_password_mismatch": "Passwords do not match", - "change_password_form_reenter_new_password": "Re-enter New Password", - "common_add_to_album": "Add to album", - "common_change_password": "Change Password", - "common_create_new_album": "Create new album", - "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", + "change_password_form_confirm_password": "Confirme a senha", + "change_password_form_description": "Olรก {name},\n\nร‰ a primeira vez que entra no sistema ou foi-lhe pedido que alterasse a sua palavra-passe. Introduza a nova palavra-passe abaixo.", + "change_password_form_new_password": "Nova senha", + "change_password_form_password_mismatch": "As senhas nรฃo coincidem", + "change_password_form_reenter_new_password": "Re-introduza a nova senha", + "common_add_to_album": "Adicionar ao รกlbum", + "common_change_password": "Mudar a senha", + "common_create_new_album": "Criar novo รกlbum", + "common_server_error": "Verifique a sua ligaรงรฃo de rede, certifique-se de que o servidor estรก acessรญvel e de que as versรตes da aplicaรงรฃo/servidor sรฃo compatรญveis.", "common_shared": "Shared", "control_bottom_app_bar_add_to_album": "Adicionar ao รกlbum", "control_bottom_app_bar_album_info": "{} itens", - "control_bottom_app_bar_album_info_shared": "{} itens ยท Compartilhado", + "control_bottom_app_bar_album_info_shared": "{} itens ยท Partilhado", "control_bottom_app_bar_archive": "Archive", "control_bottom_app_bar_create_new_album": "Criar novo รกlbum", "control_bottom_app_bar_delete": "Deletar", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_delete_from_immich": "Apagar do Immich", + "control_bottom_app_bar_delete_from_local": "Apagar do dispositivo", "control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_time": "Edit Date & Time", "control_bottom_app_bar_favorite": "Favorite", - "control_bottom_app_bar_share": "Compartilhar", + "control_bottom_app_bar_share": "Partilhar", "control_bottom_app_bar_share_to": "Share To", "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "Mover para o lixo", "control_bottom_app_bar_unarchive": "Unarchive", "control_bottom_app_bar_unfavorite": "Unfavorite", "control_bottom_app_bar_upload": "Upload", "create_album_page_untitled": "Sem tรญtulo", "create_shared_album_page_create": "Criar", - "create_shared_album_page_share": "Compartilhar", + "create_shared_album_page_share": "Partilhar", "create_shared_album_page_share_add_assets": "ADICIONAR ITENS", "create_shared_album_page_share_select_photos": "Selecionar Fotos", - "curated_location_page_title": "Places", - "curated_object_page_title": "Things", + "curated_location_page_title": "Sรญtios", + "curated_object_page_title": "Objetos", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y โ€ข h:mm a", "delete_dialog_alert": "Esses itens serรฃo permanentemente deletados do Immich e do seu dispositivo", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Estes itens serรฃo removidos permanentemente do seu dispositivo, mas continuarรฃo disponรญveis no servidor Immich", + "delete_dialog_alert_local_non_backed_up": "Alguns dos itens nรฃo estรฃo guardados no Immich e serรฃo removidos permanentemente do seu dispositivo", + "delete_dialog_alert_remote": "Estes itens serรฃo permanentemente eliminados do servidor Immich", "delete_dialog_cancel": "Cancelar", "delete_dialog_ok": "Deletar", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Apagar de qualquer forma", "delete_dialog_title": "Deletar Permanentemente", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Eliminar apenas existentes na cรณpia de seguranรงa", + "delete_local_dialog_ok_force": "Apagar de qualquer forma", "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", + "edit_date_time_dialog_date_time": "Data e Hora", + "edit_date_time_dialog_timezone": "Fuso horรกrio", "edit_location_dialog_title": "Location", "exif_bottom_sheet_description": "Adicionar Descriรงรฃo...", "exif_bottom_sheet_details": "DETALHES", "exif_bottom_sheet_location": "LOCALIZAร‡รƒO", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Trabalho em andamento", - "experimental_settings_new_asset_list_title": "Ativar visualizaรงรฃo de grade experimental", + "experimental_settings_new_asset_list_title": "Ativar visualizaรงรฃo de grelha experimental", "experimental_settings_subtitle": "Use por sua conta e risco!", "experimental_settings_title": "Experimental", "favorites_page_no_favorites": "No favorite assets found", - "favorites_page_title": "Favorites", + "favorites_page_title": "Favoritos", "home_page_add_to_album_conflicts": "Ativos {added} adicionados ao รกlbum {album}. {failed} ativos jรก estรฃo no รกlbum.", - "home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping", + "home_page_add_to_album_err_local": "Ainda nรฃo รฉ possรญvel adicionar recursos locais aos รกlbuns, ignorando", "home_page_add_to_album_success": "Ativos {added} adicionados ao รกlbum {album}.", "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", "home_page_archive_err_local": "Can not archive local assets yet, skipping", "home_page_archive_err_partner": "Can not archive partner assets, skipping", - "home_page_building_timeline": "Building the timeline", + "home_page_building_timeline": "A construir a timeline", "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", + "home_page_delete_remote_err_local": "Recursos locais na seleรงรฃo remota de eliminaรงรฃo, ignorando", + "home_page_favorite_err_local": "Ainda nรฃo รฉ possรญvel adicionar recursos locais favoritos, ignorando", "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", - "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_first_time_notice": "Se for a primeira vez que utiliza a aplicaรงรฃo, certifique-se de que escolhe um รกlbum ou รกlbuns de cรณpia de seguranรงa, para que a linha cronolรณgica possa preencher as fotografias e os vรญdeos no(s) รกlbum(s).", "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "image_viewer_page_state_provider_download_error": "Download Error", @@ -210,16 +211,16 @@ "image_viewer_page_state_provider_share_error": "Share Error", "library_page_albums": "รlbuns", "library_page_archive": "Archive", - "library_page_device_albums": "Albums on Device", - "library_page_favorites": "Favorites", - "library_page_new_album": "Novo Album", - "library_page_sharing": "Sharing", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "Created date", + "library_page_device_albums": "รlbuns no dispositivo", + "library_page_favorites": "Favoritos", + "library_page_new_album": "Novo รกlbum", + "library_page_sharing": "Partilhar", + "library_page_sort_asset_count": "Nรบmero de recursos", + "library_page_sort_created": "Data de criaรงรฃo", "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_oldest_photo": "Oldest photo", + "library_page_sort_most_oldest_photo": "Foto mais antiga", "library_page_sort_most_recent_photo": "Most recent photo", - "library_page_sort_title": "Album title", + "library_page_sort_title": "Tรญtulo do รกlbum", "location_picker_choose_on_map": "Choose on map", "location_picker_latitude": "Latitude", "location_picker_latitude_error": "Enter a valid latitude", @@ -228,8 +229,8 @@ "location_picker_longitude_error": "Enter a valid longitude", "location_picker_longitude_hint": "Enter your longitude here", "login_disabled": "Login has been disabled", - "login_form_api_exception": "API exception. Please check the server URL and try again.", - "login_form_back_button_text": "Back", + "login_form_api_exception": "Excepรงรฃo de API. Verifique o URL do servidor e tente novamente.", + "login_form_back_button_text": "Voltar", "login_form_button_text": "Login", "login_form_email_hint": "seuemail@email.com", "login_form_endpoint_hint": "http://ip-do-seu-servidor:porta/api", @@ -245,15 +246,15 @@ "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", "login_form_label_email": "Email", "login_form_label_password": "Senha", - "login_form_next_button": "Next", + "login_form_next_button": "Avanรงar", "login_form_password_hint": "senha", "login_form_save_login": "Permanecer logado", - "login_form_server_empty": "Enter a server URL.", - "login_form_server_error": "Could not connect to server.", + "login_form_server_empty": "Introduzir um URL de servidor.", + "login_form_server_error": "Nรฃo foi possรญvel ligar ao servidor.", "login_password_changed_error": "There was an error updating your password", "login_password_changed_success": "Password updated successfully", "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bounds": "{} fotos", "map_cannot_get_user_location": "Cannot get user's location", "map_location_dialog_cancel": "Cancel", "map_location_dialog_yes": "Yes", @@ -264,83 +265,83 @@ "map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?", "map_no_location_permission_title": "Location Permission denied", "map_settings_dark_mode": "Dark mode", - "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_all": "Tudo", + "map_settings_date_range_option_day": "รšltimas 24 horas", + "map_settings_date_range_option_days": "รšltimos {} dias", + "map_settings_date_range_option_year": "รšltimo ano", + "map_settings_date_range_option_years": "รšltimos {} anos", "map_settings_dialog_cancel": "Cancel", "map_settings_dialog_save": "Save", "map_settings_dialog_title": "Map Settings", "map_settings_include_show_archived": "Include Archived", "map_settings_only_relative_range": "Date range", "map_settings_only_show_favorites": "Show Favorite Only", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Tema do mapa", "map_zoom_to_see_photos": "Zoom out to see photos", "monthly_title_text_date_format": "MMMM y", - "motion_photos_page_title": "Motion Photos", + "motion_photos_page_title": "Fotos com movimento", "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", - "notification_permission_dialog_cancel": "Cancel", - "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", + "notification_permission_dialog_cancel": "Cancelar", + "notification_permission_dialog_content": "Para ativar as notificaรงรตes, vรก a Definiรงรตes e selecione permitir.", "notification_permission_dialog_settings": "Settings", "notification_permission_list_tile_content": "Grant permission to enable notifications.", "notification_permission_list_tile_enable_button": "Enable Notifications", - "notification_permission_list_tile_title": "Notification Permission", - "partner_page_add_partner": "Add partner", + "notification_permission_list_tile_title": "Permissรฃo de notificaรงรตes", + "partner_page_add_partner": "Adicionar parceiro", "partner_page_empty_message": "Your photos are not yet shared with any partner.", "partner_page_no_more_users": "No more users to add", "partner_page_partner_add_failed": "Failed to add partner", - "partner_page_select_partner": "Select partner", + "partner_page_select_partner": "Selecionar parceiro", "partner_page_shared_to_title": "Shared to", "partner_page_stop_sharing_content": "{} will no longer be able to access your photos.", "partner_page_stop_sharing_title": "Stop sharing your photos?", "partner_page_title": "Partner", "permission_onboarding_back": "Back", - "permission_onboarding_continue_anyway": "Continue anyway", + "permission_onboarding_continue_anyway": "Continuar de qualquer maneira", "permission_onboarding_get_started": "Get started", "permission_onboarding_go_to_settings": "Go to settings", "permission_onboarding_grant_permission": "Grant permission", - "permission_onboarding_log_out": "Log out", - "permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.", - "permission_onboarding_permission_granted": "Permission granted! You are all set.", - "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", - "permission_onboarding_request": "Immich requires permission to view your photos and videos.", + "permission_onboarding_log_out": "Sair", + "permission_onboarding_permission_denied": "Permissรฃo negada. Para utilizar o Immich, conceda permissรตes de fotografia e vรญdeo nas Definiรงรตes.", + "permission_onboarding_permission_granted": "Autorizaรงรฃo concedida! Estรก tudo pronto.", + "permission_onboarding_permission_limited": "Permissรฃo limitada. Para permitir que o Immich faรงa cรณpias de seguranรงa e gira toda a sua coleรงรฃo de galerias, conceda permissรตes para fotografias e vรญdeos nas Definiรงรตes.", + "permission_onboarding_request": "O Immich requer autorizaรงรฃo para ver as suas fotografias e vรญdeos.", "profile_drawer_app_logs": "Logs", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_out_of_date_major": "A aplicaรงรฃo mรณvel estรก desatualizada. Atualize para a versรฃo principal mais recente.", + "profile_drawer_client_out_of_date_minor": "A aplicaรงรฃo mรณvel estรก desatualizada. Por favor, atualize para a versรฃo mais recente.", "profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados", "profile_drawer_documentation": "Documentation", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_server_out_of_date_major": "O servidor estรก desatualizado. Atualize para a versรฃo principal mais recente.", + "profile_drawer_server_out_of_date_minor": "O servidor estรก desatualizado. Atualize para a versรฃo mais recente.", "profile_drawer_settings": "Configuraรงรตes", "profile_drawer_sign_out": "Sair", "profile_drawer_trash": "Trash", - "recently_added_page_title": "Recently Added", - "scaffold_body_error_occurred": "Error occurred", + "recently_added_page_title": "Adicionado recentemente", + "scaffold_body_error_occurred": "Ocorreu um erro", "search_bar_hint": "Busque suas fotos", "search_page_categories": "Categories", - "search_page_favorites": "Favorites", - "search_page_motion_photos": "Motion Photos", + "search_page_favorites": "Favoritos", + "search_page_motion_photos": "Fotos com movimento", "search_page_no_objects": "Nenhuma informaรงรฃo de objeto disponรญvel", - "search_page_no_places": "Nenhuma informaรงรฃo de lugares disponรญvel", + "search_page_no_places": "Nenhuma informaรงรฃo de sรญtios disponรญvel", "search_page_people": "People", - "search_page_person_add_name_dialog_cancel": "Cancel", - "search_page_person_add_name_dialog_hint": "Name", - "search_page_person_add_name_dialog_save": "Save", - "search_page_person_add_name_dialog_title": "Add a name", - "search_page_person_add_name_subtitle": "Find them fast by name with search", - "search_page_person_add_name_title": "Add a name", - "search_page_person_edit_name": "Edit name", - "search_page_places": "Lugares", - "search_page_recently_added": "Recently added", + "search_page_person_add_name_dialog_cancel": "Cancelar", + "search_page_person_add_name_dialog_hint": "Nome", + "search_page_person_add_name_dialog_save": "Guardar", + "search_page_person_add_name_dialog_title": "Adicionar um nome", + "search_page_person_add_name_subtitle": "Encontre-os rapidamente pelo nome com a pesquisa", + "search_page_person_add_name_title": "Adicionar um nome", + "search_page_person_edit_name": "Editar nome", + "search_page_places": "Sรญtios", + "search_page_recently_added": "Adicionado recentemente", "search_page_screenshots": "Screenshots", "search_page_selfies": "Selfies", "search_page_things": "Objetos", - "search_page_videos": "Videos", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", + "search_page_videos": "Vรญdeos", + "search_page_view_all_button": "Ver tudo", + "search_page_your_activity": "A sua atividade", "search_page_your_map": "Your Map", "search_result_page_new_search_hint": "Nova Busca", "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", @@ -348,10 +349,10 @@ "select_additional_user_for_sharing_page_suggestions": "Sugestรตes", "select_user_for_sharing_page_err_album": "Falha ao criar o รกlbum", "select_user_for_sharing_page_share_suggestions": "Sugestรตes", - "server_info_box_app_version": "App Version", - "server_info_box_latest_release": "Latest Version", + "server_info_box_app_version": "Versรฃo da app", + "server_info_box_latest_release": "รšltima versรฃo", "server_info_box_server_url": "Server URL", - "server_info_box_server_version": "Server Version", + "server_info_box_server_version": "Versรฃo do servidor", "setting_image_viewer_help": "O visualizador de detalhes carrega primeiro a miniatura pequena, depois carrega a visualizaรงรฃo de tamanho mรฉdio (se ativado) e, finalmente, carrega o original (se ativado).", "setting_image_viewer_original_subtitle": "Ative para carregar a imagem original em resoluรงรฃo total (grande!). Desative para reduzir o uso de dados (na rede e no cache do dispositivo).", "setting_image_viewer_original_title": "Carregar imagem original", @@ -381,65 +382,65 @@ "shared_album_activity_remove_title": "Delete Activity", "shared_album_activity_setting_subtitle": "Let others respond", "shared_album_activity_setting_title": "Comments & likes", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", + "shared_album_section_people_action_error": "Erro ao sair/remover do รกlbum", + "shared_album_section_people_action_leave": "Remover utilizador do รกlbum", + "shared_album_section_people_action_remove_user": "Remover utilizador do รกlbum", "shared_album_section_people_owner_label": "Owner", "shared_album_section_people_title": "PEOPLE", "share_dialog_preparing": "Preparando...", - "shared_link_app_bar_title": "Shared Links", + "shared_link_app_bar_title": "Links partilhados", "shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_text": "Link: {}\nPassword: {}", "shared_link_create_app_bar_title": "Create link to share", "shared_link_create_error": "Error while creating shared link", "shared_link_create_info": "Let anyone with the link see the selected photo(s)", "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", + "shared_link_edit_allow_download": "Permitir que um utilizador pรบblico descarregue", + "shared_link_edit_allow_upload": "Permitir que um utilizador pรบblico carregue", "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", + "shared_link_edit_change_expiry": "Alterar o prazo de validade", "shared_link_edit_description": "Description", "shared_link_edit_description_hint": "Enter the share description", "shared_link_edit_expire_after": "Expire after", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", + "shared_link_edit_expire_after_option_day": "1 dia", + "shared_link_edit_expire_after_option_days": "{} dias", + "shared_link_edit_expire_after_option_hour": "1 hora", + "shared_link_edit_expire_after_option_hours": "{} horas", + "shared_link_edit_expire_after_option_minute": "1 minuto", + "shared_link_edit_expire_after_option_minutes": "{} minutos", "shared_link_edit_expire_after_option_never": "Never", "shared_link_edit_password": "Password", "shared_link_edit_password_hint": "Enter the share password", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", + "shared_link_edit_show_meta": "Mostrar metadados", + "shared_link_edit_submit_button": "Atualizar link", + "shared_link_empty": "Nรฃo tem links partilhados", "shared_link_error_server_url_fetch": "Cannot fetch the server url", "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", + "shared_link_expires_day": "Expira em {} dia", + "shared_link_expires_days": "Expira em {} dias", + "shared_link_expires_hour": "Expira em {} hora", + "shared_link_expires_hours": "Expira em {} horas", + "shared_link_expires_minute": "Expira em {} minuto", "shared_link_expires_minutes": "Expires in {} minutes", "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", + "shared_link_expires_second": "Expira em {} segundo", "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_info_chip_download": "Download", + "shared_link_info_chip_download": "Descarregar", "shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_upload": "Upload", - "shared_link_manage_links": "Manage Shared links", + "shared_link_manage_links": "Gerir links partilhados", "share_done": "Done", "share_invite": "Convidar para รกlbum", - "sharing_page_album": "รlbuns compartilhados", - "sharing_page_description": "Criar รกlbuns compartilhados para compartilhas fotos e vรญdeos com pessoas na sua rede.", + "sharing_page_album": "รlbuns partilhados", + "sharing_page_description": "Crie รกlbuns partilhados para partilhar fotografias e vรญdeos com pessoas da sua rede.", "sharing_page_empty_list": "LISTA VAZIA", - "sharing_silver_appbar_create_shared_album": "Criar um รกlgum compartilhado", + "sharing_silver_appbar_create_shared_album": "Criar รกlbum partilhado", "sharing_silver_appbar_shared_links": "Shared links", - "sharing_silver_appbar_share_partner": "Compartilhar com parceiro", + "sharing_silver_appbar_share_partner": "Partilhar com parceiro", "tab_controller_nav_library": "Biblioteca", "tab_controller_nav_photos": "Fotos", - "tab_controller_nav_search": "Buscar", - "tab_controller_nav_sharing": "Compartilhando", + "tab_controller_nav_search": "Procurar", + "tab_controller_nav_sharing": "Partilhar", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento em blocos de ativos", "theme_setting_asset_list_tiles_per_row_title": "Nรบmero de itens por linha ({})", "theme_setting_dark_mode_switch": "Modo escuro", @@ -467,11 +468,11 @@ "upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?", "upload_dialog_ok": "Upload", "upload_dialog_title": "Upload Asset", - "version_announcement_overlay_ack": "Need Context", + "version_announcement_overlay_ack": "Aceitar", "version_announcement_overlay_release_notes": "notas de lanรงamento", "version_announcement_overlay_text_1": "Olรก, hรก um novo lanรงamento de", "version_announcement_overlay_text_2": "por favor, tome o seu tempo para visitar o", - "version_announcement_overlay_text_3": "e certifique-se de que a configuraรงรฃo do docker-compose e do .env estejam atualizadas para evitar configuraรงรตes incorretas, especialmente se vocรช usar o WatchTower ou qualquer mecanismo que lide com a atualizaรงรฃo automรกtica do aplicativo do servidor.", + "version_announcement_overlay_text_3": "e certifique-se de que a configuraรงรฃo do docker-compose e do .env estejam atualizadas para evitar configuraรงรตes incorretas, especialmente se usar o WatchTower ou qualquer mecanismo que lide com a atualizaรงรฃo automรกtica do servidor.", "version_announcement_overlay_title": "Nova versรฃo do servidor disponรญvel \uD83C\uDF89", "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index d8756108f..23028f9ef 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -1,12 +1,12 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "ะžั‚ะผะตะฝะฐ", + "action_common_update": "ะžะฑะฝะพะฒะธั‚ัŒ", "add_to_album_bottom_sheet_added": "ะ”ะพะฑะฐะฒะปะตะฝะพ ะฒ {album}", "add_to_album_bottom_sheet_already_exists": "ะฃะถะต ะฒ {album}", "advanced_settings_log_level_title": "Log level: {}", - "advanced_settings_prefer_remote_subtitle": "ะะตะบะพั‚ะพั€ั‹ะต ัƒัั‚ั€ะพะนัั‚ะฒะฐ ะพั‡ะตะฝัŒ ะผะตะดะปะตะฝะฝะพ ะทะฐะณั€ัƒะถะฐัŽั‚ ะฟั€ะตะดะฟั€ะพัะผะพั‚ั€ ะพะฑัŠะตะบั‚ะพะฒ, ะฝะฐั…ะพะดัั‰ะธั…ัั ะฝะฐ ัƒัั‚ั€ะพะนัั‚ะฒะต. ะะบั‚ะธะฒะธั€ัƒะนั‚ะต ัั‚ัƒ ะฝะฐัั‚ั€ะพะนะบัƒ, ั‡ั‚ะพะฑั‹ ะฒะผะตัั‚ะพ ะฝะธั… ะทะฐะณั€ัƒะถะฐะปะธััŒ ะธะทะพะฑั€ะฐะถะตะฝะธ ั ัะตั€ะฒะตั€ะฐ.", + "advanced_settings_prefer_remote_subtitle": "ะะตะบะพั‚ะพั€ั‹ะต ัƒัั‚ั€ะพะนัั‚ะฒะฐ ะพั‡ะตะฝัŒ ะผะตะดะปะตะฝะฝะพ ะทะฐะณั€ัƒะถะฐัŽั‚ ะฟั€ะตะดะฟั€ะพัะผะพั‚ั€ ะพะฑัŠะตะบั‚ะพะฒ, ะฝะฐั…ะพะดัั‰ะธั…ัั ะฝะฐ ัƒัั‚ั€ะพะนัั‚ะฒะต. ะะบั‚ะธะฒะธั€ัƒะนั‚ะต ัั‚ัƒ ะฝะฐัั‚ั€ะพะนะบัƒ, ั‡ั‚ะพะฑั‹ ะฒะผะตัั‚ะพ ะฝะธั… ะทะฐะณั€ัƒะถะฐะปะธััŒ ะธะทะพะฑั€ะฐะถะตะฝะธั ั ัะตั€ะฒะตั€ะฐ.", "advanced_settings_prefer_remote_title": "ะŸั€ะตะดะฟะพั‡ะธั‚ะฐั‚ัŒ ั„ะพั‚ะพ ะฝะฐ ัะตั€ะฒะตั€ะต", - "advanced_settings_self_signed_ssl_subtitle": "ะŸั€ะพะฟัƒัะบะฐะตั‚ ะฟั€ะพะฒะตั€ะบัƒ ัะตั€ั‚ะธั„ะธะบะฐั‚ะฐ SSL ะดะปั ะบะพะฝะตั‡ะฝะพะน ั‚ะพั‡ะบะธ ัะตั€ะฒะตั€ะฐ. ะขั€ะตะฑัƒะตั‚ัั ะดะปั ัะฐะผะพะฟะพะดะฟะธัะฐะฝะฝั‹ั… ัะตั€ั‚ะธั„ะธะบะฐั‚ะพะฒ.", + "advanced_settings_self_signed_ssl_subtitle": "ะŸั€ะพะฟัƒัะบะฐะตั‚ ะฟั€ะพะฒะตั€ะบัƒ SSL-ัะตั€ั‚ะธั„ะธะบะฐั‚ะฐ ัะตั€ะฒะตั€ะฐ. ะขั€ะตะฑัƒะตั‚ัั ะดะปั ัะฐะผะพะฟะพะดะฟะธัะฐะฝะฝั‹ั… ัะตั€ั‚ะธั„ะธะบะฐั‚ะพะฒ.", "advanced_settings_self_signed_ssl_title": "ะ ะฐะทั€ะตัˆะธั‚ัŒ ัะฐะผะพะฟะพะดะฟะธัะฐะฝะฝั‹ะต SSL-ัะตั€ั‚ะธั„ะธะบะฐั‚ั‹", "advanced_settings_tile_subtitle": "ะ ะฐััˆะธั€ะตะฝะฝั‹ะต ะฝะฐัั‚ั€ะพะนะบะธ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั", "advanced_settings_tile_title": "ะ ะฐััˆะธั€ะตะฝะฝั‹ะต", @@ -35,14 +35,14 @@ "app_bar_signout_dialog_title": "ะ’ั‹ะนั‚ะธ ะธะท ัะธัั‚ะตะผั‹", "archive_page_no_archived_assets": "ะ’ ะฐั€ั…ะธะฒะต ัะตะนั‡ะฐั ะฟัƒัั‚ะพ", "archive_page_title": "ะั€ั…ะธะฒ ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "ะะตะฒะพะทะผะพะถะฝะพ ัƒะดะฐะปะธั‚ัŒ ะพะฑัŠะตะบั‚(ั‹) ั‚ะพะปัŒะบะพ ะดะปั ั‡ั‚ะตะฝะธั, ะฟั€ะพะฟัƒัะบ...", + "asset_action_share_err_offline": "ะะตะฒะพะทะผะพะถะฝะพ ะฟะพะปัƒั‡ะธั‚ัŒ ะพั„ั„ะปะฐะนะฝ-ะพะฑัŠะตะบั‚(ั‹), ะฟั€ะพะฟัƒัะบ...", "asset_list_layout_settings_dynamic_layout_title": "ะ”ะธะฝะฐะผะธั‡ะตัะบะพะต ั€ะฐัะฟะพะปะพะถะตะฝะธะต", "asset_list_layout_settings_group_automatically": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ", "asset_list_layout_settings_group_by": "ะ“ั€ัƒะฟะฟะธั€ะพะฒะฐั‚ัŒ ะพะฑัŠะตะบั‚ั‹ ะฟะพ:", "asset_list_layout_settings_group_by_month": "ะœะตััั†ัƒ", "asset_list_layout_settings_group_by_month_day": "ะœะตััั†ัƒ ะธ ะดะฝัŽ", - "asset_list_settings_subtitle": "ะะฐัั‚ั€ะพะนะบะธ ะผะฐะบะตั‚ะฐ ัะตั‚ะบะธ ั„ะพั‚ะพะณั€ะฐั„ะธะน", + "asset_list_settings_subtitle": "ะะฐัั‚ั€ะพะนะบะฐ ะผะฐะบะตั‚ะฐ ัะตั‚ะบะธ ั„ะพั‚ะพะณั€ะฐั„ะธะน", "asset_list_settings_title": "ะกะตั‚ะบะฐ ั„ะพั‚ะพะณั€ะฐั„ะธะน", "backup_album_selection_page_albums_device": "ะะปัŒะฑะพะผะพะฒ ะฝะฐ ัƒัั‚ั€ะพะนัั‚ะฒะต ({})", "backup_album_selection_page_albums_tap": "ะะฐะถะผะธั‚ะต, ั‡ั‚ะพะฑั‹ ะฒะบะปัŽั‡ะธั‚ัŒ, ะฝะฐะถะผะธั‚ะต ะดะฒะฐะถะดั‹, ั‡ั‚ะพะฑั‹ ะธัะบะปัŽั‡ะธั‚ัŒ", @@ -65,7 +65,7 @@ "backup_controller_page_background_battery_info_link": "ะŸะพะบะฐะทะฐั‚ัŒ ะบะฐะบ", "backup_controller_page_background_battery_info_message": "ะ”ะปั ะฝะฐะธะปัƒั‡ัˆะตะณะพ ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟะธั€ะพะฒะฐะฝะธั ะพั‚ะบะปัŽั‡ะธั‚ะต ะปัŽะฑั‹ะต ะฝะฐัั‚ั€ะพะนะบะธ ะพะฟั‚ะธะผะธะทะฐั†ะธะธ ะฑะฐั‚ะฐั€ะตะธ, ะพะณั€ะฐะฝะธั‡ะธะฒะฐัŽั‰ะธะต ั„ะพะฝะพะฒัƒัŽ ะฐะบั‚ะธะฒะฝะพัั‚ัŒ ะดะปั Immich.\n\nะŸะพัะบะพะปัŒะบัƒ ัั‚ะพ ะทะฐะฒะธัะธั‚ ะพั‚ ัƒัั‚ั€ะพะนัั‚ะฒะฐ, ะฝะฐะนะดะธั‚ะต ะฝะตะพะฑั…ะพะดะธะผัƒัŽ ะธะฝั„ะพั€ะผะฐั†ะธัŽ ะดะปั ะฟั€ะพะธะทะฒะพะดะธั‚ะตะปั ะฒะฐัˆะตะณะพ ัƒัั‚ั€ะพะนัั‚ะฒะฐ.", "backup_controller_page_background_battery_info_ok": "ะžะš", - "backup_controller_page_background_battery_info_title": "\nะžะฟั‚ะธะผะธะทะฐั†ะธั ะฑะฐั‚ะฐั€ะตะธ", + "backup_controller_page_background_battery_info_title": "ะžะฟั‚ะธะผะธะทะฐั†ะธั ะฑะฐั‚ะฐั€ะตะธ", "backup_controller_page_background_charging": "ะขะพะปัŒะบะพ ะฒะพ ะฒั€ะตะผั ะทะฐั€ัะดะบะธ", "backup_controller_page_background_configure_error": "ะะต ัƒะดะฐะปะพััŒ ะฝะฐัั‚ั€ะพะธั‚ัŒ ั„ะพะฝะพะฒัƒัŽ ัะปัƒะถะฑัƒ", "backup_controller_page_background_delay": "ะžั‚ะปะพะถะธั‚ัŒ ั€ะตะทะตั€ะฒะฝะพะต ะบะพะฟะธั€ะพะฒะฐะฝะธะต ะฝะพะฒั‹ั… ะพะฑัŠะตะบั‚ะพะฒ: {}", @@ -112,8 +112,8 @@ "cache_settings_clear_cache_button": "ะžั‡ะธัั‚ะธั‚ัŒ ะบััˆ", "cache_settings_clear_cache_button_title": "ะžั‡ะธั‰ะฐะตั‚ ะบััˆ ะฟั€ะธะปะพะถะตะฝะธั. ะญั‚ะพ ะทะฝะฐั‡ะธั‚ะตะปัŒะฝะพ ะฟะพะฒะปะธัะตั‚ ะฝะฐ ะฟั€ะพะธะทะฒะพะดะธั‚ะตะปัŒะฝะพัั‚ัŒ ะฟั€ะธะปะพะถะตะฝะธั, ะดะพ ั‚ะตั… ะฟะพั€, ะฟะพะบะฐ ะบััˆ ะฝะต ะฑัƒะดะตั‚ ะฟะตั€ะตัั‚ั€ะพะตะฝ ะทะฐะฝะพะฒะพ.", "cache_settings_duplicated_assets_clear_button": "ะžะงะ˜ะกะขะ˜ะขะฌ", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "ะ”ัƒะฑะปะธั€ะพะฒะฐะฝะฝั‹ะต ั€ะตััƒั€ัั‹", + "cache_settings_duplicated_assets_subtitle": "ะคะพั‚ะพ ะธ ะฒะธะดะตะพ, ะทะฐะฝะตัะตะฝะฝั‹ะต ะฟั€ะธะปะพะถะตะฝะธะตะผ ะฒ ั‡ะตั€ะฝั‹ะน ัะฟะธัะพะบ", + "cache_settings_duplicated_assets_title": "ะ”ัƒะฑะปะธั€ัƒัŽั‰ะธะตัั ะพะฑัŠะตะบั‚ั‹ ({})", "cache_settings_image_cache_size": "ะ ะฐะทะผะตั€ ะบััˆะฐ ะธะทะพะฑั€ะฐะถะตะฝะธะน ({} ะพะฑัŠะตะบั‚ะพะฒ)", "cache_settings_statistics_album": "ะœะธะฝะธะฐั‚ัŽั€ั‹ ะฑะธะฑะปะธะพั‚ะตะบะธ", "cache_settings_statistics_assets": "{} ะพะฑัŠะตะบั‚ะพะฒ ({})", @@ -140,19 +140,19 @@ "control_bottom_app_bar_album_info": "{} ั„ะฐะนะปะพะฒ", "control_bottom_app_bar_album_info_shared": "{} ั„ะฐะนะปะพะฒ ยท ะžะฑั‰ะธะน", "control_bottom_app_bar_archive": "ะั€ั…ะธะฒ", - "control_bottom_app_bar_create_new_album": "\nะกะพะทะดะฐั‚ัŒ ะฝะพะฒั‹ะน ะฐะปัŒะฑะพะผ", + "control_bottom_app_bar_create_new_album": "ะกะพะทะดะฐั‚ัŒ ะฝะพะฒั‹ะน ะฐะปัŒะฑะพะผ", "control_bottom_app_bar_delete": "ะฃะดะฐะปะธั‚ัŒ", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "ะ˜ะทะผะตะฝะธั‚ัŒ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", - "control_bottom_app_bar_edit_time": "ะ˜ะทะผะตะฝะธั‚ัŒ ะดะฐั‚ัƒ ะธ ะฒั€ะตะผั", - "control_bottom_app_bar_favorite": "ะ˜ะทะฑั€ะฐะฝะฝะพะต", + "control_bottom_app_bar_delete_from_immich": "ะฃะดะฐะปะธั‚ัŒ ะธะท Immich\n", + "control_bottom_app_bar_delete_from_local": "ะฃะดะฐะปะธั‚ัŒ ั ัƒัั‚ั€ะพะนัั‚ะฒะฐ", + "control_bottom_app_bar_edit_location": "ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", + "control_bottom_app_bar_edit_time": "ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะดะฐั‚ัƒ ะธ ะฒั€ะตะผั", + "control_bottom_app_bar_favorite": "ะ’ ะธะทะฑั€ะฐะฝะฝะพะต", "control_bottom_app_bar_share": "ะŸะพะดะตะปะธั‚ัŒัั", "control_bottom_app_bar_share_to": "ะŸะพะดะตะปะธั‚ัŒัั", "control_bottom_app_bar_stack": "ะกั‚ะตะบ", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_trash_from_immich": "ะŸะตั€ะตะผะตัั‚ะธั‚ัŒ ะฒ ะบะพั€ะทะธะฝัƒ", "control_bottom_app_bar_unarchive": "ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ", - "control_bottom_app_bar_unfavorite": "ะ˜ัะบะปัŽั‡ะธั‚ัŒ ะธะท ะธะทะฑั€ะฐะฝะฝะพะณะพ", + "control_bottom_app_bar_unfavorite": "ะฃะดะฐะปะธั‚ัŒ ะธะท ะธะทะฑั€ะฐะฝะฝะพะณะพ", "control_bottom_app_bar_upload": "ะ—ะฐะณั€ัƒะทะธั‚ัŒ", "create_album_page_untitled": "ะ‘ะตะท ะฝะฐะทะฒะฐะฝะธั", "create_shared_album_page_create": "ะกะพะทะดะฐั‚ัŒ", @@ -165,26 +165,27 @@ "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y โ€ข h:mm a", "delete_dialog_alert": "ะญั‚ะธ ัะปะตะผะตะฝั‚ั‹ ะฑัƒะดัƒั‚ ะฑะตะทะฒะพะทะฒั€ะฐั‚ะฝะพ ัƒะดะฐะปะตะฝั‹ ะธะท ะฟั€ะธะปะพะถะตะฝะธั, ะฐ ั‚ะฐะบะถะต ั ะฒะฐัˆะตะณะพ ัƒัั‚ั€ะพะนัั‚ะฒะฐ", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "ะญั‚ะธ ะพะฑัŠะตะบั‚ั‹ ะฑัƒะดัƒั‚ ะฑะตะทะฒะพะทะฒั€ะฐั‚ะฝะพ ัƒะดะฐะปะตะฝั‹ ั ะ’ะฐัˆะตะณะพ ัƒัั‚ั€ะพะนัั‚ะฒะฐ, ะฝะพ ะฟะพ-ะฟั€ะตะถะฝะตะผัƒ ะฑัƒะดัƒั‚ ะดะพัั‚ัƒะฟะฝั‹ ะฝะฐ ัะตั€ะฒะตั€ะต Immich", + "delete_dialog_alert_local_non_backed_up": "ะ ะตะทะตั€ะฒะฝั‹ะต ะบะพะฟะธะธ ะฝะตะบะพั‚ะพั€ั‹ั… ะพะฑัŠะตะบั‚ะพะฒ ะฝะต ะฑั‹ะปะธ ะทะฐะณั€ัƒะถะตะฝั‹ ะฒ Immich ะธ ะฑัƒะดัƒั‚ ะฑะตะทะฒะพะทะฒั€ะฐั‚ะฝะพ ัƒะดะฐะปะตะฝั‹ ั ะ’ะฐัˆะตะณะพ ัƒัั‚ั€ะพะนัั‚ะฒะฐ", + "delete_dialog_alert_remote": "ะญั‚ะธ ะพะฑัŠะตะบั‚ั‹ ะฑัƒะดัƒั‚ ะฑะตะทะฒะพะทะฒั€ะฐั‚ะฝะพ ัƒะดะฐะปะตะฝั‹ ั ัะตั€ะฒะตั€ะฐ Immich", "delete_dialog_cancel": "ะžั‚ะผะตะฝะธั‚ัŒ", "delete_dialog_ok": "ะฃะดะฐะปะธั‚ัŒ", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "ะ’ัะต ั€ะฐะฒะฝะพ ัƒะดะฐะปะธั‚ัŒ", "delete_dialog_title": "ะฃะดะฐะปะธั‚ัŒ ะฝะฐะฒัะตะณะดะฐ", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "ะฃะดะฐะปะธั‚ัŒ ั‚ะพะปัŒะบะพ ั€ะตะทะตั€ะฒะฝั‹ะต ะบะพะฟะธะธ", + "delete_local_dialog_ok_force": "ะ’ัะต ั€ะฐะฒะฝะพ ัƒะดะฐะปะธั‚ัŒ", "delete_shared_link_dialog_content": "ะ’ั‹ ัƒะฒะตั€ะตะฝั‹, ั‡ั‚ะพ ั…ะพั‚ะธั‚ะต ัƒะดะฐะปะธั‚ัŒ ัั‚ัƒ ะพะฑั‰ัƒัŽ ััั‹ะปะบัƒ?", "delete_shared_link_dialog_title": "ะฃะดะฐะปะธั‚ัŒ ะพะฑั‰ัƒัŽ ััั‹ะปะบัƒ", "description_input_hint_text": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะพะฟะธัะฐะฝะธะต...", "description_input_submit_error": "ะะต ัƒะดะฐะปะพััŒ ะพะฑะฝะพะฒะธั‚ัŒ ะพะฟะธัะฐะฝะธะต, ะฟั€ะพะฒะตั€ัŒั‚ะต ะปะพะณะธ, ั‡ั‚ะพะฑั‹ ัƒะทะฝะฐั‚ัŒ ะฟั€ะธั‡ะธะฝัƒ", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "edit_date_time_dialog_date_time": "ะ”ะฐั‚ะฐ ะธ ะฒั€ะตะผั", + "edit_date_time_dialog_timezone": "ะงะฐัะพะฒะพะน ะฟะพัั", + "edit_location_dialog_title": "ะœะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", "exif_bottom_sheet_description": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะพะฟะธัะฐะฝะธะต...", "exif_bottom_sheet_details": "ะŸะžะ”ะ ะžะ‘ะะžะกะขะ˜", - "exif_bottom_sheet_location": "ะœะ•ะกะขะžะŸะžะ›ะžะ–ะ•ะะ˜ะ•", + "exif_bottom_sheet_location": "ะœะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", "exif_bottom_sheet_location_add": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "ะ’ะตะดัƒั‚ัั ั€ะฐะฑะพั‚ั‹", "experimental_settings_new_asset_list_title": "ะ’ะบะปัŽั‡ะธั‚ัŒ ัะบัะฟะตั€ะธะผะตะฝั‚ะฐะปัŒะฝัƒัŽ ัะตั‚ะบัƒ ั„ะพั‚ะพะณั€ะฐั„ะธะน", "experimental_settings_subtitle": "ะ˜ัะฟะพะปัŒะทัƒะนั‚ะต ะฝะฐ ัะฒะพะน ัั‚ั€ะฐั… ะธ ั€ะธัะบ!", @@ -194,39 +195,39 @@ "home_page_add_to_album_conflicts": "ะ”ะพะฑะฐะฒะปะตะฝะพ {added} ะพะฑัŠะตะบั‚ะพะฒ ะฒ ะฐะปัŒะฑะพะผ {album}. ะžะฑัŠะตะบั‚ั‹ {failed} ัƒะถะต ะตัั‚ัŒ ะฒ ะฐะปัŒะฑะพะผะต.", "home_page_add_to_album_err_local": "ะŸะพะบะฐ ะฝะตะปัŒะทั ะดะพะฑะฐะฒะปัั‚ัŒ ะปะพะบะฐะปัŒะฝั‹ะต ะพะฑัŠะตะบั‚ั‹ ะฒ ะฐะปัŒะฑะพะผั‹, ะฟั€ะพะฟัƒัะบะฐะตะผ", "home_page_add_to_album_success": "ะ”ะพะฑะฐะฒะปะตะฝะพ {added} ะพะฑัŠะตะบั‚ะพะฒ ะฒ ะฐะปัŒะฑะพะผ {album}.", - "home_page_album_err_partner": "ะŸะพะบะฐ ะฝะต ัƒะดะฐะตั‚ัั ะดะพะฑะฐะฒะธั‚ัŒ ะฟะฐั€ั‚ะฝะตั€ัะบะธะต ะฐะบั‚ะธะฒั‹ ะฒ ะฐะปัŒะฑะพะผ, ะฟั€ะพะฟัƒัะบ...", + "home_page_album_err_partner": "ะŸะพะบะฐ ะฝะต ัƒะดะฐะตั‚ัั ะดะพะฑะฐะฒะธั‚ัŒ ะพะฑัŠะตะบั‚ั‹ ะฟะฐั€ั‚ะฝะตั€ะฐ ะฒ ะฐะปัŒะฑะพะผ, ะฟั€ะพะฟัƒัะบ...", "home_page_archive_err_local": "ะŸะพะบะฐ ะฝะตะฒะพะทะผะพะถะฝะพ ะดะพะฑะฐะฒะธั‚ัŒ ะปะพะบะฐะปัŒะฝั‹ะต ะพะฑัŠะตะบั‚ั‹ ะฒ ะฐั€ั…ะธะฒ, ะฟั€ะพะฟัƒัะบะฐะตะผ", - "home_page_archive_err_partner": "ะะตะฒะพะทะผะพะถะฝะพ ะฐั€ั…ะธะฒะธั€ะพะฒะฐั‚ัŒ ะฐะบั‚ะธะฒั‹ ะฟะฐั€ั‚ะฝะตั€ะพะฒ, ะฟั€ะพะฟัƒัะบ...", + "home_page_archive_err_partner": "ะะตะฒะพะทะผะพะถะฝะพ ะฐั€ั…ะธะฒะธั€ะพะฒะฐั‚ัŒ ะพะฑัŠะตะบั‚ั‹ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒัะบ...", "home_page_building_timeline": "ะŸะพัั‚ั€ะพะตะฝะธะต ะฒั€ะตะผะตะฝะฝะพะน ัˆะบะฐะปั‹", - "home_page_delete_err_partner": "ะะตะฒะพะทะผะพะถะฝะพ ัƒะดะฐะปะธั‚ัŒ ะฐะบั‚ะธะฒั‹ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒัะบ...", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "ะŸะพะบะฐ ะฝะต ัƒะดะฐะตั‚ัั ะดะพะฑะฐะฒะธั‚ัŒ ะฒ ะธะทะฑั€ะฐะฝะฝะพะต ะปะพะบะฐะปัŒะฝั‹ะต ะพะฑัŠะตะบั‚ั‹, ะฟั€ะพะฟัƒัะบะฐะตะผ", - "home_page_favorite_err_partner": "ะŸะพะบะฐ ะฝะต ัƒะดะฐะตั‚ัั ะฒั‹ะดะตะปะธั‚ัŒ ะฟะฐั€ั‚ะฝะตั€ัะบะธะต ะฐะบั‚ะธะฒั‹, ะฟั€ะพะฟัƒัะบ...", + "home_page_delete_err_partner": "ะะตะฒะพะทะผะพะถะฝะพ ัƒะดะฐะปะธั‚ัŒ ะพะฑัŠะตะบั‚ั‹ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒัะบ...", + "home_page_delete_remote_err_local": "ะ›ะพะบะฐะปัŒะฝั‹ะต ะพะฑัŠะตะบั‚(ั‹) ัƒะถะต ะฒ ะฟั€ะพั†ะตััะต ัƒะดะฐะปะตะฝะธั ั ัะตั€ะฒะตั€ะฐ, ะฟั€ะพะฟัƒัะบ...", + "home_page_favorite_err_local": "ะŸะพะบะฐ ะฝะต ัƒะดะฐะตั‚ัั ะดะพะฑะฐะฒะธั‚ัŒ ะฒ ะธะทะฑั€ะฐะฝะฝะพะต ะปะพะบะฐะปัŒะฝั‹ะต ะพะฑัŠะตะบั‚ั‹, ะฟั€ะพะฟัƒัะบ...", + "home_page_favorite_err_partner": "ะŸะพะบะฐ ะฝะต ัƒะดะฐะตั‚ัั ะดะพะฑะฐะฒะธั‚ัŒ ะฒ ะธะทะฑั€ะฐะฝะฝะพะต ะพะฑัŠะตะบั‚ั‹ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒัะบ...", "home_page_first_time_notice": "ะ•ัะปะธ ะฒั‹ ะธัะฟะพะปัŒะทัƒะตั‚ะต ะฟั€ะธะปะพะถะตะฝะธะต ะฒะฟะตั€ะฒั‹ะต, ัƒะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะฒั‹ ะฒั‹ะฑั€ะฐะปะธ ั€ะตะทะตั€ะฒะฝั‹ะน(ะต) ะฐะปัŒะฑะพะผ(ั‹), ั‡ั‚ะพะฑั‹ ะฒั€ะตะผะตะฝะฝะฐั ัˆะบะฐะปะฐ ะผะพะณะปะฐ ะทะฐะฟะพะปะฝะธั‚ัŒ ั„ะพั‚ะพะณั€ะฐั„ะธะธ ะธ ะฒะธะดะตะพ ะฒ ะฐะปัŒะฑะพะผะต(ะฐั…).", "home_page_share_err_local": "ะะตะฒะพะทะผะพะถะฝะพ ะฟะพะดะตะปะธั‚ัŒัั ะปะพะบะฐะปัŒะฝั‹ะผะธ ะดะฐะฝะฝั‹ะผะธ ะฟะพ ััั‹ะปะบะต, ะฟั€ะพะฟัƒัะบ...", "home_page_upload_err_limit": "ะ’ั‹ ะผะพะถะตั‚ะต ะฒั‹ะณั€ัƒะทะธั‚ัŒ ะผะฐะบัะธะผัƒะผ 30 ั„ะฐะนะปะพะฒ ะทะฐ ั€ะฐะท", "image_viewer_page_state_provider_download_error": "ะžัˆะธะฑะบะฐ ะทะฐะณั€ัƒะทะบะธ", "image_viewer_page_state_provider_download_success": "ะฃัะฟะตัˆะฝะพ ะทะฐะณั€ัƒะถะตะฝะพ", - "image_viewer_page_state_provider_share_error": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะฟัƒะฑะปะธะบะฐั†ะธะธ", + "image_viewer_page_state_provider_share_error": "ะžัˆะธะฑะบะฐ ะพะฑั‰ะตะณะพ ะดะพัั‚ัƒะฟะฐ", "library_page_albums": "ะะปัŒะฑะพะผั‹", "library_page_archive": "ะั€ั…ะธะฒ", "library_page_device_albums": "ะะปัŒะฑะพะผั‹ ะฝะฐ ัƒัั‚ั€ะพะนัั‚ะฒะต", "library_page_favorites": "ะ˜ะทะฑั€ะฐะฝะฝะพะต", "library_page_new_album": "ะะพะฒั‹ะน ะฐะปัŒะฑะพะผ", "library_page_sharing": "ะžะฑั‰ะธะต", - "library_page_sort_asset_count": "Number of assets", - "library_page_sort_created": "ะŸะพ ะฝะพะฒะธะทะฝะต", + "library_page_sort_asset_count": "ะšะพะปะธั‡ะตัั‚ะฒะพ ะพะฑัŠะตะบั‚ะพะฒ", + "library_page_sort_created": "ะะตะดะฐะฒะฝะพ ัะพะทะดะฐะฝะฝั‹ะต", "library_page_sort_last_modified": "ะŸะพัะปะตะดะฝะตะต ะธะทะผะตะฝะตะฝะธะต", - "library_page_sort_most_oldest_photo": "Oldest photo", - "library_page_sort_most_recent_photo": "ะŸะพัะปะตะดะฝัั ั„ะพั‚ะพะณั€ะฐั„ะธั", - "library_page_sort_title": "ะŸะพ ะฝะฐะทะฒะฐะฝะธัŽ ะฐะปัŒะฑะพะผะฐ", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "library_page_sort_most_oldest_photo": "ะกะฐะผั‹ะต ัั‚ะฐั€ั‹ะต ั„ะพั‚ะพ", + "library_page_sort_most_recent_photo": "ะกะฐะผั‹ะต ะฟะพัะปะตะดะฝะธะต ั„ะพั‚ะพ", + "library_page_sort_title": "ะะฐะทะฒะฐะฝะธะต ะฐะปัŒะฑะพะผะฐ", + "location_picker_choose_on_map": "ะ’ั‹ะฑั€ะฐั‚ัŒ ะฝะฐ ะบะฐั€ั‚ะต", + "location_picker_latitude": "ะจะธั€ะพั‚ะฐ", + "location_picker_latitude_error": "ะฃะบะฐะถะธั‚ะต ะฟั€ะฐะฒะธะปัŒะฝัƒัŽ ัˆะธั€ะพั‚ัƒ", + "location_picker_latitude_hint": "ะฃะบะฐะถะธั‚ะต ัˆะธั€ะพั‚ัƒ", + "location_picker_longitude": "ะ”ะพะปะณะพั‚ะฐ", + "location_picker_longitude_error": "ะฃะบะฐะถะธั‚ะต ะฟั€ะฐะฒะธะปัŒะฝัƒัŽ ะดะพะปะณะพั‚ัƒ", + "location_picker_longitude_hint": "ะฃะบะฐะถะธั‚ะต ะดะพะปะณะพั‚ัƒ", "login_disabled": "ะ’ั…ะพะด ะพั‚ะบะปัŽั‡ะตะฝ", "login_form_api_exception": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะฟะพะฟั‹ั‚ะบะต ะฒะทะฐะธะผะพะดะตะนัั‚ะฒะธั ั ัะตั€ะฒะตั€ะพะผ. ะŸั€ะพะฒะตั€ัŒั‚ะต URL-ะฐะดั€ะตั ะดะพ ะฝะตะณะพ ะธ ะฟะพะฟั€ะพะฑัƒะนั‚ะต ะตั‰ะต ั€ะฐะท.", "login_form_back_button_text": "ะะฐะทะฐะด", @@ -252,35 +253,35 @@ "login_form_server_error": "ะะตั‚ ัะพะตะดะธะฝะตะฝะธั ั ัะตั€ะฒะตั€ะพะผ.", "login_password_changed_error": "ะŸั€ะพะธะทะพัˆะปะฐ ะพัˆะธะฑะบะฐ ะฟั€ะธ ะพะฑะฝะพะฒะปะตะฝะธะธ ะฟะฐั€ะพะปั", "login_password_changed_success": "ะŸะฐั€ะพะปัŒ ัƒัะฟะตัˆะฝะพ ะพะฑะฝะพะฒะปะตะฝ", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bound": "{} ั„ะพั‚ะพ", + "map_assets_in_bounds": "{} ั„ะพั‚ะพ", "map_cannot_get_user_location": "ะะตะฒะพะทะผะพะถะฝะพ ะฟะพะปัƒั‡ะธั‚ัŒ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธะต ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั", "map_location_dialog_cancel": "ะžั‚ะผะตะฝะฐ", "map_location_dialog_yes": "ะ”ะฐ", - "map_location_picker_page_use_location": "Use this location", + "map_location_picker_page_use_location": "ะญั‚ะพ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธะต", "map_location_service_disabled_content": "ะ”ะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั ะพะฑัŠะตะบั‚ะพะฒ ะฒ ะดะฐะฝะฝะพะผ ะผะตัั‚ะต ะฝะตะพะฑั…ะพะดะธะผะพ ะฒะบะปัŽั‡ะธั‚ัŒ ัะปัƒะถะฑัƒ ะพะฟั€ะตะดะตะปะตะฝะธั ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธั. ะฅะพั‚ะธั‚ะต ะฒะบะปัŽั‡ะธั‚ัŒ ะตะต ัะตะนั‡ะฐั?", "map_location_service_disabled_title": "ะกะปัƒะถะฑะฐ ะพะฟั€ะตะดะตะปะตะฝะธั ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธั ะพั‚ะบะปัŽั‡ะตะฝะฐ", "map_no_assets_in_bounds": "ะะตั‚ ั„ะพั‚ะพะณั€ะฐั„ะธะน ะฒ ัั‚ะพะน ะพะฑะปะฐัั‚ะธ", "map_no_location_permission_content": "ะ”ะปั ะพั‚ะพะฑั€ะฐะถะตะฝะธั ะพะฑัŠะตะบั‚ะพะฒ ะธะท ั‚ะตะบัƒั‰ะตะณะพ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธั ะฝะตะพะฑั…ะพะดะธะผะพ ั€ะฐะทั€ะตัˆะตะฝะธะต ะฝะฐ ะพะฟั€ะตะดะตะปะตะฝะธะต ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธั. ะฅะพั‚ะธั‚ะต ะปะธ ะฒั‹ ั€ะฐะทั€ะตัˆะธั‚ัŒ ะตะณะพ ัะตะนั‡ะฐั?", "map_no_location_permission_title": "ะ”ะพัั‚ัƒะฟ ะบ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธัŽ ะพั‚ะบะปะพะฝะตะฝ", "map_settings_dark_mode": "ะขะตะผะฝั‹ะน ั€ะตะถะธะผ", - "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_all": "ะ’ัะต", + "map_settings_date_range_option_day": "ะŸั€ะพัˆะปั‹ะต 24 ั‡ะฐัะฐ", + "map_settings_date_range_option_days": "ะŸั€ะพัˆะปั‹ะต {} ะดะฝะตะน", + "map_settings_date_range_option_year": "ะŸั€ะพัˆะปั‹ะน ะณะพะด", + "map_settings_date_range_option_years": "ะŸั€ะพัˆะปั‹ะต {} ะณะพะดะฐ", "map_settings_dialog_cancel": "ะžั‚ะผะตะฝะฐ", "map_settings_dialog_save": "ะกะพั…ั€ะฐะฝะธั‚ัŒ", "map_settings_dialog_title": "ะะฐัั‚ั€ะพะนะบะธ ะบะฐั€ั‚ั‹", "map_settings_include_show_archived": "ะ’ะบะปัŽั‡ะธั‚ัŒ ะฐั€ั…ะธะฒะฝั‹ะต ะดะฐะฝะฝั‹ะต", "map_settings_only_relative_range": "ะŸะตั€ะธะพะด ะฒั€ะตะผะตะฝะธ", "map_settings_only_show_favorites": "ะŸะพะบะฐะทะฐั‚ัŒ ั‚ะพะปัŒะบะพ ะธะทะฑั€ะฐะฝะฝะพะต", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "ะขะตะผะฐ ะบะฐั€ั‚ั‹", "map_zoom_to_see_photos": "ะฃะผะตะฝัŒัˆะตะฝะธะต ะผะฐััˆั‚ะฐะฑะฐ ะดะปั ะฟั€ะพัะผะพั‚ั€ะฐ ั„ะพั‚ะพะณั€ะฐั„ะธะน", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "ะ”ะธะฝะฐะผะธั‡ะตัะบะธะต ั„ะพั‚ะพ", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "ะะตะฒะพะทะผะพะถะฝะพ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะดะฐั‚ัƒ ะพะฑัŠะตะบั‚ะพะฒ ั‚ะพะปัŒะบะพ ะดะปั ั‡ั‚ะตะฝะธั, ะฟั€ะพะฟัƒัะบ...", + "multiselect_grid_edit_gps_err_read_only": "ะะตะฒะพะทะผะพะถะฝะพ ั€ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะผะตัั‚ะพะฟะพะปะพะถะตะฝะธะต ะพะฑัŠะตะบั‚ะพะฒ ั‚ะพะปัŒะบะพ ะดะปั ั‡ั‚ะตะฝะธั, ะฟั€ะพะฟัƒัะบ...", "notification_permission_dialog_cancel": "ะžั‚ะผะตะฝะฐ", "notification_permission_dialog_content": "ะงั‚ะพะฑั‹ ะฒะบะปัŽั‡ะธั‚ัŒ ัƒะฒะตะดะพะผะปะตะฝะธั, ะฟะตั€ะตะนะดะธั‚ะต ะฒ ยซะะฐัั‚ั€ะพะนะบะธยป ะธ ะฒั‹ะฑะตั€ะธั‚ะต ยซะ ะฐะทั€ะตัˆะธั‚ัŒยป.", "notification_permission_dialog_settings": "ะะฐัั‚ั€ะพะนะบะธ", @@ -318,7 +319,7 @@ "profile_drawer_sign_out": "ะ’ั‹ะนั‚ะธ", "profile_drawer_trash": "ะšะพั€ะทะธะฝะฐ", "recently_added_page_title": "ะะตะดะฐะฒะฝะพ ะดะพะฑะฐะฒะปะตะฝะฝั‹ะต", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "ะ’ะพะทะฝะธะบะปะฐ ะพัˆะธะฑะบะฐ", "search_bar_hint": "ะŸะพะธัะบ ั„ะพั‚ะพะณั€ะฐั„ะธะน", "search_page_categories": "ะšะฐั‚ะตะณะพั€ะธะธ", "search_page_favorites": "ะ˜ะทะฑั€ะฐะฝะฝะพะต", @@ -332,31 +333,31 @@ "search_page_person_add_name_dialog_title": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะธะผั", "search_page_person_add_name_subtitle": "ะ‘ั‹ัั‚ั€ะพ ะฝะฐะนะดะธั‚ะต ะธั… ะฟะพ ะธะผะตะฝะธ ั ะฟะพะผะพั‰ัŒัŽ ะฟะพะธัะบะฐ", "search_page_person_add_name_title": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะธะผั", - "search_page_person_edit_name": "ะ˜ะทะผะตะฝะธั‚ัŒ ะธะผั", + "search_page_person_edit_name": "ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ะธะผั", "search_page_places": "ะœะตัั‚ะฐ", "search_page_recently_added": "ะะตะดะฐะฒะฝะพ ะดะพะฑะฐะฒะปะตะฝะฝั‹ะต", - "search_page_screenshots": "ะกะบั€ะธะฝัˆะพั‚ั‹", + "search_page_screenshots": "ะกะฝะธะผะบะธ ัะบั€ะฐะฝะฐ", "search_page_selfies": "ะกะตะปั„ะธ", "search_page_things": "ะŸั€ะตะดะผะตั‚ั‹", "search_page_videos": "ะ’ะธะดะตะพ", "search_page_view_all_button": "ะŸะพัะผะพั‚ั€ะตั‚ัŒ ะฒัะต", - "search_page_your_activity": "ะ’ะฐัˆะฐ ะฐะบั‚ะธะฒะฝะพัั‚ัŒ", + "search_page_your_activity": "ะ’ะฐัˆะธ ะดะตะนัั‚ะฒะธั", "search_page_your_map": "ะ’ะฐัˆะฐ ะบะฐั€ั‚ะฐ", "search_result_page_new_search_hint": "ะะพะฒั‹ะน ะฟะพะธัะบ", "search_suggestion_list_smart_search_hint_1": "ะ˜ะฝั‚ะตะปะปะตะบั‚ัƒะฐะปัŒะฝั‹ะน ะฟะพะธัะบ ะฒะบะปัŽั‡ะตะฝ ะฟะพ ัƒะผะพะปั‡ะฐะฝะธัŽ, ะดะปั ะฟะพะธัะบะฐ ะผะตั‚ะฐะดะฐะฝะฝั‹ั… ะธัะฟะพะปัŒะทัƒะนั‚ะต ัะฟะตั†ะธะฐะปัŒะฝั‹ะน ัะธะฝั‚ะฐะบัะธั", - "search_suggestion_list_smart_search_hint_2": "m:ะฒะฐัˆ-ะทะฐะฟั€ะพั", + "search_suggestion_list_smart_search_hint_2": "m:ะฒะฐัˆ-ะฟะพะธัะบะพะฒั‹ะน-ะทะฐะฟั€ะพั", "select_additional_user_for_sharing_page_suggestions": "ะŸั€ะตะดะปะพะถะตะฝะธั", - "select_user_for_sharing_page_err_album": "\nะะต ัƒะดะฐะปะพััŒ ัะพะทะดะฐั‚ัŒ ะฐะปัŒะฑะพะผ", + "select_user_for_sharing_page_err_album": "ะะต ัƒะดะฐะปะพััŒ ัะพะทะดะฐั‚ัŒ ะฐะปัŒะฑะพะผ", "select_user_for_sharing_page_share_suggestions": "ะŸั€ะตะดะปะพะถะตะฝะธั", "server_info_box_app_version": "ะ’ะตั€ัะธั ะฟั€ะธะปะพะถะตะฝะธั", - "server_info_box_latest_release": "ะšั€ะฐะนะฝัั ะฒะตั€ัะธั", + "server_info_box_latest_release": "ะŸะพัะปะตะดะฝัั ะฒะตั€ัะธั", "server_info_box_server_url": "URL ัะตั€ะฒะตั€ะฐ", "server_info_box_server_version": "ะ’ะตั€ัะธั ัะตั€ะฒะตั€ะฐ", - "setting_image_viewer_help": "ะกั€ะตะดัั‚ะฒะพ ะฟั€ะพัะผะพั‚ั€ะฐ ะดะตั‚ะฐะปะตะน ัะฝะฐั‡ะฐะปะฐ ะทะฐะณั€ัƒะถะฐะตั‚ ะผะฐะปะตะฝัŒะบัƒัŽ ะผะธะฝะธะฐั‚ัŽั€ัƒ, ะทะฐั‚ะตะผ ะทะฐะณั€ัƒะถะฐะตั‚ ะฟั€ะตะดะฒะฐั€ะธั‚ะตะปัŒะฝั‹ะน ะฟั€ะพัะผะพั‚ั€ ัั€ะตะดะฝะตะณะพ ั€ะฐะทะผะตั€ะฐ (ะตัะปะธ ะฒะบะปัŽั‡ะตะฝะพ) ะธ, ะฝะฐะบะพะฝะตั†, ะทะฐะณั€ัƒะถะฐะตั‚ ะพั€ะธะณะธะฝะฐะป (ะตัะปะธ ะฒะบะปัŽั‡ะตะฝะพ).", - "setting_image_viewer_original_subtitle": "ะ’ะบะปัŽั‡ะธั‚ะต ะทะฐะณั€ัƒะทะบัƒ ะพั€ะธะณะธะฝะฐะปัŒะฝะพะณะพ ะธะทะพะฑั€ะฐะถะตะฝะธั ะฒ ะฟะพะปะฝะพะผ ั€ะฐะทั€ะตัˆะตะฝะธะธ (ะฑะพะปัŒัˆะพะต!). ะžั‚ะบะปัŽั‡ะธั‚ะต, ั‡ั‚ะพะฑั‹ ัƒะผะตะฝัŒัˆะธั‚ัŒ ะพะฑัŠะตะผ ะดะฐะฝะฝั‹ั… (ะบะฐะบ ะฒ ัะตั‚ะธ, ั‚ะฐะบ ะธ ะฒ ะบะตัˆะต ัƒัั‚ั€ะพะนัั‚ะฒะฐ).", - "setting_image_viewer_original_title": "ะ—ะฐะณั€ัƒะทะธั‚ัŒ ะธัั…ะพะดะฝะพะต ะธะทะพะฑั€ะฐะถะตะฝะธะต", - "setting_image_viewer_preview_subtitle": "ะ’ะบะปัŽั‡ะธั‚ะต ะทะฐะณั€ัƒะทะบัƒ ะธะทะพะฑั€ะฐะถะตะฝะธั ัั€ะตะดะฝะตะณะพ ั€ะฐะทั€ะตัˆะตะฝะธั. ะžั‚ะบะปัŽั‡ะธั‚ะต, ั‡ั‚ะพะฑั‹ ะทะฐะณั€ัƒะทะธั‚ัŒ ะพั€ะธะณะธะฝะฐะป ะฝะฐะฟั€ัะผัƒัŽ ะธะปะธ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ั‚ะพะปัŒะบะพ ะผะธะฝะธะฐั‚ัŽั€ัƒ.", - "setting_image_viewer_preview_title": "ะ—ะฐะณั€ัƒะทะธั‚ัŒ ะธะทะพะฑั€ะฐะถะตะฝะธะต ะดะปั ะฟั€ะตะดะฒะฐั€ะธั‚ะตะปัŒะฝะพะณะพ ะฟั€ะพัะผะพั‚ั€ะฐ", + "setting_image_viewer_help": "ะŸะพะปะฝะพัะบั€ะฐะฝะฝั‹ะน ะฟั€ะพัะผะพั‚ั€ั‰ะธะบ ัะฝะฐั‡ะฐะปะฐ ะทะฐะณั€ัƒะถะฐะตั‚ ะธะทะพะฑั€ะฐะถะตะฝะธะต ะดะปั ะฟั€ะตะดะฟั€ะพัะผะพั‚ั€ะฐ ะฒ ะฝะธะทะบะพะผ ั€ะฐะทั€ะตัˆะตะฝะธะธ, ะทะฐั‚ะตะผ ะทะฐะณั€ัƒะถะฐะตั‚ ะธะทะพะฑั€ะฐะถะตะฝะธะต ะฒ ัƒะผะตะฝัŒัˆะตะฝะฝะพะผ ั€ะฐะทั€ะตัˆะตะฝะธะธ ะพั‚ะฝะพัะธั‚ะตะปัŒะฝะพ ะพั€ะธะณะธะฝะฐะปะฐ (ะตัะปะธ ะฒะบะปัŽั‡ะตะฝะพ) ะธ ะฒ ะบะพะฝั†ะต ะบะพะฝั†ะพะฒ ะทะฐะณั€ัƒะถะฐะตั‚ ะพั€ะธะณะธะฝะฐะป (ะตัะปะธ ะฒะบะปัŽั‡ะตะฝะพ).", + "setting_image_viewer_original_subtitle": "ะ’ะบะปัŽั‡ะธั‚ะต ะดะปั ะทะฐะณั€ัƒะทะบะธ ะธัั…ะพะดะฝะพะณะพ ะธะทะพะฑั€ะฐะถะตะฝะธั ะฒ ะฟะพะปะฝะพะผ ั€ะฐะทั€ะตัˆะตะฝะธะธ (ะฑะพะปัŒัˆะพะต!).\nะžั‚ะบะปัŽั‡ะธั‚ะต, ั‡ั‚ะพะฑั‹ ัƒะผะตะฝัŒัˆะธั‚ัŒ ะพะฑัŠะตะผ ะดะฐะฝะฝั‹ั… (ะบะฐะบ ัะตั‚ะธ, ั‚ะฐะบ ะธ ะบััˆะฐ ัƒัั‚ั€ะพะนัั‚ะฒะฐ).", + "setting_image_viewer_original_title": "ะ—ะฐะณั€ัƒะถะฐั‚ัŒ ะธัั…ะพะดะฝะพะต ะธะทะพะฑั€ะฐะถะตะฝะธะต", + "setting_image_viewer_preview_subtitle": "ะ’ะบะปัŽั‡ะธั‚ะต ะดะปั ะทะฐะณั€ัƒะทะบะธ ะธะทะพะฑั€ะฐะถะตะฝะธั ัั€ะตะดะฝะตะณะพ ั€ะฐะทั€ะตัˆะตะฝะธั.\nะžั‚ะบะปัŽั‡ะธั‚ะต, ั‡ั‚ะพะฑั‹ ะทะฐะณั€ัƒะถะฐั‚ัŒ ะพั€ะธะณะธะฝะฐะป ะฝะฐะฟั€ัะผัƒัŽ ะธะปะธ ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ั‚ะพะปัŒะบะพ ะผะธะฝะธะฐั‚ัŽั€ัƒ.", + "setting_image_viewer_preview_title": "ะ—ะฐะณั€ัƒะถะฐั‚ัŒ ะธะทะพะฑั€ะฐะถะตะฝะธะต ะดะปั ะฟั€ะตะดะฒะฐั€ะธั‚ะตะปัŒะฝะพะณะพ ะฟั€ะพัะผะพั‚ั€ะฐ", "setting_notifications_notify_failures_grace_period": "ะฃะฒะตะดะพะผะปัั‚ัŒ ะพะฑ ะพัˆะธะฑะบะฐั… ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟะธั€ะพะฒะฐะฝะธั: {}", "setting_notifications_notify_hours": "{} ั‡ะฐัะพะฒ", "setting_notifications_notify_immediately": "ะฝะตะผะตะดะปะตะฝะฝะพ", @@ -365,7 +366,7 @@ "setting_notifications_notify_seconds": "{} ัะตะบัƒะฝะด", "setting_notifications_single_progress_subtitle": "ะŸะพะดั€ะพะฑะฝะฐั ะธะฝั„ะพั€ะผะฐั†ะธั ะพ ั…ะพะดะต ะทะฐะณั€ัƒะทะบะธ ะดะปั ะบะฐะถะดะพะณะพ ะพะฑัŠะตะบั‚ะฐ", "setting_notifications_single_progress_title": "ะŸะพะบะฐะทะฐั‚ัŒ ั…ะพะด ะฒั‹ะฟะพะปะฝะตะฝะธั ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟะธั€ะพะฒะฐะฝะธั", - "setting_notifications_subtitle": "ะะฐัั‚ั€ะพะธั‚ัŒ ะฟะฐั€ะฐะผะตั‚ั€ั‹ ัƒะฒะตะดะพะผะปะตะฝะธะน", + "setting_notifications_subtitle": "ะะฐัั‚ั€ะพะนะบะฐ ะฟะฐั€ะฐะผะตั‚ั€ะพะฒ ัƒะฒะตะดะพะผะปะตะฝะธ", "setting_notifications_title": "ะฃะฒะตะดะพะผะปะตะฝะธั", "setting_notifications_total_progress_subtitle": "ะžะฑั‰ะธะน ะฟั€ะพะณั€ะตัั ะทะฐะณั€ัƒะทะบะธ (ะฒั‹ะฟะพะปะฝะตะฝะพ/ะฒัะตะณะพ ะพะฑัŠะตะบั‚ะพะฒ)", "setting_notifications_total_progress_title": "ะŸะพะบะฐะทะฐั‚ัŒ ะพะฑั‰ะธะน ะฟั€ะพะณั€ะตัั ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟะธั€ะพะฒะฐะฝะธั", @@ -375,61 +376,61 @@ "share_add_photos": "ะ”ะพะฑะฐะฒะธั‚ัŒ ั„ะพั‚ะพ", "share_add_title": "ะ”ะพะฑะฐะฒะธั‚ัŒ ะฝะฐะทะฒะฐะฝะธะต", "share_create_album": "ะกะพะทะดะฐั‚ัŒ ะฐะปัŒะฑะพะผ", - "shared_album_activities_input_disable": "ะšะพะผะผะตะฝั‚ะฐั€ะธะน ะพั‚ะบะปัŽั‡ะตะฝ", + "shared_album_activities_input_disable": "ะšะพะผะผะตะฝั‚ะธั€ะพะฒะฐะฝะธะต ะพั‚ะบะปัŽั‡ะตะฝะพ", "shared_album_activities_input_hint": "ะกะบะฐะถะธั‚ะต ั‡ั‚ะพ-ะฝะธะฑัƒะดัŒ", - "shared_album_activity_remove_content": "ะฅะพั‚ะธั‚ะต ะปะธ ะ’ั‹ ัƒะดะฐะปะธั‚ัŒ ัั‚ะพ ะดะตะนัั‚ะฒะธะต?", - "shared_album_activity_remove_title": "ะฃะดะฐะปะธั‚ัŒ ะดะตะนัั‚ะฒะธะต", - "shared_album_activity_setting_subtitle": "ะŸั€ะตะดะพัั‚ะฐะฒัŒั‚ะต ะดั€ัƒะณะธะผ ะฒะพะทะผะพะถะฝะพัั‚ัŒ ะพั‚ะฒะตั‡ะฐั‚ัŒ", + "shared_album_activity_remove_content": "ะฅะพั‚ะธั‚ะต ะปะธ ะ’ั‹ ัƒะดะฐะปะธั‚ัŒ ัั‚ะพ ัะพะพะฑั‰ะตะฝะธะต?", + "shared_album_activity_remove_title": "ะฃะดะฐะปะธั‚ัŒ ัะพะพะฑั‰ะตะฝะธะต", + "shared_album_activity_setting_subtitle": "ะ ะฐะทั€ะตัˆะธั‚ัŒ ะดั€ัƒะณะธะผ ะพั‚ะฒะตั‡ะฐั‚", "shared_album_activity_setting_title": "ะšะพะผะผะตะฝั‚ะฐั€ะธะธ ะธ ะปะฐะนะบะธ", - "shared_album_section_people_action_error": "Error leaving/removing from album", + "shared_album_section_people_action_error": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ะฒั‹ั…ะพะดะต/ัƒะดะฐะปะตะฝะธะธ ะธะท ะฐะปัŒะฑะพะผะฐ", "shared_album_section_people_action_leave": "ะฃะดะฐะปะธั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะธะท ะฐะปัŒะฑะพะผะฐ", "shared_album_section_people_action_remove_user": "ะฃะดะฐะปะธั‚ัŒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปั ะธะท ะฐะปัŒะฑะพะผะฐ", - "shared_album_section_people_owner_label": "Owner", - "shared_album_section_people_title": "PEOPLE", + "shared_album_section_people_owner_label": "ะ’ะปะฐะดะตะปะตั†", + "shared_album_section_people_title": "ะ›ะฎะ”ะ˜", "share_dialog_preparing": "ะŸะพะดะณะพั‚ะพะฒะบะฐ...", "shared_link_app_bar_title": "ะžะฑั‰ะธะต ััั‹ะปะบะธ", "shared_link_clipboard_copied_massage": "ะกะบะพะฟะธั€ะพะฒะฐะฝะพ ะฒ ะฑัƒั„ะตั€ ะพะฑะผะตะฝะฐ", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_app_bar_title": "ะกะพะทะดะฐั‚ัŒ ััั‹ะปะบัƒ ะดะปั ัะพะฒะผะตัั‚ะฝะพะณะพ ะธัะฟะพะปัŒะทะพะฒะฐะฝะธั", - "shared_link_create_error": "Error while creating shared link", - "shared_link_create_info": "ะŸะพะทะฒะพะปะธั‚ัŒ ะปัŽะฑะพะผัƒ ั‡ะตะปะพะฒะตะบัƒ, ะธะผะตัŽั‰ะตะผัƒ ััั‹ะปะบัƒ, ัƒะฒะธะดะตั‚ัŒ ะฒั‹ะฑั€ะฐะฝะฝัƒัŽ ั„ะพั‚ะพะณั€ะฐั„ะธัŽ (ั„ะพั‚ะพะณั€ะฐั„ะธะธ)", + "shared_link_clipboard_text": "ะกัั‹ะปะบะฐ: {}\nะŸะฐั€ะพะปัŒ: {}", + "shared_link_create_app_bar_title": "ะกะพะทะดะฐั‚ัŒ ััั‹ะปะบัƒ ะพะฑั‰ะตะณะพ ะดะพัั‚ัƒะฟะฐ", + "shared_link_create_error": "ะžัˆะธะฑะบะฐ ะฟั€ะธ ัะพะทะดะฐะฝะธะธ ะพะฑั‰ะตะน ััั‹ะปะบะธ", + "shared_link_create_info": "ะ ะฐะทั€ะตัˆะธั‚ัŒ ะฒัะตะผ, ัƒ ะบะพะณะพ ะตัั‚ัŒ ััั‹ะปะบะฐ, ะฟั€ะพัะผะฐั‚ั€ะธะฒะฐั‚ัŒ ะฒั‹ะฑั€ะฐะฝะฝั‹ะต ั„ะพั‚ะพ", "shared_link_create_submit_button": "ะกะพะทะดะฐั‚ัŒ ััั‹ะปะบัƒ", - "shared_link_edit_allow_download": "ะ ะฐะทั€ะตัˆะธั‚ัŒ ะฟัƒะฑะปะธั‡ะฝะพะผัƒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŽ ัะบะฐั‡ะธะฒะฐั‚ัŒ", + "shared_link_edit_allow_download": "ะ ะฐะทั€ะตัˆะธั‚ัŒ ะฟัƒะฑะปะธั‡ะฝะพะผัƒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŽ ัะบะฐั‡ะธะฒะฐั‚ัŒ ั„ะฐะนะปั‹", "shared_link_edit_allow_upload": "ะ ะฐะทั€ะตัˆะธั‚ัŒ ะฟัƒะฑะปะธั‡ะฝะพะผัƒ ะฟะพะปัŒะทะพะฒะฐั‚ะตะปัŽ ะทะฐะณั€ัƒะถะฐั‚ัŒ ั„ะฐะนะปั‹", "shared_link_edit_app_bar_title": "ะ ะตะดะฐะบั‚ะธั€ะพะฒะฐั‚ัŒ ััั‹ะปะบัƒ", "shared_link_edit_change_expiry": "ะ˜ะทะผะตะฝะธั‚ัŒ ัั€ะพะบ ะดะตะนัั‚ะฒะธั ะดะพัั‚ัƒะฟะฐ", "shared_link_edit_description": "ะžะฟะธัะฐะฝะธะต", - "shared_link_edit_description_hint": "ะ’ะฒะตะดะธั‚ะต ะพะฟะธัะฐะฝะธะต ัะพะฒะผะตัั‚ะฝะพะณะพ ะดะพัั‚ัƒะฟะฐ", - "shared_link_edit_expire_after": "ะ˜ัั‚ะตะบะฐะตั‚ ะฟะพัะปะต", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", + "shared_link_edit_description_hint": "ะ’ะฒะตะดะธั‚ะต ะพะฟะธัะฐะฝะธะต ะดะปั ะพะฑั‰ะตะณะพ ะดะพัั‚ัƒะฟะฐ", + "shared_link_edit_expire_after": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท", + "shared_link_edit_expire_after_option_day": "1 ะดะตะฝัŒ", + "shared_link_edit_expire_after_option_days": "{} ะดะฝะตะน", + "shared_link_edit_expire_after_option_hour": "1 ั‡ะฐั", + "shared_link_edit_expire_after_option_hours": "{} ั‡ะฐัะพะฒ", + "shared_link_edit_expire_after_option_minute": "1 ะผะธะฝัƒั‚ัƒ", + "shared_link_edit_expire_after_option_minutes": "{} ะผะธะฝัƒั‚", "shared_link_edit_expire_after_option_never": "ะะธะบะพะณะดะฐ", "shared_link_edit_password": "ะŸะฐั€ะพะปัŒ", - "shared_link_edit_password_hint": "ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะพะปัŒ ะพะฑั‰ะตะณะพ ะดะพัั‚ัƒะฟะฐ", - "shared_link_edit_show_meta": "ะŸะพะบะฐะทะฐั‚ัŒ ะผะตั‚ะฐะดะฐะฝะฝั‹ะต", + "shared_link_edit_password_hint": "ะ’ะฒะตะดะธั‚ะต ะฟะฐั€ะพะปัŒ ะดะปั ะพะฑั‰ะตะณะพ ะดะพัั‚ัƒะฟะฐ", + "shared_link_edit_show_meta": "ะŸะพะบะฐะทั‹ะฒะฐั‚ัŒ ะผะตั‚ะฐะดะฐะฝะฝั‹ะต", "shared_link_edit_submit_button": "ะžะฑะฝะพะฒะธั‚ัŒ ััั‹ะปะบัƒ", "shared_link_empty": "ะฃ ะฒะฐั ะฝะตั‚ ะพะฑั‰ะธั… ััั‹ะปะพะบ", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", + "shared_link_error_server_url_fetch": "ะะตะฒะพะทะผะพะถะฝะพ ะทะฐะฟั€ะพัะธั‚ัŒ URL ั ัะตั€ะฒะตั€ะฐ", + "shared_link_expired": "ะกั€ะพะบ ะดะตะนัั‚ะฒะธั ะธัั‚ะตะบ", + "shared_link_expires_day": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ะดะตะฝัŒ", + "shared_link_expires_days": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ะดะฝะตะน", + "shared_link_expires_hour": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ั‡ะฐั", + "shared_link_expires_hours": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ั‡ะฐัะพะฒ", + "shared_link_expires_minute": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ะผะธะฝัƒั‚ัƒ", "shared_link_expires_minutes": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ะผะธะฝัƒั‚", - "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", + "shared_link_expires_never": "ะ˜ัั‚ะตะบะฐะตั‚ โˆž", + "shared_link_expires_second": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ัะตะบัƒะฝะดัƒ", "shared_link_expires_seconds": "ะ˜ัั‚ะตะบะฐะตั‚ ั‡ะตั€ะตะท {} ัะตะบัƒะฝะด", - "shared_link_info_chip_download": "Download", + "shared_link_info_chip_download": "ะกะบะฐั‡ะฐั‚ัŒ", "shared_link_info_chip_metadata": "EXIF", - "shared_link_info_chip_upload": "Upload", + "shared_link_info_chip_upload": "ะ—ะฐะณั€ัƒะทะธั‚ัŒ", "shared_link_manage_links": "ะฃะฟั€ะฐะฒะปะตะฝะธะต ะพะฑั‰ะธะผะธ ััั‹ะปะบะฐะผะธ", - "share_done": "ะ’ั‹ะฟะพะปะฝะตะฝะพ", - "share_invite": "\nะŸั€ะธะณะปะฐัะธั‚ัŒ ะฒ ะฐะปัŒะฑะพะผ", + "share_done": "ะ“ะพั‚ะพะฒะพ", + "share_invite": "ะŸั€ะธะณะปะฐัะธั‚ัŒ ะฒ ะฐะปัŒะฑะพะผ", "sharing_page_album": "ะžะฑั‰ะธะต ะฐะปัŒะฑะพะผั‹", "sharing_page_description": "ะกะพะทะดะฐะฒะฐะนั‚ะต ะพะฑั‰ะธะต ะฐะปัŒะฑะพะผั‹, ั‡ั‚ะพะฑั‹ ะดะตะปะธั‚ัŒัั ั„ะพั‚ะพะณั€ะฐั„ะธัะผะธ ะธ ะฒะธะดะตะพ ั ะปัŽะดัŒะผะธ ะฒ ะฒะฐัˆะตะน ัะตั‚ะธ.", "sharing_page_empty_list": "ะŸะฃะกะขะžะ™ ะกะŸะ˜ะกะžะš", @@ -443,10 +444,10 @@ "theme_setting_asset_list_storage_indicator_title": "ะŸะพะบะฐะทะฐั‚ัŒ ะธะฝะดะธะบะฐั‚ะพั€ ั…ั€ะฐะฝะธะปะธั‰ะฐ ะฝะฐ ะฟะปะธั‚ะบะฐั… ะพะฑัŠะตะบั‚ะพะฒ", "theme_setting_asset_list_tiles_per_row_title": "ะšะพะปะธั‡ะตัั‚ะฒะพ ะพะฑัŠะตะบั‚ะพะฒ ะฒ ัั‚ั€ะพะบะต ({})", "theme_setting_dark_mode_switch": "ะขั‘ะผะฝะฐั ั‚ะตะผะฐ", - "theme_setting_image_viewer_quality_subtitle": "ะะฐัั‚ั€ะพะนะบะฐ ะบะฐั‡ะตัั‚ะฒะฐ ะดะตั‚ะฐะปัŒะฝะพะณะพ ะฟั€ะพัะผะพั‚ั€ะฐ ะธะทะพะฑั€ะฐะถะตะฝะธั", + "theme_setting_image_viewer_quality_subtitle": "ะะฐัั‚ั€ะพะนะบะฐ ะบะฐั‡ะตัั‚ะฒะฐ ะฟั€ะพัะผะพั‚ั€ะฐ ะฟะพะปะฝะพัะบั€ะฐะฝะฝั‹ั… ะธะทะพะฑั€ะฐะถะตะฝะธั", "theme_setting_image_viewer_quality_title": "ะšะฐั‡ะตัั‚ะฒะพ ะฟั€ะพัะผะพั‚ั€ะฐ ะธะทะพะฑั€ะฐะถะตะฝะธะน", - "theme_setting_system_theme_switch": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ (ะšะฐะบ ะฒ ัะธัั‚ะตะผะต)", - "theme_setting_theme_subtitle": "ะ’ั‹ะฑะตั€ะธั‚ะต ะฝะฐัั‚ั€ะพะนะบะธ ั‚ะตะผั‹ ะฟั€ะธะปะพะถะตะฝะธั", + "theme_setting_system_theme_switch": "ะะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ (ะบะฐะบ ะฒ ัะธัั‚ะตะผะต)", + "theme_setting_theme_subtitle": "ะะฐัั‚ั€ะพะนะบะฐ ั‚ะตะผั‹ ะฟั€ะธะปะพะถะตะฝะธั", "theme_setting_theme_title": "ะขะตะผะฐ", "theme_setting_three_stage_loading_subtitle": "ะขั€ะตั…ัั‚ะฐะฟะฝะฐั ะทะฐะณั€ัƒะทะบะฐ ะผะพะถะตั‚ ะฟะพะฒั‹ัะธั‚ัŒ ะฟั€ะพะธะทะฒะพะดะธั‚ะตะปัŒะฝะพัั‚ัŒ ะทะฐะณั€ัƒะทะบะธ, ะฝะพ ะฒั‹ะทั‹ะฒะฐะตั‚ ะทะฝะฐั‡ะธั‚ะตะปัŒะฝะพ ะฑะพะปะตะต ะฒั‹ัะพะบัƒัŽ ะฝะฐะณั€ัƒะทะบัƒ ะฝะฐ ัะตั‚ัŒ", "theme_setting_three_stage_loading_title": "ะ’ะบะปัŽั‡ะธั‚ัŒ ั‚ั€ะตั…ัั‚ะฐะฟะฝัƒัŽ ะทะฐะณั€ัƒะทะบัƒ", @@ -454,10 +455,10 @@ "trash_page_delete": "ะฃะดะฐะปะธั‚ัŒ", "trash_page_delete_all": "ะฃะดะฐะปะธั‚ัŒ ะฒัะต", "trash_page_empty_trash_btn": "ะžั‡ะธัั‚ะธั‚ัŒ ะบะพั€ะทะธะฝัƒ", - "trash_page_empty_trash_dialog_content": "ะ’ั‹ ั…ะพั‚ะธั‚ะต ะพั‡ะธัั‚ะธั‚ัŒ ัะฒะพัŽ ะบะพั€ะทะธะฝัƒ? ะญั‚ะธ ะพะฑัŠะตะบั‚ั‹ ะฑัƒะดัƒั‚ ะฝะฐะฒัะตะณะดะฐ ัƒะดะฐะปะตะฝั‹ ะธะท Immich", + "trash_page_empty_trash_dialog_content": "ะ’ั‹ ั…ะพั‚ะธั‚ะต ะพั‡ะธัั‚ะธั‚ัŒ ัะฒะพัŽ ะบะพั€ะทะธะฝัƒ? ะญั‚ะธ ะพะฑัŠะตะบั‚ั‹ ะฑัƒะดัƒั‚ ะฝะฐะฒัะตะณะดะฐ ัƒะดะฐะปะตะฝั‹ ะธะท Immich.", "trash_page_empty_trash_dialog_ok": "ะžะš", "trash_page_info": "ะฃะดะฐะปะตะฝะฝั‹ะต ัะปะตะผะตะฝั‚ั‹ ะฑัƒะดัƒั‚ ะพะบะพะฝั‡ะฐั‚ะตะปัŒะฝะพ ัƒะดะฐะปะตะฝั‹ ั‡ะตั€ะตะท {} ะดะฝะตะน", - "trash_page_no_assets": "ะžั‚ััƒั‚ัั‚ะฒะธะต ัƒะดะฐะปะตะฝะฝั‹ั… ะพะฑัŠะตะบั‚ะพะฒ", + "trash_page_no_assets": "ะฃะดะฐะปะตะฝะฝั‹ะต ะพะฑัŠะตะบั‚ั‹ ะพั‚ััƒั‚ัะฒัƒัŽั‚", "trash_page_restore": "ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ", "trash_page_restore_all": "ะ’ะพััั‚ะฐะฝะพะฒะธั‚ัŒ ะฒัะต", "trash_page_select_assets_btn": "ะ’ั‹ะฑั€ะฐะฝะฝั‹ะต ะพะฑัŠะตะบั‚ั‹", @@ -474,6 +475,6 @@ "version_announcement_overlay_text_3": " ะธ ัƒะฑะตะดะธั‚ะตััŒ, ั‡ั‚ะพ ะฒะฐัˆะธ ะฝะฐัั‚ั€ะพะนะบะธ docker-compose ะธ .env ะพะฑะฝะพะฒะปะตะฝั‹, ั‡ั‚ะพะฑั‹ ะฟั€ะตะดะพั‚ะฒั€ะฐั‚ะธั‚ัŒ ะปัŽะฑั‹ะต ะฝะตะฟั€ะฐะฒะธะปัŒะฝั‹ะต ะฝะฐัั‚ั€ะพะนะบะธ, ะพัะพะฑะตะฝะฝะพ ะตัะปะธ ะฒั‹ ะธัะฟะพะปัŒะทัƒะตั‚ะต WatchTower ะธะปะธ ะปัŽะฑะพะน ะดั€ัƒะณะพะน ะผะตั…ะฐะฝะธะทะผ, ะบะพั‚ะพั€ั‹ะน ะพะฑั€ะฐะฑะฐั‚ั‹ะฒะฐะตั‚ ะพะฑะฝะพะฒะปะตะฝะธะต ะฒะฐัˆะตะณะพ ัะตั€ะฒะตั€ะฝะพะณะพ ะฟั€ะธะปะพะถะตะฝะธั ะฐะฒั‚ะพะผะฐั‚ะธั‡ะตัะบะธ.", "version_announcement_overlay_title": "ะ”ะพัั‚ัƒะฟะฝะฐ ะฝะพะฒะฐั ะฒะตั€ัะธั ัะตั€ะฒะตั€ะฐ \uD83C\uDF89", "viewer_remove_from_stack": "ะฃะดะฐะปะธั‚ัŒ ะธะท ัั‚ะตะบะฐ", - "viewer_stack_use_as_main_asset": "ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต ะฒ ะบะฐั‡ะตัั‚ะฒะต ะพัะฝะพะฒะฝะพะณะพ ะพะฑัŠะตะบั‚ะฐ", + "viewer_stack_use_as_main_asset": "ะ˜ัะฟะพะปัŒะทะพะฒะฐั‚ัŒ ะฒ ะบะฐั‡ะตัั‚ะฒะต ะพัะฝะพะฒะฝะพะณะพ ะพะฑัŠะตะบั‚ะฐ", "viewer_unstack": "ะ ะฐะทะพะฑั€ะฐั‚ัŒ ัั‚ะตะบ" } \ No newline at end of file diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json index 9557fa0ac..19235cbe2 100644 --- a/mobile/assets/i18n/sk-SK.json +++ b/mobile/assets/i18n/sk-SK.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "PODROBNOSTI", "exif_bottom_sheet_location": "LOKALITA", "exif_bottom_sheet_location_add": "Nastaviลฅ polohu", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Prebiehajรบca prรกca", "experimental_settings_new_asset_list_title": "Povolenie experimentรกlnej mrieลพky fotografiรญ", "experimental_settings_subtitle": "Pouลพรญvajte na vlastnรฉ riziko!", diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json index d855502ef..eda783891 100644 --- a/mobile/assets/i18n/sr-Cyrl.json +++ b/mobile/assets/i18n/sr-Cyrl.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json index 67fecb434..6ad111958 100644 --- a/mobile/assets/i18n/sr-Latn.json +++ b/mobile/assets/i18n/sr-Latn.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALJI", "exif_bottom_sheet_location": "LOKACIJA", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "U izradi", "experimental_settings_new_asset_list_title": "Aktiviraj eksperimentalni mreลพni prikaz fotografija", "experimental_settings_subtitle": "Koristiti na sopstvenu odgovornost!", diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json index d855502ef..eda783891 100644 --- a/mobile/assets/i18n/sv-FI.json +++ b/mobile/assets/i18n/sv-FI.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETAILS", "exif_bottom_sheet_location": "LOCATION", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Work in progress", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index 7d9b3dadd..3341cf66b 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "DETALJER", "exif_bottom_sheet_location": "PLATS", "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "Under uppbyggnad", "experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnรคt", "experimental_settings_subtitle": "Anvรคnd pรฅ egen risk!", diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json index cbafb9805..cd70c7ada 100644 --- a/mobile/assets/i18n/th-TH.json +++ b/mobile/assets/i18n/th-TH.json @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "เธฃเธฒเธขเธฅเธฐเน€เธญเธตเธขเธ”", "exif_bottom_sheet_location": "เธ•เธณเนเธซเธ™เนˆเธ‡", "exif_bottom_sheet_location_add": "เน€เธžเธดเนˆเธกเธ•เธณเนเธซเธ™เนˆเธ‡", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "เธเธณเธฅเธฑเธ‡เธžเธฑเธ’เธ™เธฒ", "experimental_settings_new_asset_list_title": "Enable experimental photo grid", "experimental_settings_subtitle": "Use at your own risk!", diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index b8d3f1a1b..c0d48d0fe 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -1,13 +1,13 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", + "action_common_update": "ะžะฝะพะฒะธั‚ะธ", "add_to_album_bottom_sheet_added": "ะ”ะพะดะฐั‚ะธ ะดะพ {album}", "add_to_album_bottom_sheet_already_exists": "ะ’ะถะต ั” ะฒ {album}", "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "ะ”ะตัะบั– ะฟั€ะธัั‚ั€ะพั— ะฒะตะปัŒะผะธ ะฟะพะฒั–ะปัŒะฝะพ ะทะฐะฒะฐะฝั‚ะฐะถัƒัŽั‚ัŒ ะผั–ะฝั–ะฐั‚ัŽั€ะธ ั–ะท ะตะปะตะผะตะฝั‚ั–ะฒ ะฝะฐ ะฟั€ะธัั‚ั€ะพั—. ะะบั‚ะธะฒัƒะนั‚ะต ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะฒั–ะดะดะฐะปะตะฝะธั… ะผั–ะฝั–ะฐั‚ัŽั€ ะฝะฐั‚ะพะผั–ัั‚ัŒ.", "advanced_settings_prefer_remote_title": "ะŸะตั€ะตะฒะฐะณะฐ ะฒั–ะดะดะฐะปะตะฝะธะผ ะทะพะฑั€ะฐะถะตะฝะฝัะผ", - "advanced_settings_self_signed_ssl_subtitle": "ะŸั€ะพะฟัƒัะบะฐั” SSL-ัะตั€ั‚ะธั„ั–ะบะฐั‚ ะดะปั ั‚ะพั‡ะบะธ ะดะพัั‚ัƒะฟัƒ ัะตั€ะฒะตั€ะฐ. ะŸะพั‚ั€ั–ะฑะฝะต ะดะปั ะฒะปะฐัะฝะพั€ัƒั‡ ะฟั–ะดะฟะธัะฐะฝะธั… ัะตั€ั‚ะธั„ั–ะบะฐั‚ั–ะฒ.", - "advanced_settings_self_signed_ssl_title": "ะ”ะพะทะฒะพะปะธั‚ะธ ะฒะปะฐัะฝะพั€ัƒั‡ ะฟั–ะดะฟะธัะฐะฝั– SSL-ัะตั€ั‚ะธั„ั–ะบะฐั‚ะธ", + "advanced_settings_self_signed_ssl_subtitle": "ะŸั€ะพะฟัƒัะบะฐั” ะฟะตั€ะตะฒั–ั€ะบัƒ SSL-ัะตั€ั‚ะธั„ั–ะบะฐั‚ะฐ ัะตั€ะฒะตั€ะฐ. ะŸะพั‚ั€ั–ะฑะฝะต ะดะปั ัะฐะผะพะฟั–ะดะฟะธัะฐะฝะธั… ัะตั€ั‚ะธั„ั–ะบะฐั‚ั–ะฒ.", + "advanced_settings_self_signed_ssl_title": "ะ”ะพะทะฒะพะปะธั‚ะธ ัะฐะผะพะฟั–ะดะฟะธัะฐะฝั– SSL-ัะตั€ั‚ะธั„ั–ะบะฐั‚ะธ", "advanced_settings_tile_subtitle": "ะ ะพะทัˆะธั€ะตะฝั– ะบะพั€ะธัั‚ัƒะฒะฐั†ัŒะบั– ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", "advanced_settings_tile_title": "ะ ะพะทัˆะธั€ะตะฝั–", "advanced_settings_troubleshooting_subtitle": "ะฃะฒั–ะผะบะฝั–ั‚ัŒ ะดะพะดะฐั‚ะบะพะฒั– ั„ัƒะฝะบั†ั–ั— ะดะปั ัƒััƒะฝะตะฝะฝั ะฝะตัะฟั€ะฐะฒะฝะพัั‚ะตะน", @@ -26,17 +26,17 @@ "album_viewer_appbar_share_err_title": "ะะต ะฒะดะฐะปะพัั ะทะผั–ะฝะธั‚ะธ ะฝะฐะทะฒัƒ ะฐะปัŒะฑะพะผัƒ", "album_viewer_appbar_share_leave": "ะ’ะธะนั‚ะธ ะท ะฐะปัŒะฑะพะผัƒ", "album_viewer_appbar_share_remove": "ะ’ะธะดะฐะปะธั‚ะธ ะท ะฐะปัŒะฑะพะผัƒ", - "album_viewer_appbar_share_to": "Share To", + "album_viewer_appbar_share_to": "ะŸะพะดั–ะปะธั‚ะธัั", "album_viewer_page_share_add_users": "ะ”ะพะดะฐั‚ะธ ะบะพั€ะธัั‚ัƒะฒะฐั‡ั–ะฒ", "all_people_page_title": "ะ›ัŽะดะธ", "all_videos_page_title": "ะ’ั–ะดะตะพ", - "app_bar_signout_dialog_content": "Are you sure you want to sign out?", - "app_bar_signout_dialog_ok": "Yes", - "app_bar_signout_dialog_title": "Sign out", + "app_bar_signout_dialog_content": "ะ’ะธ ะฒะฟะตะฒะฝะตะฝั–, ั‰ะพ ะฑะฐะถะฐั”ั‚ะต ะฒะธะนั‚ะธ ะท ะฐะบะบะฐัƒะฝั‚ะฐ?", + "app_bar_signout_dialog_ok": "ะขะฐะบ", + "app_bar_signout_dialog_title": "ะ’ะธะนั‚ะธ ะท ะฐะบะบะฐัƒะฝั‚ะฐ", "archive_page_no_archived_assets": "ะะตะผะฐั” ะฐั€ั…ั–ะฒะฝะธั… ะตะปะตะผะตะฝั‚ั–ะฒ", "archive_page_title": "ะั€ั…ั–ะฒ ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "ะะตะผะพะถะปะธะฒะพ ะฒะธะดะฐะปะธั‚ะธ ะตะปะตะผะตะฝั‚(ะธ) ะปะธัˆะต ะดะปั ั‡ะธั‚ะฐะฝะฝั, ะฟั€ะพะฟัƒั‰ะตะฝะพ", + "asset_action_share_err_offline": "ะะตะผะพะถะปะธะฒะพ ะพั‚ั€ะธะผะฐั‚ะธ ะพั„ั„ะปะฐะนะฝ-ะตะปะตะผะตะฝั‚(ะธ), ะฟั€ะพะฟัƒั‰ะตะฝะพ", "asset_list_layout_settings_dynamic_layout_title": "ะ”ะธะฝะฐะผั–ั‡ะฝะต ะบะพะผะฟะพะฝัƒะฒะฐะฝะฝั", "asset_list_layout_settings_group_automatically": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ", "asset_list_layout_settings_group_by": "ะ“ั€ัƒะฟัƒะฒะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ะฟะพ", @@ -50,7 +50,7 @@ "backup_album_selection_page_select_albums": "ะžะฑะตั€ั–ั‚ัŒ ะฐะปัŒะฑะพะผะธ", "backup_album_selection_page_selection_info": "ะ†ะฝั„ะพั€ะผะฐั†ั–ั ะฟั€ะพ ะพะฑั€ะฐะฝะต", "backup_album_selection_page_total_assets": "ะ—ะฐะณะฐะปัŒะฝะฐ ะบั–ะปัŒะบั–ัั‚ัŒ ัƒะฝั–ะบะฐะปัŒะฝะธั… ะตะปะตะผะตะฝั‚ั–ะฒ", - "backup_all": "ะ’ัะต", + "backup_all": "ะฃัั–", "backup_background_service_backup_failed_message": "ะะต ะฒะดะฐะปะพัั ะทั€ะพะฑะธั‚ะธ ั€ะตะทะตั€ะฒะฝัƒ ะบะพะฟั–ัŽ ะตะปะตะผะตะฝั‚ั–ะฒ. ะŸะพะฒั‚ะพั€ัŽัŽ...", "backup_background_service_connection_failed_message": "ะะต ะฒะดะฐะปะพัั ะทะฒ'ัะทะฐั‚ะธัั ั–ะท ัะตั€ะฒะตั€ะพะผ. ะŸะพะฒั‚ะพั€ัŽัŽ...", "backup_background_service_current_upload_notification": "ะ—ะฐะฒะฐะฝั‚ะฐะถัƒั”ั‚ัŒัั {}", @@ -65,7 +65,7 @@ "backup_controller_page_background_battery_info_link": "ะŸะพะบะฐะถั–ั‚ัŒ ะผะตะฝั– ัะบ", "backup_controller_page_background_battery_info_message": "ะ”ะปั ะฝะฐะนะบั€ะฐั‰ะพะณะพ ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟั–ัŽะฒะฐะฝะฝั ะฒะธะผะบะฝั–ั‚ัŒ ะฑัƒะดัŒ-ัะบัƒ ะพะฟั‚ะธะผั–ะทะฐั†ั–ัŽ ะฐะบัƒะผัƒะปัั‚ะพั€ะฐ, ัะบะฐ ะพะฑะผะตะถัƒั” ั„ะพะฝะพะฒัƒ ะฐะบั‚ะธะฒะฝั–ัั‚ัŒ ะดะปั Immich.\n\nะกะฟะพัั–ะฑ ะทะฐะปะตะถะธั‚ัŒ ะฒั–ะด ะบะพะฝะบั€ะตั‚ะฝะพะณะพ ะฟั€ะธัั‚ั€ะพัŽ, ั‚ะพะผัƒ ัˆัƒะบะฐะนั‚ะต ะฝะตะพะฑั…ั–ะดะฝัƒ ั–ะฝั„ะพั€ะผะฐั†ั–ัŽ ัƒ ะฒะธั€ะพะฑะฝะธะบะฐ ะฒะฐัˆะพะณะพ ะฟั€ะธัั‚ั€ะพัŽ.", "backup_controller_page_background_battery_info_ok": "ะžะš", - "backup_controller_page_background_battery_info_title": "ะžะฟั‚ะธะผั–ะทะฐั†ั–ั— ะฐะบะฐะผัƒะปัั‚ะพั€ะฐ", + "backup_controller_page_background_battery_info_title": "ะžะฟั‚ะธะผั–ะทะฐั†ั–ั ะฑะฐั‚ะฐั€ะตั—", "backup_controller_page_background_charging": "ะ›ะธัˆะต ะฟั–ะด ั‡ะฐั ะทะฐั€ัะดะถะฐะฝะฝั", "backup_controller_page_background_configure_error": "ะะต ะฒะดะฐะปะพัั ะฝะฐะปะฐัˆั‚ัƒะฒะฐั‚ะธ ั„ะพะฝะพะฒะธะน ัะตั€ะฒั–ั", "backup_controller_page_background_delay": "ะ—ะฐั‚ั€ะธะผะบะฐ ะฟะตั€ะตะด ั€ะตะทะตั€ะฒะฝะธะผ ะบะพะฟั–ัŽะฒะฐะฝะฝัะผ ะฝะพะฒะธั… ะตะปะตะผะตะฝั‚ั–ะฒ: {}", @@ -111,9 +111,9 @@ "cache_settings_album_thumbnails": "ะœั–ะฝั–ะฐั‚ัŽั€ะธ ัั‚ะพั€ั–ะฝะพะบ ะฑั–ะฑะปั–ะพั‚ะตะบะธ ({} ะตะปะตะผะตะฝั‚ะธ)", "cache_settings_clear_cache_button": "ะžั‡ะธัั‚ะธั‚ะธ ะบะตัˆ", "cache_settings_clear_cache_button_title": "ะžั‡ะธั‰ะฐั” ะบะตัˆ ะฟั€ะพะณั€ะฐะผะธ. ะฆะต ััƒั‚ั‚ั”ะฒะพ ะทะฝะธะทะธั‚ัŒ ะฟั€ะพะดัƒะบั‚ะธะฒะฝั–ัั‚ัŒ ะฟั€ะพะณั€ะฐะผะธ, ะดะพะบะธ ะบะตัˆ ะฝะต ะฑัƒะดะต ะฟะตั€ะตะฑัƒะดะพะฒะฐะฝะพ.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", + "cache_settings_duplicated_assets_clear_button": "ะžะงะ˜ะกะขะ˜ะขะ˜", + "cache_settings_duplicated_assets_subtitle": "ะคะพั‚ะพ ั‚ะฐ ะฒั–ะดะตะพ, ะทะฐะฝะตัะตะฝั– ะดะพะดะฐั‚ะบะพะผ ัƒ ั‡ะพั€ะฝะธะน ัะฟะธัะพะบ", + "cache_settings_duplicated_assets_title": "ะ”ัƒะฑะปัŒะพะฒะฐะฝั– ะตะปะตะผะตะฝั‚ะธ ({})", "cache_settings_image_cache_size": "ะ ะพะทะผั–ั€ ะบะตัˆะพะฒะฐะฝะธั… ะทะพะฑั€ะฐะถะตะฝัŒ ({} ะตะปะตะผะตะฝั‚ะธ)", "cache_settings_statistics_album": "ะ‘ั–ะฑะปั–ะพั‚ะตั‡ะฝั– ะผั–ะฝั–ะฐั‚ัŽั€ะธ", "cache_settings_statistics_assets": "{} ะตะปะตะผะตะฝั‚ะธ ({})", @@ -123,16 +123,16 @@ "cache_settings_statistics_title": "ะ’ะธะบะพั€ะธัั‚ะฐะฝะฝั ะบะตัˆัƒ", "cache_settings_subtitle": "ะšะพะฝั‚ั€ะพะปัŽั” ะบะตัˆัƒะฒะฐะฝะฝั ัƒ ะผะพะฑั–ะปัŒะฝะพะผัƒ ะทะฐัั‚ะพััƒะฝะบัƒ", "cache_settings_thumbnail_size": "ะ ะพะทะผั–ั€ ะบะตัˆะพะฒะฐะฝะธั… ะผั–ะฝั–ะฐั‚ัŽั€ ({} ะตะปะตะผะตะฝั‚ะธ)", - "cache_settings_tile_subtitle": "Control the local storage behaviour", - "cache_settings_tile_title": "Local Storage", - "cache_settings_title": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะšะตัˆัƒะฒะฐะฝะฝั", + "cache_settings_tile_subtitle": "ะšะตั€ัƒะฒะฐะฝะฝั ะฟะพะฒะตะดั–ะฝะบะพัŽ ะปะพะบะฐะปัŒะฝะพะณะพ ัั…ะพะฒะธั‰ะฐ", + "cache_settings_tile_title": "ะ›ะพะบะฐะปัŒะฝะต ัั…ะพะฒะธั‰ะต", + "cache_settings_title": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะบะตัˆัƒะฒะฐะฝะฝั", "change_password_form_confirm_password": "ะŸั–ะดั‚ะฒะตั€ะดะธั‚ะธ ะฟะฐั€ะพะปัŒ", "change_password_form_description": "ะŸั€ะธะฒั–ั‚ {name},\n\nะ’ะธ ะฐะฑะพ ะฐะฑะพ ะฒะฟะตั€ัˆะต ะฒั…ะพะดะธั‚ะต ัƒ ัะธัั‚ะตะผัƒ, ะฐะฑะพ ะฑัƒะปะพ ะทั€ะพะฑะปะตะฝะพ ะทะฐะฟะธั‚ ะฝะฐ ะทะผั–ะฝัƒ ะฒะฐัˆะพะณะพ ะฟะฐั€ะพะปั. \nะ’ะฒะตะดั–ั‚ัŒ ะฒะฐัˆ ะฝะพะฒะธะน ะฟะฐั€ะพะปัŒ.", - "change_password_form_new_password": "ะะพะฒะธะน ะŸะฐั€ะพะปัŒ", + "change_password_form_new_password": "ะะพะฒะธะน ะฟะฐั€ะพะปัŒ", "change_password_form_password_mismatch": "ะŸะฐั€ะพะปั– ะฝะต ัะฟั–ะฒะฟะฐะดะฐัŽั‚ัŒ", - "change_password_form_reenter_new_password": "ะŸะพะฒั‚ะพั€ั–ั‚ัŒ ะะพะฒะธะน ะŸะฐั€ะพะปัŒ", + "change_password_form_reenter_new_password": "ะŸะพะฒั‚ะพั€ั–ั‚ัŒ ะฝะพะฒะธะน ะฟะฐั€ะพะปัŒ", "common_add_to_album": "ะ”ะพะดะฐั‚ะธ ะฒ ะฐะปัŒะฑะพะผ", - "common_change_password": "ะ—ะผั–ะฝะธั‚ะธ ะŸะฐั€ะพะปัŒ", + "common_change_password": "ะ—ะผั–ะฝะธั‚ะธ ะฟะฐั€ะพะปัŒ", "common_create_new_album": "ะกั‚ะฒะพั€ะธั‚ะธ ะฝะพะฒะธะน ะฐะปัŒะฑะพะผ", "common_server_error": "ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ะฟะตั€ะตะฒั–ั€ั‚ะต ะท'ั”ะดะฝะฐะฝะฝั, ะฟะตั€ะตะบะพะฝะฐะนั‚ะตัั, ั‰ะพ ัะตั€ะฒะตั€ ะดะพัั‚ัƒะฟะฝะธะน ั– ะฒะตั€ัั–ั ะฟั€ะพะณั€ะฐะผะธ/ัะตั€ะฒะตั€ะฐ ััƒะผั–ัะฝะฐ.", "common_shared": "ะกะฟั–ะปัŒะฝั–", @@ -142,18 +142,18 @@ "control_bottom_app_bar_archive": "ะั€ั…ั–ะฒัƒะฒะฐั‚ะธ", "control_bottom_app_bar_create_new_album": "ะกั‚ะฒะพั€ะธั‚ะธ ะฝะพะฒะธะน ะฐะปัŒะฑะพะผ", "control_bottom_app_bar_delete": "ะ’ะธะดะฐะปะธั‚ะธ", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", - "control_bottom_app_bar_favorite": "ะฃะฟะพะดะพะฑะฐั‚ะธ", + "control_bottom_app_bar_delete_from_immich": "ะ’ะธะดะฐะปะธั‚ะธ ะท Immich", + "control_bottom_app_bar_delete_from_local": "ะ’ะธะดะฐะปะธั‚ะธ ะท ะฟั€ะธัั‚ั€ะพัŽ", + "control_bottom_app_bar_edit_location": "ะ ะตะดะฐะณัƒะฒะฐั‚ะธ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั", + "control_bottom_app_bar_edit_time": "ะ ะตะดะฐะณัƒะฒะฐั‚ะธ ะดะฐั‚ัƒ ั‚ะฐ ั‡ะฐั", + "control_bottom_app_bar_favorite": "ะ”ะพ ัƒะปัŽะฑะปะตะฝะธั…", "control_bottom_app_bar_share": "ะŸะพะดั–ะปะธั‚ะธัั", - "control_bottom_app_bar_share_to": "Share To", - "control_bottom_app_bar_stack": "Stack", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_share_to": "ะŸะพะดั–ะปะธั‚ะธัั", + "control_bottom_app_bar_stack": "ะกั‚ะตะบ", + "control_bottom_app_bar_trash_from_immich": "ะŸะตั€ะตะผั–ัั‚ะธั‚ะธ ะดะพ ะบะพัˆะธะบัƒ", "control_bottom_app_bar_unarchive": "ะ ะพะทะฐั€ั…ั–ะฒัƒะฒะฐั‚ะธ", - "control_bottom_app_bar_unfavorite": "Unfavorite", - "control_bottom_app_bar_upload": "Upload", + "control_bottom_app_bar_unfavorite": "ะ’ะธะดะฐะปะธั‚ะธ ะท ัƒะปัŽะฑะปะตะฝะธั…", + "control_bottom_app_bar_upload": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ", "create_album_page_untitled": "ะ‘ะตะท ะฝะฐะทะฒะธ", "create_shared_album_page_create": "ะกั‚ะฒะพั€ะธั‚ะธ", "create_shared_album_page_share": "ะŸะพะดั–ะปะธั‚ะธัั", @@ -165,26 +165,27 @@ "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y โ€ข h:mm a", "delete_dialog_alert": "ะฆั– ะตะปะตะผะตะฝั‚ะธ ะฑัƒะดัƒั‚ัŒ ะพัั‚ะฐั‚ะพั‡ะฝะพ ะฒะธะดะฐะปะตะฝั– ะท Immich ั‚ะฐ ะฒะฐัˆะพะณะพ ะฟั€ะธัั‚ั€ะพัŽ", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "ะฆั– ะตะปะตะผะตะฝั‚ะธ ะฑัƒะดัƒั‚ัŒ ะฒะธะดะฐะปะตะฝั– ะฒะธะดะฐะปะตะฝั– ะท ะ’ะฐัˆะพะณะพ ะฟั€ะธัั‚ั€ะพัŽ, ะฐะปะต ะทะฐะปะธัˆะฐั‚ัŒัั ะดะพัั‚ัƒะฟะฝะธะผะธ ะฝะฐ ัะตั€ะฒะตั€ั– Immich", + "delete_dialog_alert_local_non_backed_up": "ะ ะตะทะตั€ะฒะฝั– ะบะพะฟั–ั— ะดะตัะบะธั… ะตะปะตะผะตะฝั‚ั–ะฒ ะฝะต ะฑัƒะปะธ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝั– ะฒ Immich ั– ะฑัƒะดัƒั‚ัŒ ะฒะธะดะฐะปะตะฝั– ะฒะธะดะฐะปะตะฝั– ะท ะ’ะฐัˆะพะณะพ ะฟั€ะธัั‚ั€ะพัŽ", + "delete_dialog_alert_remote": "ะฆั– ะตะปะตะผะตะฝั‚ะธ ะฑัƒะดัƒั‚ัŒ ะฝะฐะทะฐะฒะถะดะธ ะฒะธะดะฐะปะตะฝั– ะท ัะตั€ะฒะตั€ะฐ Immich", "delete_dialog_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", "delete_dialog_ok": "ะ’ะธะดะฐะปะธั‚ะธ", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "ะ’ัะต ะพะดะฝะพ ะฒะธะดะฐะปะธั‚ะธ", "delete_dialog_title": "ะ’ะธะดะฐะปะธั‚ะธ ะพัั‚ะฐั‚ะพั‡ะฝะพ", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", - "delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?", - "delete_shared_link_dialog_title": "Delete Shared Link", + "delete_local_dialog_ok_backed_up_only": "ะ’ะธะดะฐะปะธั‚ะธ ะปะธัˆะต ั€ะตะทะตั€ะฒะฝั– ะบะพะฟั–ั—", + "delete_local_dialog_ok_force": "ะ’ัะต ะพะดะฝะพ ะฒะธะดะฐะปะธั‚ะธ", + "delete_shared_link_dialog_content": "ะ’ะธ ะฒะฟะตะฒะฝะตะฝั–, ั‰ะพ ั…ะพั‡ะตั‚ะต ะฒะธะดะฐะปะธั‚ะธ ั†ะต ัะฟั–ะปัŒะฝะต ะฟะพัะธะปะฐะฝะฝั?", + "delete_shared_link_dialog_title": "ะ’ะธะดะฐะปะธั‚ะธ ัะฟั–ะปัŒะฝะต ะฟะพัะธะปะฐะฝะฝั", "description_input_hint_text": "ะ”ะพะดะฐั‚ะธ ะพะฟะธั...", "description_input_submit_error": "ะŸะพะผะธะปะบะฐ ะพะฝะพะฒะปะตะฝะฝั ะพะฟะธััƒ, ะฟะตั€ะตะฒั–ั€ั‚ะต ะปะพะณะธ ะดะปั ะฟะพะดั€ะพะฑะธั†ัŒ", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "edit_date_time_dialog_date_time": "ะ”ะฐั‚ะฐ ั– ั‡ะฐั", + "edit_date_time_dialog_timezone": "ะงะฐัะพะฒะธะน ะฟะพัั", + "edit_location_dialog_title": "ะœั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั", "exif_bottom_sheet_description": "ะ”ะพะดะฐั‚ะธ ะพะฟะธั...", "exif_bottom_sheet_details": "ะŸะžะ”ะ ะžะ‘ะ˜ะฆะ†", "exif_bottom_sheet_location": "ะœะ†ะกะฆะ•", - "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_location_add": "ะ”ะพะดะฐั‚ะธ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "ะ’ ั€ะพะทั€ะพะฑั†ั–", "experimental_settings_new_asset_list_title": "ะ•ะบัะฟะตั€ะธะผะตะฝั‚ะฐะปัŒะฝะธะน ะผะฐะบะตั‚ ะทะฝั–ะผะบั–ะฒ", "experimental_settings_subtitle": "ะะฐ ะฒะปะฐัะฝะธะน ั€ะธะทะธะบ!", @@ -194,42 +195,42 @@ "home_page_add_to_album_conflicts": "ะ”ะพะดะฐะฝะพ {added} ะตะปะตะผะตะฝั‚ั–ะฒ ัƒ ะฐะปัŒะฑะพะผ {album}. {failed} ะตะปะตะผะตะฝั‚ั–ะฒ ะฒะถะต ะฑัƒะปะพ ะฒ ะฐะปัŒะฑะพะผั–.", "home_page_add_to_album_err_local": "ะะตะผะพะถะปะธะฒะพ ะดะพะดะฐั‚ะธ ะปะพะบะฐะปัŒะฝั– ะตะปะตะผะตะฝั‚ะธ ะดะพ ะฐะปัŒะฑะพะผั–ะฒ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "home_page_add_to_album_success": "ะ”ะพะดะฐะฝะพ {added} ะตะปะตะผะตะฝั‚ั–ะฒ ัƒ ะฐะปัŒะฑะพะผ {album}.", - "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", + "home_page_album_err_partner": "ะŸะพะบะธ ั‰ะพ ะฝะต ะฒะดะฐั”ั‚ัŒัั ะดะพะดะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ะฟะฐั€ั‚ะฝะตั€ะฐ ะดะพ ะฐะปัŒะฑะพะผัƒ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "home_page_archive_err_local": "ะŸะพะบะธ ั‰ะพ ะฝะตะผะพะถะปะธะฒะพ ะทะฐะฐั€ั…ั–ะฒัƒะฒะฐั‚ะธ ะปะพะบะฐะปัŒะฝั– ะตะปะตะผะตะฝั‚ะธ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", - "home_page_archive_err_partner": "Can not archive partner assets, skipping", + "home_page_archive_err_partner": "ะะตะผะพะถะปะธะฒะพ ะฐั€ั…ั–ะฒัƒะฒะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "home_page_building_timeline": "ะŸะพะฑัƒะดะพะฒะฐ ั…ั€ะพะฝะพะปะพะณั–ั—", - "home_page_delete_err_partner": "Can not delete partner assets, skipping", - "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", - "home_page_favorite_err_local": "ะะตะผะพะถะปะธะฒะพ ะพั‚ั€ะธะผะฐั‚ะธ ัƒะปัŽะฑะปะตะฝั– ะปะพะบะฐะปัŒะฝั– ะตะปะตะผะตะฝั‚ะธ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", - "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", + "home_page_delete_err_partner": "ะะตะผะพะถะปะธะฒะพ ะฒะธะดะฐะปะธั‚ะธ ะตะปะตะผะตะฝั‚ะธ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", + "home_page_delete_remote_err_local": "ะ›ะพะบะฐะปัŒะฝั– ะตะปะตะผะตะฝั‚(ะธ) ะฒะถะต ะฒ ะฟั€ะพั†ะตัั– ะฒะธะดะฐะปะตะฝะฝั ะท ัะตั€ะฒะตั€ะฐ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", + "home_page_favorite_err_local": "ะŸะพะบะธ ั‰ะพ ะฝะต ะผะพะถะฝะฐ ะดะพะดะฐั‚ะธ ะดะพ ัƒะปัŽะฑะปะตะฝะธั… ะปะพะบะฐะปัŒะฝั– ะตะปะตะผะตะฝั‚ะธ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", + "home_page_favorite_err_partner": "ะŸะพะบะธ ั‰ะพ ะฝะต ะผะพะถะฝะฐ ะดะพะดะฐั‚ะธ ะดะพ ัƒะปัŽะฑะปะตะฝะธั… ะตะปะตะผะตะฝั‚ะธ ะฟะฐั€ั‚ะฝะตั€ะฐ, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "home_page_first_time_notice": "ะฏะบั‰ะพ ะฒะธ ะฒะฟะตั€ัˆะต ะบะพั€ะธัั‚ัƒั”ั‚ะตัั ะฟั€ะพะณั€ะฐะผะพัŽ, ะฟะตั€ะตะบะพะฝะฐะนั‚ะตัั, ั‰ะพ ะฒะธ ะฒะธะฑั€ะฐะปะธ ะฐะปัŒะฑะพะผะธ ะดะปั ั€ะตะทะตั€ะฒัƒะฒะฐะฝะฝั, ั‰ะพะฑ ะผะพะณั‚ะธ ะทะฐะฟะพะฒะฝัŽะฒะฐั‚ะธ ั…ั€ะพะฝะพะปะพะณั–ัŽ ะทะฝั–ะผะบั–ะฒ ั‚ะฐ ะฒั–ะดะตะพ ะฒ ะฐะปัŒะฑะพะผะฐั….", - "home_page_share_err_local": "Can not share local assets via link, skipping", + "home_page_share_err_local": "ะะตะผะพะถะปะธะฒะพ ะฟะพะดั–ะปะธั‚ะธัั ะปะพะบะฐะปัŒะฝะธะผะธ ะตะปะตะผะตะฝั‚ะฐะผะธ ั‡ะตั€ะตะท ะฟะพัะธะปะฐะฝะฝั, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "home_page_upload_err_limit": "ะœะพะถะฝะฐ ะฒะฐะฝั‚ะฐะถะธั‚ะธ ะฝะต ะฑั–ะปัŒัˆะต 30 ะตะปะตะผะตะฝั‚ั–ะฒ ะฒะพะดะฝะพั‡ะฐั, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "image_viewer_page_state_provider_download_error": "ะŸะพะผะธะปะบะฐ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั", "image_viewer_page_state_provider_download_success": "ะฃัั–ะฟัˆะฝะพ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะพ", - "image_viewer_page_state_provider_share_error": "Share Error", + "image_viewer_page_state_provider_share_error": "ะŸะพะผะธะปะบะฐ ัะฟั–ะปัŒะฝะพะณะพ ะดะพัั‚ัƒะฟัƒ", "library_page_albums": "ะะปัŒะฑะพะผะธ", "library_page_archive": "ะั€ั…ั–ะฒ", - "library_page_device_albums": "ะะปัŒะฑะพะผะธ ะฝะฐ ะŸั€ะธัั‚ั€ะพั—", + "library_page_device_albums": "ะะปัŒะฑะพะผะธ ะฝะฐ ะฟั€ะธัั‚ั€ะพั—", "library_page_favorites": "ะฃะปัŽะฑะปะตะฝั–", "library_page_new_album": "ะะพะฒะธะน ะฐะปัŒะฑะพะผ", "library_page_sharing": "ะกะฟั–ะปัŒะฝั–", - "library_page_sort_asset_count": "Number of assets", + "library_page_sort_asset_count": "ะšั–ะปัŒะบั–ัั‚ัŒ ะตะปะตะผะตะฝั‚ั–ะฒ", "library_page_sort_created": "ะะตั‰ะพะดะฐะฒะฝะพ ัั‚ะฒะพั€ะตะฝั–", - "library_page_sort_last_modified": "Last modified", - "library_page_sort_most_oldest_photo": "Oldest photo", - "library_page_sort_most_recent_photo": "Most recent photo", + "library_page_sort_last_modified": "ะžัั‚ะฐะฝะฝั ะทะผั–ะฝะฐ", + "library_page_sort_most_oldest_photo": "ะะฐะนะดะฐะฒะฝั–ัˆั– ั„ะพั‚ะพ", + "library_page_sort_most_recent_photo": "ะะฐะนะฝะพะฒั–ัˆั– ั„ะพั‚ะพ", "library_page_sort_title": "ะะฐะทะฒะฐ ะฐะปัŒะฑะพะผัƒ", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "location_picker_choose_on_map": "ะžะฑั€ะฐั‚ะธ ะฝะฐ ะผะฐะฟั–", + "location_picker_latitude": "ะจะธั€ะพั‚ะฐ", + "location_picker_latitude_error": "ะ’ะบะฐะถั–ั‚ัŒ ะดั–ะนัะฝัƒ ัˆะธั€ะพั‚ัƒ", + "location_picker_latitude_hint": "ะ’ะบะฐะถั–ั‚ัŒ ัˆะธั€ะพั‚ัƒ", + "location_picker_longitude": "ะ”ะพะฒะณะพั‚ะฐ", + "location_picker_longitude_error": "ะ’ะบะฐะถั–ั‚ัŒ ะดั–ะนัะฝัƒ ะดะพะฒะณะพั‚ัƒ", + "location_picker_longitude_hint": "ะ’ะบะฐะถั–ั‚ัŒ ะดะพะฒะณะพั‚ัƒ", "login_disabled": "ะะฒั‚ะพั€ะธะทะฐั†ั–ั ะฑัƒะปะฐ ะฒั–ะดะบะปัŽั‡ะตะฝะฐ", "login_form_api_exception": "ะŸะพะผะธะปะบะฐ API. ะŸะตั€ะตะฒั–ั€ั‚ะต ะฐะดั€ะตััƒ ัะตั€ะฒะตั€ะฐ ั– ัะฟั€ะพะฑัƒะนั‚ะต ะทะฝะพะฒัƒ", - "login_form_back_button_text": "Back", + "login_form_back_button_text": "ะะฐะทะฐะด", "login_form_button_text": "ะฃะฒั–ะนั‚ะธ", "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "http://your-server-ip:port/api", @@ -239,10 +240,10 @@ "login_form_err_invalid_url": "ะฅะธะฑะฝะธะน URL", "login_form_err_leading_whitespace": "ะŸั€ะพะฑั–ะป ะฝะฐ ะฟะพั‡ะฐั‚ะบัƒ", "login_form_err_trailing_whitespace": "ะŸั€ะพะฑั–ะป ะฒ ะบั–ะฝั†ั–", - "login_form_failed_get_oauth_server_config": "ะŸะพะผะธะปะบะฐ ะฒั…ะพะดัƒ ั‡ะตั€ะตะท OAuth, ะฟะตั€ะตะฒั–ั€ั‚ะต ะฐะดั€ะตััƒ ัะตั€ะฒะตั€ะฐ\n", - "login_form_failed_get_oauth_server_disable": "OAuth ะฝะตะดะพัั‚ัƒะฟะฝะธะน ะฝะฐ ั†ัŒะพะผัƒ ัะตั€ะฒะตั€ั–\n", + "login_form_failed_get_oauth_server_config": "ะŸะพะผะธะปะบะฐ ะฒั…ะพะดัƒ ั‡ะตั€ะตะท OAuth, ะฟะตั€ะตะฒั–ั€ั‚ะต ะฐะดั€ะตััƒ ัะตั€ะฒะตั€ะฐ", + "login_form_failed_get_oauth_server_disable": "OAuth ะฝะตะดะพัั‚ัƒะฟะฝะธะน ะฝะฐ ั†ัŒะพะผัƒ ัะตั€ะฒะตั€ั–", "login_form_failed_login": "ะŸะพะผะธะปะบะฐ ะฒั…ะพะดัƒ, ะฟะตั€ะตะฒั–ั€ั‚ะต URL-ะฐะดั€ะตััƒ ัะตั€ะฒะตั€ะฐ, ะตะปะตะบั‚ั€ะพะฝะฝัƒ ะฟะพัˆั‚ัƒ ั‚ะฐ ะฟะฐั€ะพะปัŒ", - "login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.", + "login_form_handshake_exception": "ะ’ะธะฝัั‚ะพะบ ั€ัƒะบะพัั‚ะธัะบะฐะฝะฝั ะท ัะตั€ะฒะตั€ะพะผ. ะฃะฒั–ะผะบะฝั–ั‚ัŒ ะฟั–ะดั‚ั€ะธะผะบัƒ ัะฐะผะพะฟั–ะดะฟะธัะฐะฝะพะณะพ ัะตั€ั‚ะธั„ั–ะบะฐั‚ะฐ ะฒ ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝัั…, ัะบั‰ะพ ะฒะธ ะฒะธะบะพั€ะธัั‚ะพะฒัƒั”ั‚ะต ัะฐะผะพะฟั–ะดะฟะธัะฐะฝะธะน ัะตั€ั‚ะธั„ั–ะบะฐั‚.", "login_form_label_email": "ะ•ะปะตะบั‚ั€ะพะฝะฝะฐ ะฟะพัˆั‚ะฐ", "login_form_label_password": "ะŸะฐั€ะพะปัŒ", "login_form_next_button": "ะ”ะฐะปั–", @@ -252,35 +253,35 @@ "login_form_server_error": "ะะตะผะพะถะปะธะฒะพ ะท'ั”ะดะฝะฐั‚ะธัั ั–ะท ัะตั€ะฒะตั€ะพะผ", "login_password_changed_error": "ะŸะพะผะธะปะบะฐ ัƒ ะพะฝะพะฒะปะตะฝั– ะฒะฐัˆะพะณะพ ะฟะฐั€ะพะปั", "login_password_changed_success": "ะŸะฐั€ะพะปัŒ ะพะฝะพะฒะปะตะฝะพ ัƒัะฟั–ัˆะฝะพ", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", - "map_cannot_get_user_location": "ะะต ะผะพะถัƒ ะพั‚ั€ะธะผะฐั‚ะธ ะผั–ัั†ะตะฟะตั€ะตะฑัƒะฒะฐะฝะฝั", + "map_assets_in_bound": "{} ั„ะพั‚ะพ", + "map_assets_in_bounds": "{} ั„ะพั‚ะพ", + "map_cannot_get_user_location": "ะะต ะผะพะถัƒ ะพั‚ั€ะธะผะฐั‚ะธ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั", "map_location_dialog_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", "map_location_dialog_yes": "ะขะฐะบ", - "map_location_picker_page_use_location": "Use this location", - "map_location_service_disabled_content": "ะกะปัƒะถะฑะฐ ะปะพะบะฐั†ั–ั— ะผะฐั” ะฑัƒั‚ะธ ะฒะฒั–ะผะบะฝะตะฝะพัŽ, ั‰ะพะฑ ะฒั–ะดะพะฑั€ะฐะถะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ะท ะฒะฐัˆะพะณะพ ะฟะพั‚ะพั‡ะฝะพะณะพ ะผั–ัั†ะตะฟะตั€ะตะฑัƒะฒะฐะฝะฝั. ะฃะฒั–ะผะบะฝัƒั‚ะธ ั—ั— ะทะฐั€ะฐะท?", - "map_location_service_disabled_title": "ะกะปัƒะถะฑะฐ ะผั–ัั†ะตะฟะตั€ะตะฑัƒะฒะฐะฝะฝั ะฒะธะผะบะฝะตะฝะฐ", + "map_location_picker_page_use_location": "ะฆะต ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั", + "map_location_service_disabled_content": "ะกะปัƒะถะฑะฐ ะปะพะบะฐั†ั–ั— ะผะฐั” ะฑัƒั‚ะธ ะฒะฒั–ะผะบะฝะตะฝะพัŽ, ั‰ะพะฑ ะฒั–ะดะพะฑั€ะฐะถะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ะท ะฒะฐัˆะพะณะพ ะฟะพั‚ะพั‡ะฝะพะณะพ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั. ะฃะฒั–ะผะบะฝัƒั‚ะธ ั—ั— ะทะฐั€ะฐะท?", + "map_location_service_disabled_title": "ะกะปัƒะถะฑะฐ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั ะฒะธะผะบะฝะตะฝะฐ", "map_no_assets_in_bounds": "ะะตะผะฐั” ะทะฝั–ะผะบั–ะฒ ั–ะท ั†ัŒะพะณะพ ะผั–ัั†ั", - "map_no_location_permission_content": "ะŸะพั‚ั€ั–ะฑะตะฝ ะดะพะทะฒั–ะป, ะฐะฑะธ ะฟะพะบะฐะทัƒะฒะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ั–ะท ะฟะพั‚ะพั‡ะฝะพะณะพ ะผั–ัั†ะตะฟะตั€ะตะฑัƒะฒะฐะฝะฝั. ะะฐะดะฐั‚ะธ ะนะพะณะพ ะทะฐั€ะฐะท?", - "map_no_location_permission_title": "ะŸะพะผะธะปะบะฐ ะดะพัั‚ัƒะฟัƒ ะดะพ ะผั–ัั†ะตะฟะตั€ะตะฑัƒะฒะฐะฝะฝั", + "map_no_location_permission_content": "ะŸะพั‚ั€ั–ะฑะตะฝ ะดะพะทะฒั–ะป, ะฐะฑะธ ะฟะพะบะฐะทัƒะฒะฐั‚ะธ ะตะปะตะผะตะฝั‚ะธ ั–ะท ะฟะพั‚ะพั‡ะฝะพะณะพ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั. ะะฐะดะฐั‚ะธ ะนะพะณะพ ะทะฐั€ะฐะท?", + "map_no_location_permission_title": "ะŸะพะผะธะปะบะฐ ะดะพัั‚ัƒะฟัƒ ะดะพ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั", "map_settings_dark_mode": "ะขะตะผะฝะธะน ั€ะตะถะธะผ", - "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_all": "ะฃัั–", + "map_settings_date_range_option_day": "ะœะธะฝัƒะปั– 24 ะณะพะดะธะฝะธ", + "map_settings_date_range_option_days": "ะœะธะฝัƒะปะธั… {} ะดะฝั–ะฒ", + "map_settings_date_range_option_year": "ะœะธะฝัƒะปะธะน ั€ั–ะบ", + "map_settings_date_range_option_years": "ะœะธะฝัƒะปั– {} ั€ะพะบะธ", "map_settings_dialog_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", "map_settings_dialog_save": "ะ—ะฑะตั€ะตะณั‚ะธ", "map_settings_dialog_title": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะผะฐะฟะธ", - "map_settings_include_show_archived": "Include Archived", - "map_settings_only_relative_range": "ะ”ั–ะฐะฟะฐะทะพะฝ ะดะฐั‚", + "map_settings_include_show_archived": "ะ’ะบะปัŽั‡ะธั‚ะธ ะฐั€ั…ั–ะฒะฝั– ะดะฐะฝั–", + "map_settings_only_relative_range": "ะŸั€ะพะผั–ะถะพะบ ั‡ะฐััƒ", "map_settings_only_show_favorites": "ะ›ะธัˆะต ัƒะปัŽะฑะตะฝั–", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "ะขะตะผะฐ ะบะฐั€ั‚ะธ", "map_zoom_to_see_photos": "ะ—ะผะตะฝัˆั–ั‚ัŒ, ะฐะฑะธ ะฟะตั€ะตะณะปัะฝัƒั‚ะธ ะทะฝั–ะผะบะธ", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "ะ ัƒั…ะพะผั– ะ—ะฝั–ะผะบะธ", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "ะะตะผะพะถะปะธะฒะพ ั€ะตะดะฐะณัƒะฒะฐั‚ะธ ะดะฐั‚ัƒ ะตะปะตะผะตะฝั‚ั–ะฒ ะปะธัˆะต ะดะปั ั‡ะธั‚ะฐะฝะฝั, ะฟั€ะพะฟัƒั‰ะตะฝะพ", + "multiselect_grid_edit_gps_err_read_only": "ะะตะผะพะถะปะธะฒะพ ั€ะตะดะฐะณัƒะฒะฐั‚ะธ ะผั–ัั†ะตะทะฝะฐั…ะพะดะถะตะฝะฝั ะตะปะตะผะตะฝั‚ั–ะฒ ะปะธัˆะต ะดะปั ั‡ะธั‚ะฐะฝะฝั, ะฟั€ะพะฟัƒั‰ะตะฝะพ", "notification_permission_dialog_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", "notification_permission_dialog_content": "ะฉะพะฑ ัƒะฒั–ะผะบะฝัƒั‚ะธ ัะฟะพะฒั–ั‰ะตะฝะฝั, ะฟะตั€ะตะนะดั–ั‚ัŒ ะดะพ ะะฐะปะฐัˆั‚ัƒะฒะฐะฝัŒ ั– ะฝะฐะดะฐะนั‚ะต ะดะพะทะฒั–ะป.", "notification_permission_dialog_settings": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", @@ -296,7 +297,7 @@ "partner_page_stop_sharing_content": "{} ะฒั‚ั€ะฐั‚ะธั‚ัŒ ะดะพัั‚ัƒะฟ ะดะพ ะฒะฐัˆะธั… ะทะฝั–ะผะบั–ะฒ.", "partner_page_stop_sharing_title": "ะŸั€ะธะฟะธะฝะธั‚ะธ ะฝะฐะดะฐะฝะฝั ะฒะฐัˆะธั… ะทะฝั–ะผะบั–ะฒ?", "partner_page_title": "ะŸะฐั€ั‚ะฝะตั€", - "permission_onboarding_back": "Back", + "permission_onboarding_back": "ะะฐะทะฐะด", "permission_onboarding_continue_anyway": "ะ’ัะต ะพะดะฝะพ ะฟั€ะพะดะพะฒะถะธั‚ะธ", "permission_onboarding_get_started": "ะ ะพะทะฟะพั‡ะฐั‚ะธ", "permission_onboarding_go_to_settings": "ะŸะตั€ะตะนั‚ะธ ะดะพ ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝัŒ", @@ -307,56 +308,56 @@ "permission_onboarding_permission_limited": "ะžะฑะผะตะถะตะฝะธะน ะดะพัั‚ัƒะฟ. ะะฑะธ ะดะพะทะฒะพะปะธั‚ะธ Immich ั€ะตะทะตั€ะฒะฝะต ะบะพะฟั–ัŽะฒะฐะฝะฝั ั‚ะฐ ะบะตั€ัƒะฒะฐะฝะฝั ะฒะฐัˆะพัŽ ะณะฐะปะตั€ะตั”ัŽ, ะฝะฐะดะฐะนั‚ะต ะดะพัั‚ัƒะฟ ะดะพ ะทะฝั–ะผะบั–ะฒ ั‚ะฐ ะฒั–ะดะตะพ ัƒ ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝัั…", "permission_onboarding_request": "Immich ะฟะพั‚ั€ะตะฑัƒั” ะดะพัั‚ัƒะฟัƒ ะดะพ ะฒะฐัˆะธั… ะทะฝั–ะผะบั–ะฒ ั‚ะฐ ะฒั–ะดะตะพ.", "profile_drawer_app_logs": "ะ–ัƒั€ะฝะฐะป", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", - "profile_drawer_client_server_up_to_date": "ะšะปั–ั”ะฝั‚ ั‚ะฐ ะกะตั€ะฒะตั€ โ€” ะฐะบั‚ัƒะฐะปัŒะฝั–", - "profile_drawer_documentation": "Documentation", + "profile_drawer_client_out_of_date_major": "ะœะพะฑั–ะปัŒะฝะธะน ะดะพะดะฐั‚ะพะบ ะทะฐัั‚ะฐั€ั–ะฒ. ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ะพะฝะพะฒั–ั‚ัŒ ะดะพ ะพัั‚ะฐะฝะฝัŒะพั— ะผะฐะถะพั€ะฝะพั— ะฒะตั€ัั–ั—.", + "profile_drawer_client_out_of_date_minor": "ะœะพะฑั–ะปัŒะฝะธะน ะดะพะดะฐั‚ะพะบ ะทะฐัั‚ะฐั€ั–ะฒ. ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ะพะฝะพะฒั–ั‚ัŒ ะดะพ ะพัั‚ะฐะฝะฝัŒะพั— ะผั–ะฝะพั€ะฝะพั— ะฒะตั€ัั–ั—.", + "profile_drawer_client_server_up_to_date": "ะšะปั–ั”ะฝั‚ ั‚ะฐ ัะตั€ะฒะตั€ โ€” ะฐะบั‚ัƒะฐะปัŒะฝั–", + "profile_drawer_documentation": "ะ”ะพะบัƒะผะตะฝั‚ะฐั†ั–ั", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_server_out_of_date_major": "ะกะตั€ะฒะตั€ ะทะฐัั‚ะฐั€ั–ะฒ. ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ะพะฝะพะฒั–ั‚ัŒ ะดะพ ะพัั‚ะฐะฝะฝัŒะพั— ะผะฐะถะพั€ะฝะพั— ะฒะตั€ัั–ั—.", + "profile_drawer_server_out_of_date_minor": "ะกะตั€ะฒะตั€ ะทะฐัั‚ะฐั€ั–ะฒ. ะ‘ัƒะดัŒ ะปะฐัะบะฐ, ะพะฝะพะฒั–ั‚ัŒ ะดะพ ะพัั‚ะฐะฝะฝัŒะพั— ะผั–ะฝะพั€ะฝะพั— ะฒะตั€ัั–ั—.", "profile_drawer_settings": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", "profile_drawer_sign_out": "ะ’ะธะนั‚ะธ", - "profile_drawer_trash": "Trash", + "profile_drawer_trash": "ะšะพัˆะธะบ", "recently_added_page_title": "ะะตั‰ะพะดะฐะฒะฝั–", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "ะ’ะธะฝะธะบะปะฐ ะฟะพะผะธะปะบะฐ", "search_bar_hint": "ะจัƒะบะฐั‚ะธ ะฒะฐัˆั– ะทะฝั–ะผะบะธ", "search_page_categories": "ะšะฐั‚ะตะณะพั€ั–ั—", "search_page_favorites": "ะฃะปัŽะฑะปะตะฝั–", - "search_page_motion_photos": "ะ ัƒั…ะพะผั– ะ—ะฝั–ะผะบะธ", + "search_page_motion_photos": "ะ ัƒั…ะพะผั– ะทะฝั–ะผะบะธ", "search_page_no_objects": "ะะตะผะฐั” ั–ะฝั„ะพั€ะผะฐั†ั–ั— ะฟั€ะพ ะพะฑ'ั”ะบั‚ะธ", - "search_page_no_places": "No Places Info Available", - "search_page_people": "People", - "search_page_person_add_name_dialog_cancel": "Cancel", - "search_page_person_add_name_dialog_hint": "Name", - "search_page_person_add_name_dialog_save": "Save", - "search_page_person_add_name_dialog_title": "Add a name", - "search_page_person_add_name_subtitle": "Find them fast by name with search", - "search_page_person_add_name_title": "Add a name", - "search_page_person_edit_name": "Edit name", - "search_page_places": "Places", - "search_page_recently_added": "Recently added", - "search_page_screenshots": "Screenshots", - "search_page_selfies": "Selfies", - "search_page_things": "Things", - "search_page_videos": "Videos", - "search_page_view_all_button": "View all", - "search_page_your_activity": "Your activity", - "search_page_your_map": "Your Map", - "search_result_page_new_search_hint": "New Search", - "search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ", - "search_suggestion_list_smart_search_hint_2": "m:your-search-term", - "select_additional_user_for_sharing_page_suggestions": "Suggestions", + "search_page_no_places": "ะ†ะฝั„ะพั€ะผะฐั†ั–ั ะฟั€ะพ ะผั–ัั†ั ะฝะตะดะพัั‚ัƒะฟะฝะฐ", + "search_page_people": "ะ›ัŽะดะธ", + "search_page_person_add_name_dialog_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", + "search_page_person_add_name_dialog_hint": "ะ†ะผ'ั", + "search_page_person_add_name_dialog_save": "ะ—ะฑะตั€ะตะณั‚ะธ", + "search_page_person_add_name_dialog_title": "ะ”ะพะดะฐั‚ะธ ั–ะผ'ั", + "search_page_person_add_name_subtitle": "ะจะฒะธะดะบะพ ะทะฝะฐะนะดั–ั‚ัŒ ั—ั… ะทะฐ ะฝะฐะทะฒะพัŽ ะทะฐ ะดะพะฟะพะผะพะณะพัŽ ะฟะพัˆัƒะบัƒ", + "search_page_person_add_name_title": "ะ”ะพะดะฐั‚ะธ ั–ะผ'ั", + "search_page_person_edit_name": "ะ’ั–ะดั€ะตะดะฐะณัƒะฒะฐั‚ะธ ั–ะผ'ั", + "search_page_places": "ะœั–ัั†ั", + "search_page_recently_added": "ะะตั‰ะพะดะฐะฒะฝะพ ะดะพะดะฐะฝั–", + "search_page_screenshots": "ะ—ะฝั–ะผะบะธ ะตะบั€ะฐะฝัƒ", + "search_page_selfies": "ะกะตะปั„ั–", + "search_page_things": "ะ ะตั‡ั–", + "search_page_videos": "ะ’ั–ะดะตะพ", + "search_page_view_all_button": "ะŸะตั€ะตะณะปัะฝัƒั‚ะธ ัƒัั–", + "search_page_your_activity": "ะ’ะฐัˆั– ะดั–ั—", + "search_page_your_map": "ะ’ะฐัˆะฐ ะผะฐะฟะฐ", + "search_result_page_new_search_hint": "ะะพะฒะธะน ะฟะพัˆัƒะบ", + "search_suggestion_list_smart_search_hint_1": "ะ†ะฝั‚ะตะปะตะบั‚ัƒะฐะปัŒะฝะธะน ะฟะพัˆัƒะบ ัƒะฒั–ะผะบะฝะตะฝะพ ะทะฐ ะทะฐะผะพะฒั‡ัƒะฒะฐะฝะฝัะผ, ะดะปั ะฟะพัˆัƒะบัƒ ะผะตั‚ะฐะดะฐะฝะธั… ะฒะธะบะพั€ะธัั‚ะพะฒัƒะนั‚ะต ัะธะฝั‚ะฐะบัะธั", + "search_suggestion_list_smart_search_hint_2": "m:ะฒะฐัˆ-ะฟะพัˆัƒะบะพะฒะธะน-ั‚ะตั€ะผั–ะฝ", + "select_additional_user_for_sharing_page_suggestions": "ะŸั€ะพะฟะพะทะธั†ั–ั—", "select_user_for_sharing_page_err_album": "ะะต ะฒะดะฐะปะพัั ัั‚ะฒะพั€ะธั‚ะธ ะฐะปัŒะฑะพะผ", - "select_user_for_sharing_page_share_suggestions": "Suggestions", - "server_info_box_app_version": "App Version", - "server_info_box_latest_release": "Latest Version", - "server_info_box_server_url": "Server URL", - "server_info_box_server_version": "Server Version", - "setting_image_viewer_help": "The detail viewer loads the small thumbnail first, then loads the medium-size preview (if enabled), finally loads the original (if enabled).", - "setting_image_viewer_original_subtitle": "Enable to load the original full-resolution image (large!). Disable to reduce data usage (both network and on device cache).", - "setting_image_viewer_original_title": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ ะพั€ะธะณั–ะฝะฐะปัŒะฝะต ะทะพะฑั€ะฐะถะตะฝะฝั", - "setting_image_viewer_preview_subtitle": "ะฃะฒั–ะผะบะฝั–ั‚ัŒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะทะพะฑั€ะฐะถะตะฝะฝั ัะตั€ะตะดะฝัŒะพั— ั€ะพะทะดั–ะปัŒะฝะพั— ะทะดะฐั‚ะฝะพัั‚ั–. ะ’ะธะผะบะฝั–ั‚ัŒ ะฐะฑะพ ะดะปั ะผะพะถะปะธะฒะพัั‚ั– ะฑะตะทะฟะพัะตั€ะตะดะฝัŒะพะณะพ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะพั€ะธะณั–ะฝะฐะปัƒ, ะฐะฑะพ ะดะปั ะฒะธะบะพั€ะธัั‚ะฐะฝะฝั ะปะธัˆะต ะผั–ะฝั–ะฐั‚ัŽั€.", - "setting_image_viewer_preview_title": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ ะฟะพะฟะตั€ะตะดะฝั–ะน ะฟะตั€ะตะณะปัะด ะทะพะฑั€ะฐะถะตะฝะฝั", + "select_user_for_sharing_page_share_suggestions": "ะŸั€ะพะฟะพะทะธั†ั–ั—", + "server_info_box_app_version": "ะ’ะตั€ัั–ั ะดะพะดะฐั‚ะบะฐ", + "server_info_box_latest_release": "ะžัั‚ะฐะฝะฝั ะฒะตั€ัั–ั", + "server_info_box_server_url": "URL ัะตั€ะฒะตั€ะฐ", + "server_info_box_server_version": "ะ’ะตั€ัั–ั ัะตั€ะฒะตั€ะฐ", + "setting_image_viewer_help": "ะŸะพะฒะฝะพะตะบั€ะฐะฝะฝะธะน ะฟะตั€ะตะณะปัะดะฐั‡ ัะฟะพั‡ะฐั‚ะบัƒ ะทะฐะฒะฐะฝั‚ะฐะถัƒั” ะทะพะฑั€ะฐะถะตะฝะฝั ะดะปั ะฟะพะฟะตั€ะตะดะฝัŒะพะณะพ ะฟะตั€ะตะณะปัะดัƒ ะฒ ะฝะธะทัŒะบั–ะน ั€ะพะทะดั–ะปัŒะฝั–ะน ะทะดะฐั‚ะฝะพัั‚ั–, ะฟะพั‚ั–ะผ ะทะฐะฒะฐะฝั‚ะฐะถัƒั” ะทะพะฑั€ะฐะถะตะฝะฝั ะฒ ะทะผะตะฝัˆะตะฝั–ะน ั€ะพะทะดั–ะปัŒะฝั–ะน ะทะดะฐั‚ะฝะพัั‚ั– ะฒั–ะดะฝะพัะฝะพ ะพั€ะธะณั–ะฝะฐะปัƒ (ัะบั‰ะพ ะฒะบะปัŽั‡ะตะฝะพ) ั– ะทั€ะตัˆั‚ะพัŽ ะทะฐะฒะฐะฝั‚ะฐะถัƒั” ะพั€ะธะณั–ะฝะฐะป (ัะบั‰ะพ ะฒะบะปัŽั‡ะตะฝะพ).", + "setting_image_viewer_original_subtitle": "ะฃะฒั–ะผะบะฝั–ั‚ัŒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะพั€ะธะณั–ะฝะฐะปัŒะฝะพะณะพ ะทะพะฑั€ะฐะถะตะฝะฝั ะท ะฟะพะฒะฝะพัŽ ั€ะพะทะดั–ะปัŒะฝะพัŽ ะทะดะฐั‚ะฝั–ัั‚ัŽ (ะฒะตะปะธะบะต!).\nะ’ะธะผะบะฝั–ั‚ัŒ, ั‰ะพะฑ ะทะผะตะฝัˆะธั‚ะธ ะฒะธะบะพั€ะธัั‚ะฐะฝะฝั ะดะฐะฝะธั… (ะผะตั€ะตะถั– ั‚ะฐ ะบะตัˆัƒ ะฟั€ะธัั‚ั€ะพัŽ).", + "setting_image_viewer_original_title": "ะ—ะฐะฒะฐะฝั‚ะฐะถัƒะฒะฐั‚ะธ ะพั€ะธะณั–ะฝะฐะปัŒะฝะต ะทะพะฑั€ะฐะถะตะฝะฝั", + "setting_image_viewer_preview_subtitle": "ะฃะฒั–ะผะบะฝั–ั‚ัŒ ะดะปั ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะทะพะฑั€ะฐะถะตะฝัŒ ัะตั€ะตะดะฝัŒะพั— ั€ะพะทะดั–ะปัŒะฝะพั— ะทะดะฐั‚ะฝะพัั‚ั–.\nะ’ะธะผะบะฝั–ั‚ัŒ ะดะปั ะฑะตะทะฟะพัะตั€ะตะดะฝัŒะพะณะพ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะพั€ะธะณั–ะฝะฐะปัƒ ะฐะฑะพ ะฒะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ะปะธัˆะต ะผั–ะฝั–ะฐั‚ัŽั€ัƒ.", + "setting_image_viewer_preview_title": "ะ—ะฐะฒะฐะฝั‚ะฐะถัƒะฒะฐั‚ะธ ะทะพะฑั€ะฐะถะตะฝะฝั ะฟะพะฟะตั€ะตะดะฝัŒะพะณะพ ะฟะตั€ะตะณะปัะดัƒ", "setting_notifications_notify_failures_grace_period": "ะŸะพะฒั–ะดะพะผะธั‚ะธ ะฟั€ะพ ะฟะพะผะธะปะบะธ ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟั–ัŽะฒะฐะฝะฝั: {}", "setting_notifications_notify_hours": "{} ะณะพะดะธะฝ", "setting_notifications_notify_immediately": "ะฝะตะณะฐะนะฝะพ", @@ -365,9 +366,9 @@ "setting_notifications_notify_seconds": "{} ัะตะบัƒะฝะด", "setting_notifications_single_progress_subtitle": "ะ”ะตั‚ะฐะปัŒะฝะฐ ั–ะฝั„ะพั€ะผะฐั†ั–ั ะฟั€ะพ ั…ั–ะด ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะดะปั ะบะพะถะฝะพะณะพ ะตะปะตะผะตะฝั‚ัƒ", "setting_notifications_single_progress_title": "ะŸะพะบะฐะทะฐั‚ะธ ั…ั–ะด ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟั–ัŽะฒะฐะฝะฝั", - "setting_notifications_subtitle": "ะะฐะปะฐัˆั‚ัƒะนั‚ะต ัะฒะพั— ะฟะฐั€ะฐะผะตั‚ั€ะธ ัะฟะพะฒั–ั‰ะตะฝัŒ", + "setting_notifications_subtitle": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ะฟะฐั€ะฐะผะตั‚ั€ั–ะฒ ัะฟะพะฒั–ั‰ะตะฝัŒ", "setting_notifications_title": "ะกะฟะพะฒั–ั‰ะตะฝะฝั", - "setting_notifications_total_progress_subtitle": "ะ—ะฐะณะฐะปัŒะฝะธะน ะฟั€ะพะณั€ะตั (ะณะพั‚ะพะฒะพ/ะทะฐะณะฐะปะพะผ)", + "setting_notifications_total_progress_subtitle": "ะ—ะฐะณะฐะปัŒะฝะธะน ะฟั€ะพะณั€ะตั (ะฒะธะบะพะฝะฐะฝะพ/ะทะฐะณะฐะปะพะผ)", "setting_notifications_total_progress_title": "ะŸะพะบะฐะทะฐั‚ะธ ะทะฐะณะฐะปัŒะฝะธะน ั…ั–ะด ั„ะพะฝะพะฒะพะณะพ ั€ะตะทะตั€ะฒะฝะพะณะพ ะบะพะฟั–ัŽะฒะฐะฝะฝั", "setting_pages_app_bar_settings": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", "settings_require_restart": "ะŸะตั€ะตะทะฐะฒะฐะฝั‚ะฐะถั‚ะต ะฟั€ะพะณั€ะฐะผัƒ ะดะปั ะทะฐัั‚ะพััƒะฒะฐะฝะฝั ั†ัŒะพะณะพ ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", @@ -375,66 +376,66 @@ "share_add_photos": "ะ”ะพะดะฐั‚ะธ ะทะฝั–ะผะบะธ", "share_add_title": "ะ”ะพะดะฐั‚ะธ ะฝะฐะทะฒัƒ", "share_create_album": "ะกั‚ะฒะพั€ะธั‚ะธ ะฐะปัŒะฑะพะผ", - "shared_album_activities_input_disable": "Comment is disabled", - "shared_album_activities_input_hint": "Say something", - "shared_album_activity_remove_content": "Do you want to delete this activity?", - "shared_album_activity_remove_title": "Delete Activity", - "shared_album_activity_setting_subtitle": "Let others respond", - "shared_album_activity_setting_title": "Comments & likes", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_owner_label": "Owner", - "shared_album_section_people_title": "PEOPLE", + "shared_album_activities_input_disable": "ะšะพะผะตะฝั‚ัƒะฒะฐะฝะฝั ะฒะธะผะบะฝะตะฝะพ", + "shared_album_activities_input_hint": "ะกะบะฐะถั–ั‚ัŒ ั‰ะพ-ะฝะตะฑัƒะดัŒ", + "shared_album_activity_remove_content": "ะ’ะธ ะฑะฐะถะฐั”ั‚ะต ะฒะธะดะฐะปะธั‚ะธ ั†ะต ะฟะพะฒั–ะดะพะผะปะตะฝะฝั?", + "shared_album_activity_remove_title": "ะ’ะธะดะฐะปะธั‚ะธ ะฟะพะฒั–ะดะพะผะปะตะฝะฝั", + "shared_album_activity_setting_subtitle": "ะ”ะพะทะฒะพะปะธั‚ะธ ั–ะฝัˆะธะผ ะฒั–ะดะฟะพะฒั–ะดะฐั‚ะธ", + "shared_album_activity_setting_title": "ะšะพะผะตะฝั‚ะฐั€ั– ั‚ะฐ ะปะฐะนะบะธ", + "shared_album_section_people_action_error": "ะŸะพะผะธะปะบะฐ ะฒะธั…ะพะดัƒ/ะฒะธะดะฐะปะตะฝะฝั ะท ะฐะปัŒะฑะพะผัƒ", + "shared_album_section_people_action_leave": "ะ’ะธะดะฐะปะธั‚ะธ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ ะท ะฐะปัŒะฑะพะผัƒ", + "shared_album_section_people_action_remove_user": "ะ’ะธะดะฐะปะธั‚ะธ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะฐ ะท ะฐะปัŒะฑะพะผัƒ", + "shared_album_section_people_owner_label": "ะ’ะปะฐัะฝะธะบ", + "shared_album_section_people_title": "ะ›ะฎะ”ะ˜", "share_dialog_preparing": "ะŸั–ะดะณะพั‚ะพะฒะบะฐ...", - "shared_link_app_bar_title": "Shared Links", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", - "shared_link_create_app_bar_title": "Create link to share", - "shared_link_create_error": "Error while creating shared link", - "shared_link_create_info": "Let anyone with the link see the selected photo(s)", - "shared_link_create_submit_button": "Create link", - "shared_link_edit_allow_download": "Allow public user to download", - "shared_link_edit_allow_upload": "Allow public user to upload", - "shared_link_edit_app_bar_title": "Edit link", - "shared_link_edit_change_expiry": "Change expiration time", - "shared_link_edit_description": "Description", - "shared_link_edit_description_hint": "Enter the share description", - "shared_link_edit_expire_after": "Expire after", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_never": "Never", - "shared_link_edit_password": "Password", - "shared_link_edit_password_hint": "Enter the share password", - "shared_link_edit_show_meta": "Show metadata", - "shared_link_edit_submit_button": "Update link", - "shared_link_empty": "You don't have any shared links", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_info_chip_download": "Download", + "shared_link_app_bar_title": "ะกะฟั–ะปัŒะฝั– ะฟะพัะธะปะฐะฝะฝั", + "shared_link_clipboard_copied_massage": "ะกะบะพะฟั–ะนะพะฒะฐะฝะพ ะฒ ะฑัƒั„ะตั€ ะพะฑะผั–ะฝัƒ", + "shared_link_clipboard_text": "ะŸะพัะธะปะฐะฝะฝั: {}\nะŸะฐั€ะพะปัŒ: {}", + "shared_link_create_app_bar_title": "ะกั‚ะฒะพั€ะธั‚ะธ ะฟะพัะธะปะฐะฝะฝั ัะฟั–ะปัŒะฝะพะณะพ ะดะพัั‚ัƒะฟัƒ", + "shared_link_create_error": "ะŸะพะผะธะปะบะฐ ะฟั–ะด ั‡ะฐั ัั‚ะฒะพั€ะตะฝะฝั ัะฟั–ะปัŒะฝะพะณะพ ะฟะพัะธะปะฐะฝะฝั", + "shared_link_create_info": "ะ”ะพะทะฒะพะปะธั‚ะธ ะฒัั–ะผ, ั…ั‚ะพ ะผะฐั” ะฟะพัะธะปะฐะฝะฝั, ะฟะตั€ะตะณะปัะฝัƒั‚ะธ ะฒะธะฑั€ะฐะฝั– ั„ะพั‚ะพ", + "shared_link_create_submit_button": "ะกั‚ะฒะพั€ะธั‚ะธ ะฟะพัะธะปะฐะฝะฝั", + "shared_link_edit_allow_download": "ะ”ะพะทะฒะพะปะธั‚ะธ ะฟัƒะฑะปั–ั‡ะฝะพะผัƒ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะตะฒั– ัะบะฐั‡ัƒะฒะฐั‚ะธ ั„ะฐะนะปะธ", + "shared_link_edit_allow_upload": "ะ”ะพะทะฒะพะปะธั‚ะธ ะฟัƒะฑะปั–ั‡ะฝะพะผัƒ ะบะพั€ะธัั‚ัƒะฒะฐั‡ะตะฒั– ะทะฐะฒะฐะฝั‚ะฐะถัƒะฒะฐั‚ะธ ั„ะฐะนะปะธ", + "shared_link_edit_app_bar_title": "ะ ะตะดะฐะณัƒะฒะฐั‚ะธ ะฟะพัะธะปะฐะฝะฝั", + "shared_link_edit_change_expiry": "ะ—ะผั–ะฝะธั‚ะธ ั‚ะตั€ะผั–ะฝ ะดั–ั—", + "shared_link_edit_description": "ะžะฟะธั", + "shared_link_edit_description_hint": "ะ’ะฒะตะดั–ั‚ัŒ ะพะฟะธั ะดะปั ัะฟั–ะปัŒะฝะพะณะพ ะดะพัั‚ัƒะฟัƒ", + "shared_link_edit_expire_after": "ะขะตั€ะผั–ะฝ ะดั–ั— ะทะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท", + "shared_link_edit_expire_after_option_day": "1 ะดะตะฝัŒ", + "shared_link_edit_expire_after_option_days": "{} ะดะฝั–ะฒ", + "shared_link_edit_expire_after_option_hour": "1 ะณะพะดะธะฝัƒ", + "shared_link_edit_expire_after_option_hours": "{} ะณะพะดะธะฝ", + "shared_link_edit_expire_after_option_minute": "1 ั…ะฒะธะปะธะฝัƒ", + "shared_link_edit_expire_after_option_minutes": "{} ั…ะฒะธะปะธะฝ", + "shared_link_edit_expire_after_option_never": "ะั–ะบะพะปะธ", + "shared_link_edit_password": "ะŸะฐั€ะพะปัŒ", + "shared_link_edit_password_hint": "ะ’ะฒะตะดั–ั‚ัŒ ะฟะฐั€ะพะปัŒ ะดะปั ัะฟั–ะปัŒะฝะพะณะพ ะดะพัั‚ัƒะฟัƒ", + "shared_link_edit_show_meta": "ะŸะพะบะฐะทัƒะฒะฐั‚ะธ ะผะตั‚ะฐะดะฐะฝั–", + "shared_link_edit_submit_button": "ะžะฝะพะฒะธั‚ะธ ะฟะพัะธะปะฐะฝะฝั", + "shared_link_empty": "ะฃ ะฒะฐั ะฝะตะผะฐั” ัะฟั–ะปัŒะฝะธั… ะฟะพัะธะปะฐะฝัŒ", + "shared_link_error_server_url_fetch": "ะะตะผะพะถะปะธะฒะพ ะทะฐะฟะธั‚ะฐั‚ะธ URL ั–ะท ัะตั€ะฒะตั€ะฐ", + "shared_link_expired": "ะ—ะฐะบั–ะฝั‡ะธะฒัั ั‚ะตั€ะผั–ะฝ ะดั–ั—", + "shared_link_expires_day": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ะดะตะฝัŒ", + "shared_link_expires_days": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ะดะฝั–ะฒ", + "shared_link_expires_hour": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ะณะพะดะธะฝัƒ", + "shared_link_expires_hours": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ะณะพะดะธะฝ", + "shared_link_expires_minute": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ั…ะฒะธะปะธะฝัƒ", + "shared_link_expires_minutes": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ั…ะฒะธะปะธะฝ", + "shared_link_expires_never": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั โˆž", + "shared_link_expires_second": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ัะตะบัƒะฝะดัƒ", + "shared_link_expires_seconds": "ะ—ะฐะบั–ะฝั‡ัƒั”ั‚ัŒัั ั‡ะตั€ะตะท {} ัะตะบัƒะฝะด", + "shared_link_info_chip_download": "ะกะบะฐั‡ะฐั‚ะธ", "shared_link_info_chip_metadata": "EXIF", - "shared_link_info_chip_upload": "Upload", - "shared_link_manage_links": "Manage Shared links", - "share_done": "Done", + "shared_link_info_chip_upload": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ", + "shared_link_manage_links": "ะšะตั€ัƒะฒะฐะฝะฝั ัะฟั–ะปัŒะฝะธะผะธ ะฟะพัะธะปะฐะฝะฝัะผะธ", + "share_done": "ะ“ะพั‚ะพะฒะพ", "share_invite": "ะ—ะฐะฟั€ะพัะธั‚ะธ ะฒ ะฐะปัŒะฑะพะผ", "sharing_page_album": "ะกะฟั–ะปัŒะฝั– ะฐะปัŒะฑะพะผะธ", "sharing_page_description": "ะกั‚ะฒะพั€ัŽะนั‚ะต ัะฟั–ะปัŒะฝั– ะฐะปัŒะฑะพะผะธ, ั‰ะพะฑ ะดั–ะปะธั‚ะธัั ะทะฝั–ะผะบะฐะผะธ ั‚ะฐ ะฒั–ะดะตะพ ะท ะปัŽะดัŒะผะธ ัƒ ะฒะฐัˆั–ะน ะผะตั€ะตะถั–.", "sharing_page_empty_list": "ะŸะžะ ะžะ–ะะ†ะ™ ะกะŸะ˜ะกะžะš", "sharing_silver_appbar_create_shared_album": "ะกั‚ะฒะพั€ะธั‚ะธ ัะฟั–ะปัŒะฝะธะน ะฐะปัŒะฑะพะผ", - "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_shared_links": "ะกะฟั–ะปัŒะฝั– ะฟะพัะธะปะฐะฝะฝั", "sharing_silver_appbar_share_partner": "ะŸะพะดั–ะปะธั‚ะธัั ะท ะฟะฐั€ั‚ะฝะตั€ะพะผ", "tab_controller_nav_library": "ะ‘ั–ะฑะปั–ะพั‚ะตะบะฐ", "tab_controller_nav_photos": "ะ—ะฝั–ะผะบะธ", @@ -443,26 +444,26 @@ "theme_setting_asset_list_storage_indicator_title": "ะŸะพะบะฐะทัƒะฒะฐั‚ะธ ะฟั–ะบั‚ะพะณั€ะฐะผัƒ ัั…ะพะฒะธั‰ะฐ ะฝะฐ ะฟะปะธั‚ะบะฐั… ะตะปะตะผะตะฝั‚ั–ะฒ", "theme_setting_asset_list_tiles_per_row_title": "ะšั–ะปัŒะบั–ัั‚ัŒ ะตะปะตะผะตะฝั‚ั–ะฒ ัƒ ั€ัะดะบัƒ ({})", "theme_setting_dark_mode_switch": "ะขะตะผะฝะฐ ั‚ะตะผะฐ", - "theme_setting_image_viewer_quality_subtitle": "ะะฐะปะฐัˆั‚ัƒะฒะฐั‚ะธ ัะบั–ัั‚ัŒ ะฟะตั€ะตะณะปัะดัƒ ะฟะพะฒะฝะธั… ะทะพะฑั€ะฐะถะตะฝัŒ", + "theme_setting_image_viewer_quality_subtitle": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ัะบะพัั‚ั– ะฟะตั€ะตะณะปัะดัƒ ะฟะพะฒะฝะพะตะบั€ะฐะฝะฝะธั… ะทะพะฑั€ะฐะถะตะฝัŒ", "theme_setting_image_viewer_quality_title": "ะฏะบั–ัั‚ัŒ ะฟะตั€ะตะณะปัะดัƒ ะทะพะฑั€ะฐะถะตะฝัŒ", - "theme_setting_system_theme_switch": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ (ัะบ ัะธัั‚ะตะผะฐ)", - "theme_setting_theme_subtitle": "ะ’ะธะฑะตั€ั–ั‚ัŒ ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ั‚ะตะผะธ ะฟั€ะพะณั€ะฐะผะธ", + "theme_setting_system_theme_switch": "ะะฒั‚ะพะผะฐั‚ะธั‡ะฝะพ (ัะบ ัƒ ัะธัั‚ะตะผั–)", + "theme_setting_theme_subtitle": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั ั‚ะตะผะธ ะดะพะดะฐั‚ะบะฐ", "theme_setting_theme_title": "ะขะตะผะฐ", "theme_setting_three_stage_loading_subtitle": "ะขั€ะธะตั‚ะฐะฟะฝะต ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะผะพะถะต ะฟั–ะดะฒะธั‰ะธั‚ะธ ะฟั€ะพะดัƒะบั‚ะธะฒะฝั–ัั‚ัŒ ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั, ะฐะปะต ัะฟั€ะธั‡ะธะฝะธั‚ัŒ ะทะฝะฐั‡ะฝะพ ะฑั–ะปัŒัˆะต ะฝะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั ะฝะฐ ะผะตั€ะตะถัƒ", "theme_setting_three_stage_loading_title": "ะฃะฒั–ะผะบะฝัƒั‚ะธ ั‚ั€ะธะตั‚ะฐะฟะฝะต ะทะฐะฒะฐะฝั‚ะฐะถะตะฝะฝั", "translated_text_options": "ะะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั", - "trash_page_delete": "Delete", - "trash_page_delete_all": "Delete All", - "trash_page_empty_trash_btn": "Empty trash", - "trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich", - "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Trashed items will be permanently deleted after {} days", - "trash_page_no_assets": "No trashed assets", - "trash_page_restore": "Restore", - "trash_page_restore_all": "Restore All", - "trash_page_select_assets_btn": "Select assets", - "trash_page_select_btn": "Select", - "trash_page_title": "Trash ({})", + "trash_page_delete": "ะ’ะธะดะฐะปะธั‚ะธ", + "trash_page_delete_all": "ะ’ะธะดะฐะปะธั‚ะธ ัƒัั–", + "trash_page_empty_trash_btn": "ะžั‡ะธัั‚ะธั‚ะธ ะบะพัˆะธะบ", + "trash_page_empty_trash_dialog_content": "ะ‘ะฐะถะฐั”ั‚ะต ะพั‡ะธัั‚ะธั‚ะธ ะฒะฐัˆั– ะตะปะตะผะตะฝั‚ะธ ะฒ ะบะพัˆะธะบัƒ? ะฆั– ะตะปะตะผะตะฝั‚ะธ ะฑัƒะดะต ะพัั‚ะฐั‚ะพั‡ะฝะพ ะฒะธะดะฐะปะตะฝะพ ะท Immich.", + "trash_page_empty_trash_dialog_ok": "OK", + "trash_page_info": "ะŸะพะผั–ั‰ะตะฝั– ัƒ ะบะพัˆะธะบ ะตะปะตะผะตะฝั‚ะธ ะฑัƒะดะต ะพัั‚ะฐั‚ะพั‡ะฝะพ ะฒะธะดะฐะปะตะฝะพ ั‡ะตั€ะตะท {} ะดะฝั–ะฒ", + "trash_page_no_assets": "ะ’ั–ะดะดะฐะปะตะฝั– ะตะปะตะผะตะฝั‚ะธ ะฒั–ะดััƒั‚ะฝั–", + "trash_page_restore": "ะ’ั–ะดะฝะพะฒะธั‚ะธ", + "trash_page_restore_all": "ะ’ั–ะดะฝะพะฒะธั‚ะธ ัƒัั–", + "trash_page_select_assets_btn": "ะ’ะธะฑั€ะฐะฝั– ะตะปะตะผะตะฝั‚ะธ", + "trash_page_select_btn": "ะ’ะธะฑั€ะฐั‚ะธ", + "trash_page_title": "ะšะพัˆะธะบ ({})", "upload_dialog_cancel": "ะกะบะฐััƒะฒะฐั‚ะธ", "upload_dialog_info": "ะ‘ะฐะถะฐั”ั‚ะต ัั‚ะฒะพั€ะธั‚ะธ ั€ะตะทะตั€ะฒะฝัƒ ะบะพะฟั–ัŽ ะฒะธะฑั€ะฐะฝะธั… ะตะปะตะผะตะฝั‚ั–ะฒ ะฝะฐ ัะตั€ะฒะตั€ั–?", "upload_dialog_ok": "ะ—ะฐะฒะฐะฝั‚ะฐะถะธั‚ะธ", @@ -473,7 +474,7 @@ "version_announcement_overlay_text_2": "ะทะฝะฐะนะดั–ั‚ัŒ ั…ะฒะธะปัŒะบัƒ ะฝะฐะฒั–ะดะฐั‚ะธัั ะฝะฐ ", "version_announcement_overlay_text_3": "ั– ะฟะตั€ะตะบะพะฝะฐะนั‚ะตัั, ั‰ะพ ะฒะฐัˆั– ะฝะฐะปะฐัˆั‚ัƒะฒะฐะฝะฝั docker-compose ั‚ะฐ .env ะพะฝะพะฒะปะตะฝั–, ะฐะฑะธ ะทะฐะฟะพะฑั–ะณั‚ะธ ะฑัƒะดัŒ-ัะบั–ะน ะฝะตะฟั€ะฐะฒะธะปัŒะฝั–ะน ะบะพะฝั„ั–ะณัƒั€ะฐั†ั–ั—, ะพัะพะฑะปะธะฒะพ, ัะบั‰ะพ ะฒะธ ะฒะธะบะพั€ะธัั‚ะพะฒัƒั”ั‚ะต WatchTower ะฐะฑะพ ั–ะฝัˆะธะน ะผะตั…ะฐะฝั–ะทะผ, ะดะปั ะฐะฒั‚ะพะผะฐั‚ะธั‡ะฝะธั… ะพะฝะพะฒะปะตะฝัŒ ะฒะฐัˆะพั— ัะตั€ะฒะตั€ะฝะพั— ั‡ะฐัั‚ะธะฝะธ.", "version_announcement_overlay_title": "ะ”ะพัั‚ัƒะฟะฝะฐ ะฝะพะฒะฐ ะฒะตั€ัั–ั ัะตั€ะฒะตั€ะฐ \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", - "viewer_unstack": "Un-Stack" + "viewer_remove_from_stack": "ะ’ะธะดะฐะปะธั‚ะธ ะทั– ัั‚ะตะบัƒ", + "viewer_stack_use_as_main_asset": "ะ’ะธะบะพั€ะธัั‚ะพะฒัƒะฒะฐั‚ะธ ัะบ ะพัะฝะพะฒะฝะธะน ะตะปะตะผะตะฝั‚ะธ", + "viewer_unstack": "ะ ะพะทั–ะฑั€ะฐั‚ะธ ัั‚ะตะบ" } \ No newline at end of file diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index 9684d5a74..aadb63d3e 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -1,9 +1,9 @@ { - "action_common_cancel": "Cancel", - "action_common_update": "Update", + "action_common_cancel": "Tแปซ chแป‘i", + "action_common_update": "Cแบญp nhแบญt", "add_to_album_bottom_sheet_added": "Thรชm vร o {album}", "add_to_album_bottom_sheet_already_exists": "ฤรฃ cรณ sแบตn trong {album}", - "advanced_settings_log_level_title": "Log level: {}", + "advanced_settings_log_level_title": "Phรขn loแบกi nhแบญt kรฝ: {}", "advanced_settings_prefer_remote_subtitle": "Trรชn mแป™t sแป‘ thiแบฟt bแป‹, viแป‡c tแบฃi hรฌnh thu nhแป tแปซ แบฃnh trรชn thiแบฟt bแป‹ diแป…n ra chแบญm. Kรญch hoแบกt cร i ฤ‘แบทt nร y ฤ‘แปƒ tแบฃi แบฃnh tแปซ mรกy chแปง.", "advanced_settings_prefer_remote_title": "ฦฏu tiรชn แบฃnh tแปซ mรกy chแปง", "advanced_settings_self_signed_ssl_subtitle": "Bแป qua xรกc minh chแปฉng chแป‰ SSL cho mรกy chแปง cuแป‘i. Yรชu cแบงu cho chแปฉng chแป‰ tแปฑ kรฝ.", @@ -35,8 +35,8 @@ "app_bar_signout_dialog_title": "ฤฤƒng xuแบฅt", "archive_page_no_archived_assets": "Khรดng tรฌm thแบฅy แบฃnh ฤ‘รฃ lฦฐu trแปฏ", "archive_page_title": "Kho lฦฐu trแปฏ ({})", - "asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping", - "asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping", + "asset_action_delete_err_read_only": "Khรดng thแปƒ xoรก แบฃnh chแป‰ cรณ quyแปn ฤ‘แปc, bแป qua", + "asset_action_share_err_offline": "Khรดng thแปƒ tแบฃi แบฃnh ngoแบกi tuyแบฟn, bแป qua", "asset_list_layout_settings_dynamic_layout_title": "Bแป‘ cแปฅc ฤ‘แป™ng", "asset_list_layout_settings_group_automatically": "Tแปฑ ฤ‘แป™ng", "asset_list_layout_settings_group_by": " Nhรณm แบฃnh theo", @@ -111,9 +111,9 @@ "cache_settings_album_thumbnails": "Trang thฦฐ viแป‡n hรฌnh thu nhแป ({} แบฃnh)", "cache_settings_clear_cache_button": "Xoรก bแป™ nhแป› ฤ‘แป‡m", "cache_settings_clear_cache_button_title": "Xรณa bแป™ nhแป› ฤ‘แป‡m cแปงa แปฉng dแปฅng. ฤiแปu nร y sแบฝ แบฃnh hฦฐแปŸng ฤ‘แบฟn hiแป‡u suแบฅt cแปงa แปฉng dแปฅng ฤ‘แบฟn khi bแป™ nhแป› ฤ‘แป‡m ฤ‘ฦฐแปฃc tแบกo lแบกi.", - "cache_settings_duplicated_assets_clear_button": "CLEAR", - "cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app", - "cache_settings_duplicated_assets_title": "Duplicated Assets ({})", + "cache_settings_duplicated_assets_clear_button": "XOร", + "cache_settings_duplicated_assets_subtitle": "แบขnh vร  video khรดng ฤ‘ฦฐแปฃc phรฉp hiแปƒn thแป‹ trรชn แปฉng dแปฅng", + "cache_settings_duplicated_assets_title": "แบขnh bแป‹ trรนng lแบทp ({})", "cache_settings_image_cache_size": "Kรญch thฦฐแป›c bแป™ nhแป› ฤ‘แป‡m แบฃnh ({} แบฃnh)", "cache_settings_statistics_album": "Thฦฐ viแป‡n hรฌnh thu nhแป", "cache_settings_statistics_assets": "{} แบฃnh ({})", @@ -142,17 +142,17 @@ "control_bottom_app_bar_archive": "Kho lฦฐu trแปฏ", "control_bottom_app_bar_create_new_album": "Tแบกo album mแป›i", "control_bottom_app_bar_delete": "Xoรก", - "control_bottom_app_bar_delete_from_immich": "Delete from Immich", - "control_bottom_app_bar_delete_from_local": "Delete from device", - "control_bottom_app_bar_edit_location": "Edit Location", - "control_bottom_app_bar_edit_time": "Edit Date & Time", + "control_bottom_app_bar_delete_from_immich": "Xรณa khแปi Immich", + "control_bottom_app_bar_delete_from_local": "Xรณa khแปi thiแบฟt bแป‹\n", + "control_bottom_app_bar_edit_location": "Chแป‰nh sแปญa vแป‹ trรญ", + "control_bottom_app_bar_edit_time": "Chแป‰nh sแปญa Ngร y vร  Giแป", "control_bottom_app_bar_favorite": "Yรชu thรญch", "control_bottom_app_bar_share": "Chia sแบป", "control_bottom_app_bar_share_to": "Chia sแบป vแป›i", - "control_bottom_app_bar_stack": "Ngแบฏn xแบฟp", - "control_bottom_app_bar_trash_from_immich": "Move to Trash", + "control_bottom_app_bar_stack": "Xแบฟp nhรณm", + "control_bottom_app_bar_trash_from_immich": "Di chuyแปƒn tแป›i thรนng rรกc", "control_bottom_app_bar_unarchive": "Huแปท lฦฐu trแปฏ", - "control_bottom_app_bar_unfavorite": "Unfavorite", + "control_bottom_app_bar_unfavorite": "Bแป yรชu thรญch", "control_bottom_app_bar_upload": "Tแบฃi lรชn", "create_album_page_untitled": "Khรดng tiรชu ฤ‘แป", "create_shared_album_page_create": "Tแบกo", @@ -165,26 +165,27 @@ "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y โ€ข h:mm a", "delete_dialog_alert": "Nhแปฏng mแปฅc nร y sแบฝ bแป‹ xรณa vฤฉnh viแป…n khแปi Immich vร  thiแบฟt bแป‹ cแปงa bแบกn", - "delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server", - "delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device", - "delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server", + "delete_dialog_alert_local": "Nhแปฏng mแปฅc nร y sแบฝ bแป‹ xรณa vฤฉnh viแป…n khแปi thiแบฟt bแป‹ cแปงa bแบกn nhฦฐng vแบซn cรฒn lฦฐu trแปฏ trรชn mรกy chแปง Immich", + "delete_dialog_alert_local_non_backed_up": "Mแป™t sแป‘ mแปฅc chฦฐa ฤ‘ฦฐแปฃc sao lฦฐu lรชn Immich vร  sแบฝ bแป‹ xรณa vฤฉnh viแป…n khแปi thiแบฟt bแป‹ cแปงa bแบกn.", + "delete_dialog_alert_remote": "Nhแปฏng mแปฅc nร y sแบฝ bแป‹ xรณa vฤฉnh viแป…n khแปi mรกy chแปง Immich", "delete_dialog_cancel": "Tแปซ chแป‘i", "delete_dialog_ok": "Xoรก", - "delete_dialog_ok_force": "Delete Anyway", + "delete_dialog_ok_force": "Xรณa Vฤฉnh Viแป…n", "delete_dialog_title": "Xoรก vฤฉnh viแป…n", - "delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only", - "delete_local_dialog_ok_force": "Delete Anyway", + "delete_local_dialog_ok_backed_up_only": "Xoรก แบฃnh ฤ‘รฃ sao lฦฐu", + "delete_local_dialog_ok_force": "Vแบซn xoรก", "delete_shared_link_dialog_content": "Bแบกn cรณ muแป‘n xรณa liรชn kแบฟt ฤ‘รฃ chia sแบป nร y khรดng?", "delete_shared_link_dialog_title": "Xoรก liรชn kแบฟt ฤ‘รฃ chia sแบป", "description_input_hint_text": "Thรชm mรด tแบฃ...", "description_input_submit_error": "Cแบญp nhแบญt mรด tแบฃ khรดng thร nh cรดng, vui lรฒng kiแปƒm tra nhแบญt kรฝ ฤ‘แปƒ biแบฟt thรชm chi tiแบฟt", - "edit_date_time_dialog_date_time": "Date and Time", - "edit_date_time_dialog_timezone": "Timezone", - "edit_location_dialog_title": "Location", + "edit_date_time_dialog_date_time": "Ngร y vร  Giแป", + "edit_date_time_dialog_timezone": "Mรบi giแป", + "edit_location_dialog_title": "Vแป‹ trรญ", "exif_bottom_sheet_description": "Thรชm mรด tแบฃ...", "exif_bottom_sheet_details": "CHI TIแบพT", "exif_bottom_sheet_location": "VแปŠ TRร", - "exif_bottom_sheet_location_add": "Add a location", + "exif_bottom_sheet_location_add": "Thรชm vแป‹ trรญ", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "ฤang trong quรก trรฌnh phรกt triแปƒn", "experimental_settings_new_asset_list_title": "Bแบญt lฦฐแป›i แบฃnh thแปญ nghiแป‡m", "experimental_settings_subtitle": "Sแปญ dแปฅng cรณ thแปƒ rแปงi ro!", @@ -214,22 +215,22 @@ "library_page_favorites": "แบขnh yรชu thรญch", "library_page_new_album": "Album mแป›i", "library_page_sharing": "Chia sแบป", - "library_page_sort_asset_count": "Number of assets", + "library_page_sort_asset_count": "Sแป‘ lฦฐแปฃng แบฃnh", "library_page_sort_created": "Mแป›i tแบกo gแบงn ฤ‘รขy", "library_page_sort_last_modified": "Sแปญa ฤ‘แป•i lแบงn cuแป‘i", - "library_page_sort_most_oldest_photo": "Oldest photo", + "library_page_sort_most_oldest_photo": "แบขnh cลฉ nhแบฅt", "library_page_sort_most_recent_photo": "แบขnh gแบงn ฤ‘รขy nhแบฅt", "library_page_sort_title": "Tiรชu ฤ‘แป album", - "location_picker_choose_on_map": "Choose on map", - "location_picker_latitude": "Latitude", - "location_picker_latitude_error": "Enter a valid latitude", - "location_picker_latitude_hint": "Enter your latitude here", - "location_picker_longitude": "Longitude", - "location_picker_longitude_error": "Enter a valid longitude", - "location_picker_longitude_hint": "Enter your longitude here", + "location_picker_choose_on_map": "Chแปn trรชn bแบฃn ฤ‘แป“", + "location_picker_latitude": "Vฤฉ ฤ‘แป™", + "location_picker_latitude_error": "Nhแบญp vฤฉ ฤ‘แป™ hแปฃp lแป‡", + "location_picker_latitude_hint": "Nhแบญp vฤฉ ฤ‘แป™ cแปงa bแบกn", + "location_picker_longitude": "Kinh ฤ‘แป™", + "location_picker_longitude_error": "Nhแบญp kinh ฤ‘แป™ hแปฃp lแป‡", + "location_picker_longitude_hint": "Nhแบญp kinh ฤ‘แป™ cแปงa bแบกn", "login_disabled": "ฤฤƒng nhแบญp bแป‹ vรด hiแป‡u hoรก", "login_form_api_exception": "Lแป—i API. Vui lรฒng kiแปƒm tra ฤ‘แป‹a chแป‰ mรกy chแปง vร  thแปญ lแบกi.", - "login_form_back_button_text": "Back", + "login_form_back_button_text": "Quay lแบกi", "login_form_button_text": "ฤฤƒng nhแบญp", "login_form_email_hint": "emailcuaban@email.com", "login_form_endpoint_hint": "http://ฤ‘แป‹a-chแป‰-ip-mรกy-chแปง-bแบกn:cแป•ng/api", @@ -252,35 +253,35 @@ "login_form_server_error": "Khรดng thแปƒ kแบฟt nแป‘i tแป›i mรกy chแปง.", "login_password_changed_error": "Thay ฤ‘แป•i mแบญt khแบฉu khรดng thร nh cรดng", "login_password_changed_success": "Cแบญp nhแบญt mแบญt khแบฉu thร nh cรดng", - "map_assets_in_bound": "{} photo", - "map_assets_in_bounds": "{} photos", + "map_assets_in_bound": "{} แบฃnh", + "map_assets_in_bounds": "{} แบฃnh", "map_cannot_get_user_location": "Khรดng thแปƒ xรกc ฤ‘แป‹nh vแป‹ trรญ cแปงa bแบกn", "map_location_dialog_cancel": "Tแปซ chแป‘i", "map_location_dialog_yes": "Cรณ", - "map_location_picker_page_use_location": "Use this location", + "map_location_picker_page_use_location": "Dรนng vแป‹ trรญ nร y", "map_location_service_disabled_content": "Cแบงn bแบญt dแป‹ch vแปฅ ฤ‘แป‹nh vแป‹ ฤ‘แปƒ hiแปƒn thแป‹ แบฃnh hoแบทc video tแปซ vแป‹ trรญ hiแป‡n tแบกi cแปงa bแบกn. Bแบกn cรณ muแป‘n bแบญt nรณ ngay bรขy giแป khรดng?", "map_location_service_disabled_title": "Dแป‹ch vแปฅ vแป‹ trรญ bแป‹ vรด hiแป‡u hoรก", "map_no_assets_in_bounds": "Khรดng cรณ แบฃnh trong khu vแปฑc nร y", "map_no_location_permission_content": "Cแบงn quyแปn truy cแบญp vแป‹ trรญ ฤ‘แปƒ hiแปƒn thแป‹ แบฃnh tแปซ vแป‹ trรญ hiแป‡n tแบกi cแปงa bแบกn. Bแบกn cรณ muแป‘n cho phรฉp ngay bรขy giแป khรดng?", "map_no_location_permission_title": "แปจng dแปฅng khรดng ฤ‘ฦฐแปฃc phรฉp truy cแบญp vแป‹ trรญ", "map_settings_dark_mode": "Chแบฟ ฤ‘แป™ tแป‘i", - "map_settings_date_range_option_all": "All", - "map_settings_date_range_option_day": "Past 24 hours", - "map_settings_date_range_option_days": "Past {} days", - "map_settings_date_range_option_year": "Past year", - "map_settings_date_range_option_years": "Past {} years", + "map_settings_date_range_option_all": "Tแบฅt cแบฃ", + "map_settings_date_range_option_day": "Trong vรฒng 24 giแป qua", + "map_settings_date_range_option_days": "Trong {} ngร y qua", + "map_settings_date_range_option_year": "Nฤƒm ngoรกi", + "map_settings_date_range_option_years": "Trong {} nฤƒm qua", "map_settings_dialog_cancel": "Tแปซ chแป‘i", "map_settings_dialog_save": "Lฦฐu", "map_settings_dialog_title": "Cร i ฤ‘แบทt bแบฃn ฤ‘แป“", "map_settings_include_show_archived": "Bao gแป“m แบฃnh ฤ‘รฃ lฦฐu trแปฏ", "map_settings_only_relative_range": "Khoแบฃng thแปi gian", "map_settings_only_show_favorites": "Chแป‰ hiแปƒn thแป‹ mแปฅc yรชu thรญch", - "map_settings_theme_settings": "Map Theme", + "map_settings_theme_settings": "Giao diแป‡n bแบฃn ฤ‘แป“", "map_zoom_to_see_photos": "Thu nhแป ฤ‘แปƒ xem แบฃnh", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "แบขnh ฤ‘แป™ng", - "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", - "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", + "multiselect_grid_edit_date_time_err_read_only": "Khรดng thแปƒ chแป‰nh sแปญa ngร y cแปงa แบฃnh chแป‰ cรณ quyแปn ฤ‘แปc, bแป qua", + "multiselect_grid_edit_gps_err_read_only": "Khรดng thแปƒ chแป‰nh sแปญa vแป‹ trรญ cแปงa แบฃnh chแป‰ cรณ quyแปn ฤ‘แปc, bแป qua", "notification_permission_dialog_cancel": "Tแปซ chแป‘i", "notification_permission_dialog_content": "ฤแปƒ bแบญt thรดng bรกo, chuyแปƒn tแป›i Cร i ฤ‘แบทt vร  chแปn cho phรฉp", "notification_permission_dialog_settings": "Cร i ฤ‘แบทt", @@ -307,18 +308,18 @@ "permission_onboarding_permission_limited": "Quyแปn truy cแบญp vร o แบฃnh cแปงa bแบกn bแป‹ hแบกn chแบฟ. ฤแปƒ Immich sao lฦฐu vร  quแบฃn lรฝ toร n bแป™ thฦฐ viแป‡n แบฃnh cแปงa bแบกn, hรฃy cแบฅp quyแปn truy cแบญp toร n bแป™ แบฃnh trong Cร i ฤ‘แบทt.", "permission_onboarding_request": "Immich cแบงn quyแปn ฤ‘แปƒ xem แบฃnh vร  video cแปงa bแบกn", "profile_drawer_app_logs": "Nhแบญt kรฝ", - "profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.", - "profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.", + "profile_drawer_client_out_of_date_major": "แปจng dแปฅng ฤ‘รฃ lแป—i thแปi. Vui lรฒng cแบญp nhแบญt lรชn phiรชn bแบฃn chรญnh mแป›i nhแบฅt.", + "profile_drawer_client_out_of_date_minor": "แปจng dแปฅng ฤ‘รฃ lแป—i thแปi. Vui lรฒng cแบญp nhแบญt lรชn phiรชn bแบฃn phแปฅ mแป›i nhแบฅt.", "profile_drawer_client_server_up_to_date": "Mรกy khรกch vร  mรกy chแปง ฤ‘รฃ cแบญp nhแบญt", "profile_drawer_documentation": "Tร i liแป‡u", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.", - "profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.", + "profile_drawer_server_out_of_date_major": "Mรกy chแปง ฤ‘รฃ lแป—i thแปi. Vui lรฒng cแบญp nhแบญt lรชn phiรชn bแบฃn chรญnh mแป›i nhแบฅt.", + "profile_drawer_server_out_of_date_minor": "Mรกy chแปง ฤ‘รฃ lแป—i thแปi. Vui lรฒng cแบญp nhแบญt lรชn phiรชn bแบฃn phแปฅ mแป›i nhแบฅt.", "profile_drawer_settings": "Cร i ฤ‘แบทt", "profile_drawer_sign_out": "ฤฤƒng xuแบฅt", "profile_drawer_trash": "Thรนng rรกc", "recently_added_page_title": "Mแป›i thรชm gแบงn ฤ‘รขy", - "scaffold_body_error_occurred": "Error occurred", + "scaffold_body_error_occurred": "Xแบฃy ra lแป—i", "search_bar_hint": "Tรฌm kiแบฟm แบฃnh cแปงa bแบกn", "search_page_categories": "Danh mแปฅc", "search_page_favorites": "แบขnh yรชu thรญch", @@ -326,13 +327,13 @@ "search_page_no_objects": "Khรดng cรณ thรดng tin sแปฑ vแบญt", "search_page_no_places": "Khรดng cรณ thรดng tin ฤ‘แป‹a ฤ‘iแปƒm nร o", "search_page_people": "Mแปi ngฦฐแปi", - "search_page_person_add_name_dialog_cancel": "Cancel", - "search_page_person_add_name_dialog_hint": "Name", - "search_page_person_add_name_dialog_save": "Save", - "search_page_person_add_name_dialog_title": "Add a name", - "search_page_person_add_name_subtitle": "Find them fast by name with search", - "search_page_person_add_name_title": "Add a name", - "search_page_person_edit_name": "Edit name", + "search_page_person_add_name_dialog_cancel": "Tแปซ chแป‘i", + "search_page_person_add_name_dialog_hint": "Tรชn", + "search_page_person_add_name_dialog_save": "Lฦฐu", + "search_page_person_add_name_dialog_title": "Thรชm tรชn", + "search_page_person_add_name_subtitle": "Tรฌm nhanh theo tรชn vแป›i chแปฉc nฤƒng tรฌm kiแบฟm", + "search_page_person_add_name_title": "Thรชm tรชn", + "search_page_person_edit_name": "Chแป‰nh sแปญa tรชn", "search_page_places": "ฤแป‹a ฤ‘iแปƒm", "search_page_recently_added": "Mแป›i thรชm gแบงn ฤ‘รขy", "search_page_screenshots": "แบขnh mร n hรฌnh", @@ -341,7 +342,7 @@ "search_page_videos": "Video", "search_page_view_all_button": "Xem tแบฅt cแบฃ", "search_page_your_activity": "Hoแบกt ฤ‘แป™ng cแปงa bแบกn", - "search_page_your_map": "Your Map", + "search_page_your_map": "Bแบฃn ฤ‘แป“ cแปงa bแบกn", "search_result_page_new_search_hint": "Tรฌm kiแบฟm mแป›i", "search_suggestion_list_smart_search_hint_1": "Tรฌm kiแบฟm thรดng minh ฤ‘ฦฐแปฃc bแบญt mแบทc ฤ‘แป‹nh, ฤ‘แปƒ tรฌm kiแบฟm siรชu dแปฏ liแป‡u hรฃy sแปญ dแปฅng cรบ phรกp", "search_suggestion_list_smart_search_hint_2": "m:cแปฅm-tแปซ-tรฌm-kiแบฟm-cแปงa-bแบกn", @@ -381,17 +382,17 @@ "shared_album_activity_remove_title": "Xoรก hoแบกt ฤ‘แป™ng", "shared_album_activity_setting_subtitle": "Cho phรฉp ngฦฐแปi khรกc phแบฃn hแป“i", "shared_album_activity_setting_title": "Bรฌnh luแบญn vร  lฦฐแปฃt thรญch", - "shared_album_section_people_action_error": "Error leaving/removing from album", - "shared_album_section_people_action_leave": "Remove user from album", - "shared_album_section_people_action_remove_user": "Remove user from album", - "shared_album_section_people_owner_label": "Owner", - "shared_album_section_people_title": "PEOPLE", + "shared_album_section_people_action_error": "Lแป—i khi xoรก khแปi album", + "shared_album_section_people_action_leave": "Xรณa ngฦฐแปi dรนng khแปi album", + "shared_album_section_people_action_remove_user": "Xรณa ngฦฐแปi dรนng khแปi album", + "shared_album_section_people_owner_label": "Chแปง sแปŸ hแปฏu", + "shared_album_section_people_title": "MแปŒI NGฦฏแปœI", "share_dialog_preparing": "ฤang xแปญ lรฝ...", "shared_link_app_bar_title": "ฤฦฐแปng liรชn kแบฟt chia sแบป", - "shared_link_clipboard_copied_massage": "Copied to clipboard", - "shared_link_clipboard_text": "Link: {}\nPassword: {}", + "shared_link_clipboard_copied_massage": "ฤรฃ sao chรฉp tแป›i bแบฃn ghi tแบกm", + "shared_link_clipboard_text": "Liรชn kแบฟt: {}\nMแบญt khแบฉu: {}", "shared_link_create_app_bar_title": "Tแบกo liรชn kแบฟt ฤ‘แปƒ chia sแบป", - "shared_link_create_error": "Error while creating shared link", + "shared_link_create_error": "Tแบกo liรชn kแบฟt chia sแบป khรดng thร nh cรดng", "shared_link_create_info": "Cho phรฉp bแบฅt cแปฉ ai cรณ liรชn kแบฟt xem (cรกc) แบฃnh ฤ‘รฃ chแปn", "shared_link_create_submit_button": "Tแบกo liรชn kแบฟt", "shared_link_edit_allow_download": "Cho phรฉp bแบฅt cแปฉ ai ฤ‘แปu cรณ thแปƒ tแบฃi xuแป‘ng", @@ -401,32 +402,32 @@ "shared_link_edit_description": "Mรด tแบฃ", "shared_link_edit_description_hint": "Nhแบญp mรด tแบฃ chia sแบป", "shared_link_edit_expire_after": "Hแบฟt hแบกn sau", - "shared_link_edit_expire_after_option_day": "1 day", - "shared_link_edit_expire_after_option_days": "{} days", - "shared_link_edit_expire_after_option_hour": "1 hour", - "shared_link_edit_expire_after_option_hours": "{} hours", - "shared_link_edit_expire_after_option_minute": "1 minute", - "shared_link_edit_expire_after_option_minutes": "{} minutes", - "shared_link_edit_expire_after_option_never": "Never", + "shared_link_edit_expire_after_option_day": "1 ngร y", + "shared_link_edit_expire_after_option_days": "{} ngร y", + "shared_link_edit_expire_after_option_hour": "1 giแป", + "shared_link_edit_expire_after_option_hours": "{} giแป", + "shared_link_edit_expire_after_option_minute": "1 phรบt", + "shared_link_edit_expire_after_option_minutes": "{} phรบt", + "shared_link_edit_expire_after_option_never": "Khรดng bao giแป", "shared_link_edit_password": "Mแบญt khแบฉu", "shared_link_edit_password_hint": "Nhแบญp mแบญt khแบฉu chia sแบป", "shared_link_edit_show_meta": "Hiแป‡n thแป‹ siรชu dแปฏ liแป‡u", "shared_link_edit_submit_button": "Cแบญp nhแบญt liรชn kแบฟt", "shared_link_empty": "Bแบกn khรดng cรณ liรชn kแบฟt ฤ‘ฦฐแปฃc chia sแบป nร o", - "shared_link_error_server_url_fetch": "Cannot fetch the server url", - "shared_link_expired": "Expired", - "shared_link_expires_day": "Expires in {} day", - "shared_link_expires_days": "Expires in {} days", - "shared_link_expires_hour": "Expires in {} hour", - "shared_link_expires_hours": "Expires in {} hours", - "shared_link_expires_minute": "Expires in {} minute", - "shared_link_expires_minutes": "Expires in {} minutes", - "shared_link_expires_never": "Expires โˆž", - "shared_link_expires_second": "Expires in {} second", - "shared_link_expires_seconds": "Expires in {} seconds", - "shared_link_info_chip_download": "Download", - "shared_link_info_chip_metadata": "EXIF", - "shared_link_info_chip_upload": "Upload", + "shared_link_error_server_url_fetch": "Khรดng thแปƒ kแบฟt nแป‘i ฤ‘แป‹a chแป‰ mรกy chแปง", + "shared_link_expired": "ฤรฃ hแบฟt hแบกn", + "shared_link_expires_day": "Hแบฟt hแบกn trong {} ngร y", + "shared_link_expires_days": "Hแบฟt hแบกn trong {} ngร y", + "shared_link_expires_hour": "Hแบฟt hแบกn trong {} giแป", + "shared_link_expires_hours": "Hแบฟt hแบกn trong {} giแป", + "shared_link_expires_minute": "Hแบฟt hแบกn trong {} phรบt", + "shared_link_expires_minutes": "Hแบฟt hแบกn trong {} phรบt", + "shared_link_expires_never": "Hแบฟt hแบกn โˆž\n", + "shared_link_expires_second": "Hแบฟt hแบกn trong {} giรขy", + "shared_link_expires_seconds": "Hแบฟt hแบกn trong {} giรขy", + "shared_link_info_chip_download": "Tแบฃi xuแป‘ng", + "shared_link_info_chip_metadata": "Dแปฏ liแป‡u EXIF", + "shared_link_info_chip_upload": "Tแบฃi lรชn", "shared_link_manage_links": "Quแบฃn lรฝ liรชn kแบฟt ฤ‘ฦฐแปฃc chia sแบป", "share_done": "Hoร n tแบฅt", "share_invite": "Mแปi vร o album", @@ -441,11 +442,11 @@ "tab_controller_nav_search": "Tรฌm kiแบฟm", "tab_controller_nav_sharing": "Chia sแบป", "theme_setting_asset_list_storage_indicator_title": "Hiแป‡n thแป‹ trแบกng thรกi sao lฦฐu แบฃnh trรชn hรฌnh thu nhแป ", - "theme_setting_asset_list_tiles_per_row_title": "Sแป‘ แบฃnh vร  video trรชn mแป™t dรฒng ({})", + "theme_setting_asset_list_tiles_per_row_title": "Sแป‘ lฦฐแปฃng แบฃnh trรชn mแป™t dรฒng ({})", "theme_setting_dark_mode_switch": "Chแบฟ ฤ‘แป™ tแป‘i", "theme_setting_image_viewer_quality_subtitle": "ฤiแปu chแป‰nh chแบฅt lฦฐแปฃng cแปงa trรฌnh xem แบฃnh", "theme_setting_image_viewer_quality_title": "Chแบฅt lฦฐแปฃng trรฌnh xem แบฃnh", - "theme_setting_system_theme_switch": "Tฦฐ ฤ‘แป™ng (Theo cร i ฤ‘แบทt hแป‡ thแป‘ng)", + "theme_setting_system_theme_switch": "Tแปฑ ฤ‘แป™ng (Theo cร i ฤ‘แบทt hแป‡ thแป‘ng)", "theme_setting_theme_subtitle": "Chแปn cร i ฤ‘แบทt giao diแป‡n แปฉng dแปฅng", "theme_setting_theme_title": "Giao diแป‡n", "theme_setting_three_stage_loading_subtitle": "Tแบฃi ba giai doแบกn cรณ thแปƒ tฤƒng hiแป‡u nฤƒng tแบฃi แบฃnh nhฦฐng sแบฝ tแป‘n dแปฏ liแป‡u mแบกng ฤ‘รกng kแปƒ.", @@ -473,7 +474,7 @@ "version_announcement_overlay_text_2": "vui lรฒng dร nh thแปi gian cแปงa bแบกn ฤ‘แปƒ ฤ‘แบฟn thฤƒm", "version_announcement_overlay_text_3": "vร  ฤ‘แบฃm bแบฃo cร i ฤ‘แบทt docker-compose vร  tแป‡p .env cแปงa bแบกn ฤ‘รฃ cแบญp nhแบญt ฤ‘แปƒ trรกnh bแบฅt kแปณ cแบฅu hรฌnh sai sรณt, ฤ‘แบทc biแป‡t nแบฟu bแบกn dรนng WatchTower hoแบทc bแบฅt kแปณ cฦก chแบฟ nร o xแปญ lรฝ viแป‡c cแบญp nhแบญt แปฉng dแปฅng mรกy chแปง cแปงa bแบกn tแปฑ ฤ‘แป™ng.", "version_announcement_overlay_title": "Phiรชn bแบฃn mรกy chแปง cรณ bแบฃn cแบญp nhแบญt mแป›i", - "viewer_remove_from_stack": "Loแบกi bแป khแปi ngฤƒn xแบฟp", - "viewer_stack_use_as_main_asset": "Sแปญ dแปฅng lร m แบฃnh bรฌa ngฤƒn xแบฟp", - "viewer_unstack": "Hoร n tรกc ngฤƒn xแบฟp" + "viewer_remove_from_stack": "Xoรก khแปi nhรณm", + "viewer_stack_use_as_main_asset": "ฤแบทt lร m lแปฑa chแปn hร ng ฤ‘แบงu", + "viewer_unstack": "Huแปท xแบฟp nhรณm" } \ No newline at end of file diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json index 2a6b95101..c133f1041 100644 --- a/mobile/assets/i18n/zh-CN.json +++ b/mobile/assets/i18n/zh-CN.json @@ -80,7 +80,7 @@ "backup_controller_page_backup_sub": "ๅทฒๅค‡ไปฝ็š„็…ง็‰‡ๅ’Œ่ง†้ข‘", "backup_controller_page_cancel": "ๅ–ๆถˆ", "backup_controller_page_created": "ๅˆ›ๅปบๆ—ถ้—ด: {}", - "backup_controller_page_desc_backup": "ๆ‰“ๅผ€ๅ‰ๅฐๅค‡ไปฝ๏ผŒไปฅๅœจ็จ‹ๅบ่ฟ่กŒๆ—ถ่‡ชๅŠจๅค‡ไปฝใ€‚", + "backup_controller_page_desc_backup": "ๆ‰“ๅผ€ๅ‰ๅฐๅค‡ไปฝ๏ผŒไปฅๅœจ็จ‹ๅบ่ฟ่กŒๆ—ถ่‡ชๅŠจๅค‡ไปฝๆ–ฐ้กน็›ฎใ€‚", "backup_controller_page_excluded": "ๅทฒๆŽ’้™ค๏ผš", "backup_controller_page_failed": "ๅคฑ่ดฅ๏ผˆ{}๏ผ‰", "backup_controller_page_filename": "ๆ–‡ไปถๅ็งฐ: {} [{}]", @@ -121,7 +121,7 @@ "cache_settings_statistics_shared": "ๅ…ฑไบซ็›ธๅ†Œ็ผฉ็•ฅๅ›พ", "cache_settings_statistics_thumbnail": "็ผฉ็•ฅๅ›พ", "cache_settings_statistics_title": "็ผ“ๅญ˜ไฝฟ็”จๆƒ…ๅ†ต", - "cache_settings_subtitle": "ๆŽงๅˆถ Immich ็š„็ผ“ๅญ˜่กŒไธบ", + "cache_settings_subtitle": "ๆŽงๅˆถ Immich app ็š„็ผ“ๅญ˜่กŒไธบ", "cache_settings_thumbnail_size": "็ผฉ็•ฅๅ›พ็ผ“ๅญ˜ๅคงๅฐ๏ผˆ{} ้กน๏ผ‰", "cache_settings_tile_subtitle": "่ฎพ็ฝฎๆœฌๅœฐๅญ˜ๅ‚จ่กŒไธบ", "cache_settings_tile_title": "ๆœฌๅœฐๅญ˜ๅ‚จ", @@ -134,7 +134,7 @@ "common_add_to_album": "ๆทปๅŠ ๅˆฐ็›ธๅ†Œ", "common_change_password": "ๆ›ดๆ”นๅฏ†็ ", "common_create_new_album": "ๆ–ฐๅปบ็›ธๅ†Œ", - "common_server_error": "่ฏทๆฃ€ๆŸฅๆ‚จ็š„็ฝ‘็ปœ่ฟžๆŽฅ๏ผŒ็กฎไฟๆœๅŠกๅ™จๅฏ่ฎฟ้—ฎไธ”่ฏฅๅบ”็”จ็จ‹ๅบๆˆ–ๆœๅŠกๅ™จ็‰ˆๆœฌๅ…ผๅฎนใ€‚", + "common_server_error": "่ฏทๆฃ€ๆŸฅๆ‚จ็š„็ฝ‘็ปœ่ฟžๆŽฅ๏ผŒ็กฎไฟๆœๅŠกๅ™จๅฏ่ฎฟ้—ฎไธ”่ฏฅๅบ”็”จ็จ‹ๅบไธŽๆœๅŠกๅ™จ็‰ˆๆœฌๅ…ผๅฎนใ€‚", "common_shared": "ๅ…ฑไบซ", "control_bottom_app_bar_add_to_album": "ๆทปๅŠ ๅˆฐ็›ธๅ†Œ", "control_bottom_app_bar_album_info": "{} ้กน", @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "่ฏฆๆƒ…", "exif_bottom_sheet_location": "ไฝ็ฝฎ", "exif_bottom_sheet_location_add": "ๆทปๅŠ ไฝ็ฝฎไฟกๆฏ", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "ๆญฃๅœจๅค„็†", "experimental_settings_new_asset_list_title": "ๅฏ็”จๅฎž้ชŒๆ€ง็…ง็‰‡็ฝ‘ๆ ผ", "experimental_settings_subtitle": "ไฝฟ็”จ้ฃŽ้™ฉ่‡ช่ดŸ๏ผ", @@ -207,7 +208,7 @@ "home_page_upload_err_limit": "ไธ€ๆฌกๆœ€ๅคšๅช่ƒฝไธŠไผ  30 ไธช้กน็›ฎ๏ผŒ่ทณ่ฟ‡", "image_viewer_page_state_provider_download_error": "ไธ‹่ฝฝๅ‡บ็Žฐ้”™่ฏฏ", "image_viewer_page_state_provider_download_success": "ไธ‹่ฝฝๆˆๅŠŸ", - "image_viewer_page_state_provider_share_error": "ๅ…ฑไบซ้”™่ฏฏ", + "image_viewer_page_state_provider_share_error": "ๅ…ฑไบซๅ‡บ้”™", "library_page_albums": "็›ธๅ†Œ", "library_page_archive": "ๅฝ’ๆกฃ", "library_page_device_albums": "่ฎพๅค‡ไธŠ็š„็›ธๅ†Œ", @@ -248,7 +249,7 @@ "login_form_next_button": "ไธ‹ไธ€ไธช", "login_form_password_hint": "ๅฏ†็ ", "login_form_save_login": "ไฟๆŒ็™ปๅฝ•", - "login_form_server_empty": "่พ“ๅ…ฅๆœๅŠกๅ™จๅœฐๅ€ใ€‚", + "login_form_server_empty": "่พ“ๅ…ฅๆœๅŠกๅ™จๅœฐๅ€", "login_form_server_error": "ๆ— ๆณ•่ฟžๆŽฅๅˆฐๆœๅŠกๅ™จใ€‚", "login_password_changed_error": "ๆ›ดๆ–ฐๅฏ†็ ๆ—ถๅ‡บ้”™\n", "login_password_changed_success": "ๅฏ†็ ๆ›ดๆ–ฐๆˆๅŠŸ", @@ -267,7 +268,7 @@ "map_settings_date_range_option_all": "ๆ‰€ๆœ‰", "map_settings_date_range_option_day": "่ฟ‡ๅŽป24ๅฐๆ—ถ", "map_settings_date_range_option_days": "{}ๅคฉๅ‰", - "map_settings_date_range_option_year": "ๅŽปๅนด", + "map_settings_date_range_option_year": "1ๅนดๅ‰", "map_settings_date_range_option_years": "{}ๅนดๅ‰", "map_settings_dialog_cancel": "ๅ–ๆถˆ", "map_settings_dialog_save": "ไฟๅญ˜", @@ -343,7 +344,7 @@ "search_page_your_activity": "ๆ‚จ็š„ๆดปๅŠจ", "search_page_your_map": "่ถณ่ฟน", "search_result_page_new_search_hint": "ๆœ็ดขๆ–ฐ็š„", - "search_suggestion_list_smart_search_hint_1": "้ป˜่ฎคๆƒ…ๅ†ตไธ‹ๅฏ็”จๆ™บ่ƒฝๆœ็ดข๏ผ›่ฆๆœ็ดขๅ…ƒๆ•ฐๆฎ๏ผŒ่ฏทไฝฟ็”จ็›ธๅ…ณ่ฏญๆณ•", + "search_suggestion_list_smart_search_hint_1": "้ป˜่ฎคๆƒ…ๅ†ตไธ‹ๅฏ็”จๆ™บ่ƒฝๆœ็ดข๏ผŒ่ฆๆœ็ดขๅ…ƒๆ•ฐๆฎ๏ผŒ่ฏทไฝฟ็”จ็›ธๅ…ณ่ฏญๆณ•", "search_suggestion_list_smart_search_hint_2": "m:ๆ‚จ็š„ๆœ็ดขๅ…ณ้”ฎ่ฏ", "select_additional_user_for_sharing_page_suggestions": "ๅปบ่ฎฎ", "select_user_for_sharing_page_err_album": "ๅˆ›ๅปบ็›ธๅ†Œๅคฑ่ดฅ", diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json index c5359c09f..811e56ea6 100644 --- a/mobile/assets/i18n/zh-Hans.json +++ b/mobile/assets/i18n/zh-Hans.json @@ -80,7 +80,7 @@ "backup_controller_page_backup_sub": "ๅทฒๅค‡ไปฝ็š„็…ง็‰‡ๅ’Œ่ง†้ข‘", "backup_controller_page_cancel": "ๅ–ๆถˆ", "backup_controller_page_created": "ๅˆ›ๅปบๆ—ถ้—ด: {}", - "backup_controller_page_desc_backup": "ๆ‰“ๅผ€ๅ‰ๅฐๅค‡ไปฝ๏ผŒไปฅๅœจ็จ‹ๅบ่ฟ่กŒๆ—ถ่‡ชๅŠจๅค‡ไปฝใ€‚", + "backup_controller_page_desc_backup": "ๆ‰“ๅผ€ๅ‰ๅฐๅค‡ไปฝ๏ผŒไปฅๅœจ็จ‹ๅบ่ฟ่กŒๆ—ถ่‡ชๅŠจๅค‡ไปฝๆ–ฐ้กน็›ฎใ€‚", "backup_controller_page_excluded": "ๅทฒๆŽ’้™ค๏ผš", "backup_controller_page_failed": "ๅคฑ่ดฅ๏ผˆ{}๏ผ‰", "backup_controller_page_filename": "ๆ–‡ไปถๅ็งฐ: {} [{}]", @@ -121,7 +121,7 @@ "cache_settings_statistics_shared": "ๅ…ฑไบซ็›ธๅ†Œ็ผฉ็•ฅๅ›พ", "cache_settings_statistics_thumbnail": "็ผฉ็•ฅๅ›พ", "cache_settings_statistics_title": "็ผ“ๅญ˜ไฝฟ็”จๆƒ…ๅ†ต", - "cache_settings_subtitle": "ๆŽงๅˆถ Immich ็š„็ผ“ๅญ˜่กŒไธบ", + "cache_settings_subtitle": "ๆŽงๅˆถ Immich app ็š„็ผ“ๅญ˜่กŒไธบ", "cache_settings_thumbnail_size": "็ผฉ็•ฅๅ›พ็ผ“ๅญ˜ๅคงๅฐ๏ผˆ{} ้กน๏ผ‰", "cache_settings_tile_subtitle": "่ฎพ็ฝฎๆœฌๅœฐๅญ˜ๅ‚จ่กŒไธบ", "cache_settings_tile_title": "ๆœฌๅœฐๅญ˜ๅ‚จ", @@ -134,7 +134,7 @@ "common_add_to_album": "ๆทปๅŠ ๅˆฐ็›ธๅ†Œ", "common_change_password": "ๆ›ดๆ”นๅฏ†็ ", "common_create_new_album": "ๆ–ฐๅปบ็›ธๅ†Œ", - "common_server_error": "่ฏทๆฃ€ๆŸฅๆ‚จ็š„็ฝ‘็ปœ่ฟžๆŽฅ๏ผŒ็กฎไฟๆœๅŠกๅ™จๅฏ่ฎฟ้—ฎไธ”่ฏฅๅบ”็”จ็จ‹ๅบๆˆ–ๆœๅŠกๅ™จ็‰ˆๆœฌๅ…ผๅฎนใ€‚", + "common_server_error": "่ฏทๆฃ€ๆŸฅๆ‚จ็š„็ฝ‘็ปœ่ฟžๆŽฅ๏ผŒ็กฎไฟๆœๅŠกๅ™จๅฏ่ฎฟ้—ฎไธ”่ฏฅๅบ”็”จ็จ‹ๅบไธŽๆœๅŠกๅ™จ็‰ˆๆœฌๅ…ผๅฎนใ€‚", "common_shared": "ๅ…ฑไบซ", "control_bottom_app_bar_add_to_album": "ๆทปๅŠ ๅˆฐ็›ธๅ†Œ", "control_bottom_app_bar_album_info": "{} ้กน", @@ -185,6 +185,7 @@ "exif_bottom_sheet_details": "่ฏฆๆƒ…", "exif_bottom_sheet_location": "ไฝ็ฝฎ", "exif_bottom_sheet_location_add": "ๆทปๅŠ ไฝ็ฝฎไฟกๆฏ", + "exif_bottom_sheet_people": "PEOPLE", "experimental_settings_new_asset_list_subtitle": "ๆญฃๅœจๅค„็†", "experimental_settings_new_asset_list_title": "ๅฏ็”จๅฎž้ชŒๆ€ง็…ง็‰‡็ฝ‘ๆ ผ", "experimental_settings_subtitle": "ไฝฟ็”จ้ฃŽ้™ฉ่‡ช่ดŸ๏ผ", @@ -192,7 +193,7 @@ "favorites_page_no_favorites": "ๆœชๆ‰พๅˆฐๆ”ถ่—้กน็›ฎ", "favorites_page_title": "ๆ”ถ่—", "home_page_add_to_album_conflicts": "ๅทฒๅ‘็›ธๅ†Œ {album} ไธญๆทปๅŠ  {added} ้กนใ€‚\nๅ…ถไธญ {failed} ้กนๅœจ็›ธๅ†Œไธญๅทฒๅญ˜ๅœจใ€‚", - "home_page_add_to_album_err_local": "ๆš‚ไธ่ƒฝๅฐ†ๆœฌๅœฐ่ต„้กน็›ฎๆทปๅŠ ๅˆฐ็›ธๅ†Œไธญ๏ผŒ่ทณ่ฟ‡", + "home_page_add_to_album_err_local": "ๆš‚ไธ่ƒฝๅฐ†ๆœฌๅœฐ้กน็›ฎๆทปๅŠ ๅˆฐ็›ธๅ†Œไธญ๏ผŒ่ทณ่ฟ‡", "home_page_add_to_album_success": "ๅทฒๅ‘็›ธๅ†Œ {album} ไธญๆทปๅŠ  {added} ้กนใ€‚", "home_page_album_err_partner": "ๆš‚ๆ— ๆณ•ๅฐ†ๅŒไผด็š„้กน็›ฎๆทปๅŠ ๅˆฐ็›ธๅ†Œ๏ผŒ่ทณ่ฟ‡", "home_page_archive_err_local": "ๆš‚ๆ— ๆณ•ๅฝ’ๆกฃๆœฌๅœฐ้กน็›ฎ๏ผŒ่ทณ่ฟ‡", @@ -207,7 +208,7 @@ "home_page_upload_err_limit": "ไธ€ๆฌกๆœ€ๅคšๅช่ƒฝไธŠไผ  30 ไธช้กน็›ฎ๏ผŒ่ทณ่ฟ‡", "image_viewer_page_state_provider_download_error": "ไธ‹่ฝฝๅ‡บ็Žฐ้”™่ฏฏ", "image_viewer_page_state_provider_download_success": "ไธ‹่ฝฝๆˆๅŠŸ", - "image_viewer_page_state_provider_share_error": "ๅ…ฑไบซ้”™่ฏฏ", + "image_viewer_page_state_provider_share_error": "ๅ…ฑไบซๅ‡บ้”™", "library_page_albums": "็›ธๅ†Œ", "library_page_archive": "ๅฝ’ๆกฃ", "library_page_device_albums": "่ฎพๅค‡ไธŠ็š„็›ธๅ†Œ", @@ -248,7 +249,7 @@ "login_form_next_button": "ไธ‹ไธ€ไธช", "login_form_password_hint": "ๅฏ†็ ", "login_form_save_login": "ไฟๆŒ็™ปๅฝ•", - "login_form_server_empty": "่พ“ๅ…ฅๆœๅŠกๅ™จๅœฐๅ€ใ€‚", + "login_form_server_empty": "่พ“ๅ…ฅๆœๅŠกๅ™จๅœฐๅ€", "login_form_server_error": "ๆ— ๆณ•่ฟžๆŽฅๅˆฐๆœๅŠกๅ™จใ€‚", "login_password_changed_error": "ๆ›ดๆ–ฐๅฏ†็ ๆ—ถๅ‡บ้”™\n", "login_password_changed_success": "ๅฏ†็ ๆ›ดๆ–ฐๆˆๅŠŸ", @@ -267,7 +268,7 @@ "map_settings_date_range_option_all": "ๆ‰€ๆœ‰", "map_settings_date_range_option_day": "่ฟ‡ๅŽป24ๅฐๆ—ถ", "map_settings_date_range_option_days": "{}ๅคฉๅ‰", - "map_settings_date_range_option_year": "ๅŽปๅนด", + "map_settings_date_range_option_year": "1ๅนดๅ‰", "map_settings_date_range_option_years": "{}ๅนดๅ‰", "map_settings_dialog_cancel": "ๅ–ๆถˆ", "map_settings_dialog_save": "ไฟๅญ˜", @@ -343,7 +344,7 @@ "search_page_your_activity": "ๆ‚จ็š„ๆดปๅŠจ", "search_page_your_map": "่ถณ่ฟน", "search_result_page_new_search_hint": "ๆœ็ดขๆ–ฐ็š„", - "search_suggestion_list_smart_search_hint_1": "้ป˜่ฎคๆƒ…ๅ†ตไธ‹ๅฏ็”จๆ™บ่ƒฝๆœ็ดข๏ผ›่ฆๆœ็ดขๅ…ƒๆ•ฐๆฎ๏ผŒ่ฏทไฝฟ็”จ็›ธๅ…ณ่ฏญๆณ•", + "search_suggestion_list_smart_search_hint_1": "้ป˜่ฎคๆƒ…ๅ†ตไธ‹ๅฏ็”จๆ™บ่ƒฝๆœ็ดข๏ผŒ่ฆๆœ็ดขๅ…ƒๆ•ฐๆฎ๏ผŒ่ฏทไฝฟ็”จ็›ธๅ…ณ่ฏญๆณ•", "search_suggestion_list_smart_search_hint_2": "m:ๆ‚จ็š„ๆœ็ดขๅ…ณ้”ฎ่ฏ", "select_additional_user_for_sharing_page_suggestions": "ๅปบ่ฎฎ", "select_user_for_sharing_page_err_album": "ๅˆ›ๅปบ็›ธๅ†Œๅคฑ่ดฅ", diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index a9ac5b338..6081988b7 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -180,4 +180,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index caf1dd8e6..328fe1536 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.97.0" + version_number: "1.98.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart b/mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart index 224eb838e..5daeb389e 100644 --- a/mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart +++ b/mobile/lib/modules/asset_viewer/hooks/chewiew_controller_hook.dart @@ -1,26 +1,19 @@ -import 'dart:async'; - import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/models/store.dart'; import 'package:video_player/video_player.dart'; -import 'package:immich_mobile/shared/models/store.dart' as store; -import 'package:wakelock_plus/wakelock_plus.dart'; /// Provides the initialized video player controller /// If the asset is local, use the local file /// Otherwise, use a video player with a URL -ChewieController? useChewieController( - Asset asset, { +ChewieController useChewieController({ + required VideoPlayerController controller, EdgeInsets controlsSafeAreaMinimum = const EdgeInsets.only( bottom: 100, ), bool showOptions = true, bool showControlsOnInitialize = false, bool autoPlay = true, - bool autoInitialize = true, bool allowFullScreen = false, bool allowedScreenSleep = false, bool showControls = true, @@ -33,7 +26,7 @@ ChewieController? useChewieController( }) { return use( _ChewieControllerHook( - asset: asset, + controller: controller, placeholder: placeholder, showOptions: showOptions, controlsSafeAreaMinimum: controlsSafeAreaMinimum, @@ -43,7 +36,6 @@ ChewieController? useChewieController( hideControlsTimer: hideControlsTimer, showControlsOnInitialize: showControlsOnInitialize, showControls: showControls, - autoInitialize: autoInitialize, allowedScreenSleep: allowedScreenSleep, onPlaying: onPlaying, onPaused: onPaused, @@ -52,13 +44,12 @@ ChewieController? useChewieController( ); } -class _ChewieControllerHook extends Hook { - final Asset asset; +class _ChewieControllerHook extends Hook { + final VideoPlayerController controller; final EdgeInsets controlsSafeAreaMinimum; final bool showOptions; final bool showControlsOnInitialize; final bool autoPlay; - final bool autoInitialize; final bool allowFullScreen; final bool allowedScreenSleep; final bool showControls; @@ -70,14 +61,13 @@ class _ChewieControllerHook extends Hook { final VoidCallback? onVideoEnded; const _ChewieControllerHook({ - required this.asset, + required this.controller, this.controlsSafeAreaMinimum = const EdgeInsets.only( bottom: 100, ), this.showOptions = true, this.showControlsOnInitialize = false, this.autoPlay = true, - this.autoInitialize = true, this.allowFullScreen = false, this.allowedScreenSleep = false, this.showControls = true, @@ -94,28 +84,33 @@ class _ChewieControllerHook extends Hook { } class _ChewieControllerHookState - extends HookState { - ChewieController? chewieController; - VideoPlayerController? videoPlayerController; - - @override - void initHook() async { - super.initHook(); - unawaited(_initialize()); - } + extends HookState { + late ChewieController chewieController = ChewieController( + videoPlayerController: hook.controller, + controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum, + showOptions: hook.showOptions, + showControlsOnInitialize: hook.showControlsOnInitialize, + autoPlay: hook.autoPlay, + allowFullScreen: hook.allowFullScreen, + allowedScreenSleep: hook.allowedScreenSleep, + showControls: hook.showControls, + customControls: hook.customControls, + placeholder: hook.placeholder, + hideControlsTimer: hook.hideControlsTimer, + ); @override void dispose() { - chewieController?.dispose(); - videoPlayerController?.dispose(); + chewieController.dispose(); super.dispose(); } @override - ChewieController? build(BuildContext context) { + ChewieController build(BuildContext context) { return chewieController; } + /* /// Initializes the chewie controller and video player controller Future _initialize() async { if (hook.asset.isLocal && hook.asset.livePhotoVideoId == null) { @@ -141,39 +136,21 @@ class _ChewieControllerHookState ); } - videoPlayerController!.addListener(() { - final value = videoPlayerController!.value; - if (value.isPlaying) { - WakelockPlus.enable(); - hook.onPlaying?.call(); - } else if (!value.isPlaying) { - WakelockPlus.disable(); - hook.onPaused?.call(); - } - - if (value.position == value.duration) { - WakelockPlus.disable(); - hook.onVideoEnded?.call(); - } - }); - await videoPlayerController!.initialize(); - setState(() { - chewieController = ChewieController( - videoPlayerController: videoPlayerController!, - controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum, - showOptions: hook.showOptions, - showControlsOnInitialize: hook.showControlsOnInitialize, - autoPlay: hook.autoPlay, - autoInitialize: hook.autoInitialize, - allowFullScreen: hook.allowFullScreen, - allowedScreenSleep: hook.allowedScreenSleep, - showControls: hook.showControls, - customControls: hook.customControls, - placeholder: hook.placeholder, - hideControlsTimer: hook.hideControlsTimer, - ); - }); + chewieController = ChewieController( + videoPlayerController: videoPlayerController!, + controlsSafeAreaMinimum: hook.controlsSafeAreaMinimum, + showOptions: hook.showOptions, + showControlsOnInitialize: hook.showControlsOnInitialize, + autoPlay: hook.autoPlay, + allowFullScreen: hook.allowFullScreen, + allowedScreenSleep: hook.allowedScreenSleep, + showControls: hook.showControls, + customControls: hook.customControls, + placeholder: hook.placeholder, + hideControlsTimer: hook.hideControlsTimer, + ); } + */ } diff --git a/mobile/lib/modules/asset_viewer/providers/asset_people.provider.dart b/mobile/lib/modules/asset_viewer/providers/asset_people.provider.dart new file mode 100644 index 000000000..a856a0014 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/asset_people.provider.dart @@ -0,0 +1,51 @@ +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/services/asset.service.dart'; +import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'asset_people.provider.g.dart'; + +/// Maintains the list of people for an asset. +@riverpod +class AssetPeopleNotifier extends _$AssetPeopleNotifier { + final log = Logger('AssetPeopleNotifier'); + + @override + Future> build(Asset asset) async { + if (!asset.isRemote) { + return []; + } + + final list = await ref + .watch(assetServiceProvider) + .getRemotePeopleOfAsset(asset.remoteId!); + if (list == null) { + return []; + } + + // explicitly a sorted slice to make it deterministic + // named people will be at the beginning, and names are sorted + // ascendingly + list.sort((a, b) { + final aNotEmpty = a.name.isNotEmpty; + final bNotEmpty = b.name.isNotEmpty; + if (aNotEmpty && !bNotEmpty) { + return -1; + } else if (!aNotEmpty && bNotEmpty) { + return 1; + } else if (!aNotEmpty && !bNotEmpty) { + return 0; + } + + return a.name.compareTo(b.name); + }); + return list; + } + + Future refresh() async { + // invalidate the state โ€“ this way we don't have to + // duplicate the code from build. + ref.invalidateSelf(); + } +} diff --git a/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart b/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart new file mode 100644 index 000000000..449d5b6c8 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/asset_people.provider.g.dart @@ -0,0 +1,189 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'asset_people.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$assetPeopleNotifierHash() => + r'192a4ee188f781000fe43f1675c49e1081ccc631'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$AssetPeopleNotifier extends BuildlessAutoDisposeAsyncNotifier< + List> { + late final Asset asset; + + Future> build( + Asset asset, + ); +} + +/// Maintains the list of people for an asset. +/// +/// Copied from [AssetPeopleNotifier]. +@ProviderFor(AssetPeopleNotifier) +const assetPeopleNotifierProvider = AssetPeopleNotifierFamily(); + +/// Maintains the list of people for an asset. +/// +/// Copied from [AssetPeopleNotifier]. +class AssetPeopleNotifierFamily + extends Family>> { + /// Maintains the list of people for an asset. + /// + /// Copied from [AssetPeopleNotifier]. + const AssetPeopleNotifierFamily(); + + /// Maintains the list of people for an asset. + /// + /// Copied from [AssetPeopleNotifier]. + AssetPeopleNotifierProvider call( + Asset asset, + ) { + return AssetPeopleNotifierProvider( + asset, + ); + } + + @override + AssetPeopleNotifierProvider getProviderOverride( + covariant AssetPeopleNotifierProvider provider, + ) { + return call( + provider.asset, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'assetPeopleNotifierProvider'; +} + +/// Maintains the list of people for an asset. +/// +/// Copied from [AssetPeopleNotifier]. +class AssetPeopleNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl< + AssetPeopleNotifier, List> { + /// Maintains the list of people for an asset. + /// + /// Copied from [AssetPeopleNotifier]. + AssetPeopleNotifierProvider( + Asset asset, + ) : this._internal( + () => AssetPeopleNotifier()..asset = asset, + from: assetPeopleNotifierProvider, + name: r'assetPeopleNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$assetPeopleNotifierHash, + dependencies: AssetPeopleNotifierFamily._dependencies, + allTransitiveDependencies: + AssetPeopleNotifierFamily._allTransitiveDependencies, + asset: asset, + ); + + AssetPeopleNotifierProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.asset, + }) : super.internal(); + + final Asset asset; + + @override + Future> runNotifierBuild( + covariant AssetPeopleNotifier notifier, + ) { + return notifier.build( + asset, + ); + } + + @override + Override overrideWith(AssetPeopleNotifier Function() create) { + return ProviderOverride( + origin: this, + override: AssetPeopleNotifierProvider._internal( + () => create()..asset = asset, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + asset: asset, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement> createElement() { + return _AssetPeopleNotifierProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is AssetPeopleNotifierProvider && other.asset == asset; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, asset.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin AssetPeopleNotifierRef + on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `asset` of this provider. + Asset get asset; +} + +class _AssetPeopleNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement> with AssetPeopleNotifierRef { + _AssetPeopleNotifierProviderElement(super.provider); + + @override + Asset get asset => (origin as AssetPeopleNotifierProvider).asset; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.dart b/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.dart index 5c20e1479..b6928c6ba 100644 --- a/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.dart @@ -2,6 +2,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:isar/isar.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'asset_stack.provider.g.dart'; class AssetStackNotifier extends StateNotifier> { final Asset _asset; @@ -49,3 +52,8 @@ final assetStackProvider = .sortByFileCreatedAtDesc() .findAll(); }); + +@riverpod +int assetStackIndex(AssetStackIndexRef ref, Asset asset) { + return -1; +} diff --git a/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.g.dart b/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.g.dart new file mode 100644 index 000000000..142e46d32 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/asset_stack.provider.g.dart @@ -0,0 +1,158 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'asset_stack.provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$assetStackIndexHash() => r'0f2df55e929767c8c698bd432b5e6e351d000a16'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [assetStackIndex]. +@ProviderFor(assetStackIndex) +const assetStackIndexProvider = AssetStackIndexFamily(); + +/// See also [assetStackIndex]. +class AssetStackIndexFamily extends Family { + /// See also [assetStackIndex]. + const AssetStackIndexFamily(); + + /// See also [assetStackIndex]. + AssetStackIndexProvider call( + Asset asset, + ) { + return AssetStackIndexProvider( + asset, + ); + } + + @override + AssetStackIndexProvider getProviderOverride( + covariant AssetStackIndexProvider provider, + ) { + return call( + provider.asset, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'assetStackIndexProvider'; +} + +/// See also [assetStackIndex]. +class AssetStackIndexProvider extends AutoDisposeProvider { + /// See also [assetStackIndex]. + AssetStackIndexProvider( + Asset asset, + ) : this._internal( + (ref) => assetStackIndex( + ref as AssetStackIndexRef, + asset, + ), + from: assetStackIndexProvider, + name: r'assetStackIndexProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$assetStackIndexHash, + dependencies: AssetStackIndexFamily._dependencies, + allTransitiveDependencies: + AssetStackIndexFamily._allTransitiveDependencies, + asset: asset, + ); + + AssetStackIndexProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.asset, + }) : super.internal(); + + final Asset asset; + + @override + Override overrideWith( + int Function(AssetStackIndexRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: AssetStackIndexProvider._internal( + (ref) => create(ref as AssetStackIndexRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + asset: asset, + ), + ); + } + + @override + AutoDisposeProviderElement createElement() { + return _AssetStackIndexProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is AssetStackIndexProvider && other.asset == asset; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, asset.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin AssetStackIndexRef on AutoDisposeProviderRef { + /// The parameter `asset` of this provider. + Asset get asset; +} + +class _AssetStackIndexProviderElement extends AutoDisposeProviderElement + with AssetStackIndexRef { + _AssetStackIndexProviderElement(super.provider); + + @override + Asset get asset => (origin as AssetStackIndexProvider).asset; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.dart b/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.dart new file mode 100644 index 000000000..714c38e2a --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.dart @@ -0,0 +1,44 @@ +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/store.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:video_player/video_player.dart'; + +part 'video_player_controller_provider.g.dart'; + +@riverpod +Future videoPlayerController( + VideoPlayerControllerRef ref, { + required Asset asset, +}) async { + late VideoPlayerController controller; + if (asset.isLocal && asset.livePhotoVideoId == null) { + // Use a local file for the video player controller + final file = await asset.local!.file; + if (file == null) { + throw Exception('No file found for the video'); + } + controller = VideoPlayerController.file(file); + } else { + // Use a network URL for the video player controller + final serverEndpoint = Store.get(StoreKey.serverEndpoint); + final String videoUrl = asset.livePhotoVideoId != null + ? '$serverEndpoint/asset/file/${asset.livePhotoVideoId}' + : '$serverEndpoint/asset/file/${asset.remoteId}'; + + final url = Uri.parse(videoUrl); + final accessToken = Store.get(StoreKey.accessToken); + + controller = VideoPlayerController.networkUrl( + url, + httpHeaders: {"x-immich-user-token": accessToken}, + ); + } + + await controller.initialize(); + + ref.onDispose(() { + controller.dispose(); + }); + + return controller; +} diff --git a/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart b/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart new file mode 100644 index 000000000..a9b287e95 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/providers/video_player_controller_provider.g.dart @@ -0,0 +1,164 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'video_player_controller_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$videoPlayerControllerHash() => + r'72b45de66542021717807655e25ec92d78d80eec'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [videoPlayerController]. +@ProviderFor(videoPlayerController) +const videoPlayerControllerProvider = VideoPlayerControllerFamily(); + +/// See also [videoPlayerController]. +class VideoPlayerControllerFamily + extends Family> { + /// See also [videoPlayerController]. + const VideoPlayerControllerFamily(); + + /// See also [videoPlayerController]. + VideoPlayerControllerProvider call({ + required Asset asset, + }) { + return VideoPlayerControllerProvider( + asset: asset, + ); + } + + @override + VideoPlayerControllerProvider getProviderOverride( + covariant VideoPlayerControllerProvider provider, + ) { + return call( + asset: provider.asset, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'videoPlayerControllerProvider'; +} + +/// See also [videoPlayerController]. +class VideoPlayerControllerProvider + extends AutoDisposeFutureProvider { + /// See also [videoPlayerController]. + VideoPlayerControllerProvider({ + required Asset asset, + }) : this._internal( + (ref) => videoPlayerController( + ref as VideoPlayerControllerRef, + asset: asset, + ), + from: videoPlayerControllerProvider, + name: r'videoPlayerControllerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$videoPlayerControllerHash, + dependencies: VideoPlayerControllerFamily._dependencies, + allTransitiveDependencies: + VideoPlayerControllerFamily._allTransitiveDependencies, + asset: asset, + ); + + VideoPlayerControllerProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.asset, + }) : super.internal(); + + final Asset asset; + + @override + Override overrideWith( + FutureOr Function(VideoPlayerControllerRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: VideoPlayerControllerProvider._internal( + (ref) => create(ref as VideoPlayerControllerRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + asset: asset, + ), + ); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _VideoPlayerControllerProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is VideoPlayerControllerProvider && other.asset == asset; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, asset.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin VideoPlayerControllerRef + on AutoDisposeFutureProviderRef { + /// The parameter `asset` of this provider. + Asset get asset; +} + +class _VideoPlayerControllerProviderElement + extends AutoDisposeFutureProviderElement + with VideoPlayerControllerRef { + _VideoPlayerControllerProviderElement(super.provider); + + @override + Asset get asset => (origin as VideoPlayerControllerProvider).asset; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/mobile/lib/modules/asset_viewer/providers/video_player_controls_provider.dart b/mobile/lib/modules/asset_viewer/providers/video_player_controls_provider.dart index b73824f86..d93535893 100644 --- a/mobile/lib/modules/asset_viewer/providers/video_player_controls_provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/video_player_controls_provider.dart @@ -1,10 +1,15 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; class VideoPlaybackControls { - VideoPlaybackControls({required this.position, required this.mute}); + VideoPlaybackControls({ + required this.position, + required this.mute, + required this.pause, + }); final double position; final bool mute; + final bool pause; } final videoPlayerControlsProvider = @@ -17,6 +22,7 @@ class VideoPlayerControls extends StateNotifier { : super( VideoPlaybackControls( position: 0, + pause: false, mute: false, ), ); @@ -29,18 +35,62 @@ class VideoPlayerControls extends StateNotifier { state = value; } + void reset() { + state = VideoPlaybackControls( + position: 0, + pause: false, + mute: false, + ); + } + double get position => state.position; bool get mute => state.mute; set position(double value) { - state = VideoPlaybackControls(position: value, mute: state.mute); + state = VideoPlaybackControls( + position: value, + mute: state.mute, + pause: state.pause, + ); } set mute(bool value) { - state = VideoPlaybackControls(position: state.position, mute: value); + state = VideoPlaybackControls( + position: state.position, + mute: value, + pause: state.pause, + ); } void toggleMute() { - state = VideoPlaybackControls(position: state.position, mute: !state.mute); + state = VideoPlaybackControls( + position: state.position, + mute: !state.mute, + pause: state.pause, + ); + } + + void pause() { + state = VideoPlaybackControls( + position: state.position, + mute: state.mute, + pause: true, + ); + } + + void play() { + state = VideoPlaybackControls( + position: state.position, + mute: state.mute, + pause: false, + ); + } + + void togglePlay() { + state = VideoPlaybackControls( + position: state.position, + mute: state.mute, + pause: !state.pause, + ); } } diff --git a/mobile/lib/modules/asset_viewer/providers/video_player_value_provider.dart b/mobile/lib/modules/asset_viewer/providers/video_player_value_provider.dart index 66f9389a0..ebdf739ef 100644 --- a/mobile/lib/modules/asset_viewer/providers/video_player_value_provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/video_player_value_provider.dart @@ -1,10 +1,65 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:video_player/video_player.dart'; + +enum VideoPlaybackState { + initializing, + paused, + playing, + buffering, + completed, +} class VideoPlaybackValue { - VideoPlaybackValue({required this.position, required this.duration}); - + /// The current position of the video final Duration position; + + /// The total duration of the video final Duration duration; + + /// The current state of the video playback + final VideoPlaybackState state; + + /// The volume of the video + final double volume; + + VideoPlaybackValue({ + required this.position, + required this.duration, + required this.state, + required this.volume, + }); + + factory VideoPlaybackValue.fromController(VideoPlayerController? controller) { + final video = controller?.value; + late VideoPlaybackState s; + if (video == null) { + s = VideoPlaybackState.initializing; + } else if (video.isCompleted) { + s = VideoPlaybackState.completed; + } else if (video.isPlaying) { + s = VideoPlaybackState.playing; + } else if (video.isBuffering) { + s = VideoPlaybackState.buffering; + } else { + s = VideoPlaybackState.paused; + } + + return VideoPlaybackValue( + position: video?.position ?? Duration.zero, + duration: video?.duration ?? Duration.zero, + state: s, + volume: video?.volume ?? 0.0, + ); + } + + factory VideoPlaybackValue.uninitialized() { + return VideoPlaybackValue( + position: Duration.zero, + duration: Duration.zero, + state: VideoPlaybackState.initializing, + volume: 0.0, + ); + } } final videoPlaybackValueProvider = @@ -15,10 +70,7 @@ final videoPlaybackValueProvider = class VideoPlaybackValueState extends StateNotifier { VideoPlaybackValueState(this.ref) : super( - VideoPlaybackValue( - position: Duration.zero, - duration: Duration.zero, - ), + VideoPlaybackValue.uninitialized(), ); final Ref ref; @@ -30,6 +82,11 @@ class VideoPlaybackValueState extends StateNotifier { } set position(Duration value) { - state = VideoPlaybackValue(position: value, duration: state.duration); + state = VideoPlaybackValue( + position: value, + duration: state.duration, + state: state.state, + volume: state.volume, + ); } } diff --git a/mobile/lib/modules/asset_viewer/ui/bottom_gallery_bar.dart b/mobile/lib/modules/asset_viewer/ui/bottom_gallery_bar.dart new file mode 100644 index 000000000..a7d5e4e71 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/bottom_gallery_bar.dart @@ -0,0 +1,345 @@ +import 'dart:io'; + +import 'package:auto_route/auto_route.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/modules/asset_viewer/providers/asset_stack.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/video_controls.dart'; +import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/shared/providers/server_info.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; + +class BottomGalleryBar extends ConsumerWidget { + final Asset asset; + final bool showStack; + final int stackIndex; + final int totalAssets; + final bool showVideoPlayerControls; + final PageController controller; + + const BottomGalleryBar({ + super.key, + required this.showStack, + required this.stackIndex, + required this.asset, + required this.controller, + required this.totalAssets, + required this.showVideoPlayerControls, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; + + final stack = showStack && asset.stackChildrenCount > 0 + ? ref.watch(assetStackStateProvider(asset)) + : []; + final stackElements = showStack ? [asset, ...stack] : []; + bool isParent = stackIndex == -1 || stackIndex == 0; + final navStack = AutoRouter.of(context).stackData; + final isTrashEnabled = + ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); + final isFromTrash = isTrashEnabled && + navStack.length > 2 && + navStack.elementAt(navStack.length - 2).name == TrashRoute.name; + // !!!! itemsList and actionlist should always be in sync + final itemsList = [ + BottomNavigationBarItem( + icon: Icon( + Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, + ), + label: 'control_bottom_app_bar_share'.tr(), + tooltip: 'control_bottom_app_bar_share'.tr(), + ), + if (isOwner) + asset.isArchived + ? BottomNavigationBarItem( + icon: const Icon(Icons.unarchive_rounded), + label: 'control_bottom_app_bar_unarchive'.tr(), + tooltip: 'control_bottom_app_bar_unarchive'.tr(), + ) + : BottomNavigationBarItem( + icon: const Icon(Icons.archive_outlined), + label: 'control_bottom_app_bar_archive'.tr(), + tooltip: 'control_bottom_app_bar_archive'.tr(), + ), + if (isOwner && stack.isNotEmpty) + BottomNavigationBarItem( + icon: const Icon(Icons.burst_mode_outlined), + label: 'control_bottom_app_bar_stack'.tr(), + tooltip: 'control_bottom_app_bar_stack'.tr(), + ), + if (isOwner) + BottomNavigationBarItem( + icon: const Icon(Icons.delete_outline), + label: 'control_bottom_app_bar_delete'.tr(), + tooltip: 'control_bottom_app_bar_delete'.tr(), + ), + if (!isOwner) + BottomNavigationBarItem( + icon: const Icon(Icons.download_outlined), + label: 'download'.tr(), + tooltip: 'download'.tr(), + ), + ]; + + void removeAssetFromStack() { + if (stackIndex > 0 && showStack) { + ref + .read(assetStackStateProvider(asset).notifier) + .removeChild(stackIndex - 1); + } + } + + void handleDelete() async { + // Cannot delete readOnly / external assets. They are handled through library offline jobs + if (asset.isReadOnly) { + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'asset_action_delete_err_read_only'.tr(), + gravity: ToastGravity.BOTTOM, + ); + return; + } + Future onDelete(bool force) async { + final isDeleted = await ref.read(assetProvider.notifier).deleteAssets( + {asset}, + force: force, + ); + if (isDeleted && isParent) { + if (totalAssets == 1) { + // Handle only one asset + context.popRoute(); + } else { + // Go to next page otherwise + controller.nextPage( + duration: const Duration(milliseconds: 100), + curve: Curves.fastLinearToSlowEaseIn, + ); + } + } + return isDeleted; + } + + // Asset is trashed + if (isTrashEnabled && !isFromTrash) { + final isDeleted = await onDelete(false); + if (isDeleted) { + // Can only trash assets stored in server. Local assets are always permanently removed for now + if (context.mounted && asset.isRemote && isParent) { + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'Asset trashed', + gravity: ToastGravity.BOTTOM, + ); + } + removeAssetFromStack(); + } + return; + } + + // Asset is permanently removed + showDialog( + context: context, + builder: (BuildContext _) { + return DeleteDialog( + onDelete: () async { + final isDeleted = await onDelete(true); + if (isDeleted) { + removeAssetFromStack(); + } + }, + ); + }, + ); + } + + void showStackActionItems() { + showModalBottomSheet( + context: context, + enableDrag: false, + builder: (BuildContext ctx) { + return SafeArea( + child: Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isParent) + ListTile( + leading: const Icon( + Icons.bookmark_border_outlined, + size: 24, + ), + onTap: () async { + await ref + .read(assetStackServiceProvider) + .updateStackParent( + asset, + stackElements.elementAt(stackIndex), + ); + ctx.pop(); + context.popRoute(); + }, + title: const Text( + "viewer_stack_use_as_main_asset", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + ), + ListTile( + leading: const Icon( + Icons.copy_all_outlined, + size: 24, + ), + onTap: () async { + if (isParent) { + await ref + .read(assetStackServiceProvider) + .updateStackParent( + asset, + stackElements + .elementAt(1), // Next asset as parent + ); + // Remove itself from stack + await ref.read(assetStackServiceProvider).updateStack( + stackElements.elementAt(1), + childrenToRemove: [asset], + ); + ctx.pop(); + context.popRoute(); + } else { + await ref.read(assetStackServiceProvider).updateStack( + asset, + childrenToRemove: [ + stackElements.elementAt(stackIndex), + ], + ); + removeAssetFromStack(); + ctx.pop(); + } + }, + title: const Text( + "viewer_remove_from_stack", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + ), + ListTile( + leading: const Icon( + Icons.filter_none_outlined, + size: 18, + ), + onTap: () async { + await ref.read(assetStackServiceProvider).updateStack( + asset, + childrenToRemove: stack, + ); + ctx.pop(); + context.popRoute(); + }, + title: const Text( + "viewer_unstack", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + ), + ], + ), + ), + ); + }, + ); + } + + shareAsset() { + if (asset.isOffline) { + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'asset_action_share_err_offline'.tr(), + gravity: ToastGravity.BOTTOM, + ); + return; + } + ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context); + } + + handleArchive() { + ref.read(assetProvider.notifier).toggleArchive([asset]); + if (isParent) { + context.popRoute(); + return; + } + removeAssetFromStack(); + } + + handleDownload() { + if (asset.isLocal) { + return; + } + if (asset.isOffline) { + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'asset_action_share_err_offline'.tr(), + gravity: ToastGravity.BOTTOM, + ); + return; + } + + ref.read(imageViewerStateProvider.notifier).downloadAsset( + asset, + context, + ); + } + + List actionslist = [ + (_) => shareAsset(), + if (isOwner) (_) => handleArchive(), + if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(), + if (isOwner) (_) => handleDelete(), + if (!isOwner) (_) => handleDownload(), + ]; + + return IgnorePointer( + ignoring: !ref.watch(showControlsProvider), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, + child: Column( + children: [ + Visibility( + visible: showVideoPlayerControls, + child: const VideoControls(), + ), + BottomNavigationBar( + backgroundColor: Colors.black.withOpacity(0.4), + unselectedIconTheme: const IconThemeData(color: Colors.white), + selectedIconTheme: const IconThemeData(color: Colors.white), + unselectedLabelStyle: const TextStyle(color: Colors.black), + selectedLabelStyle: const TextStyle(color: Colors.black), + showSelectedLabels: false, + showUnselectedLabels: false, + items: itemsList, + onTap: (index) { + if (index < actionslist.length) { + actionslist[index].call(index); + } + }, + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/custom_video_player_controls.dart b/mobile/lib/modules/asset_viewer/ui/custom_video_player_controls.dart new file mode 100644 index 000000000..0e8f14301 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/custom_video_player_controls.dart @@ -0,0 +1,107 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart'; +import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/hooks/timer_hook.dart'; + +class CustomVideoPlayerControls extends HookConsumerWidget { + final Duration hideTimerDuration; + + const CustomVideoPlayerControls({ + super.key, + this.hideTimerDuration = const Duration(seconds: 3), + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // A timer to hide the controls + final hideTimer = useTimer( + hideTimerDuration, + () { + final state = ref.read(videoPlaybackValueProvider).state; + // Do not hide on paused + if (state != VideoPlaybackState.paused) { + ref.read(showControlsProvider.notifier).show = false; + } + }, + ); + + final showBuffering = useState(false); + final VideoPlaybackState state = + ref.watch(videoPlaybackValueProvider).state; + + /// Shows the controls and starts the timer to hide them + void showControlsAndStartHideTimer() { + hideTimer.reset(); + ref.read(showControlsProvider.notifier).show = true; + } + + // When we mute, show the controls + ref.listen(videoPlayerControlsProvider.select((v) => v.mute), + (previous, next) { + showControlsAndStartHideTimer(); + }); + + // When we change position, show or hide timer + ref.listen(videoPlayerControlsProvider.select((v) => v.position), + (previous, next) { + showControlsAndStartHideTimer(); + }); + + ref.listen(videoPlaybackValueProvider.select((value) => value.state), + (_, state) { + // Show buffering + showBuffering.value = state == VideoPlaybackState.buffering; + }); + + /// Toggles between playing and pausing depending on the state of the video + void togglePlay() { + showControlsAndStartHideTimer(); + final state = ref.read(videoPlaybackValueProvider).state; + if (state == VideoPlaybackState.playing) { + ref.read(videoPlayerControlsProvider.notifier).pause(); + } else { + ref.read(videoPlayerControlsProvider.notifier).play(); + } + } + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: showControlsAndStartHideTimer, + child: AbsorbPointer( + absorbing: !ref.watch(showControlsProvider), + child: Stack( + children: [ + if (showBuffering.value) + const Center( + child: DelayedLoadingIndicator( + fadeInDuration: Duration(milliseconds: 400), + ), + ) + else + GestureDetector( + onTap: () { + if (state != VideoPlaybackState.playing) { + togglePlay(); + } + ref.read(showControlsProvider.notifier).show = false; + }, + child: CenterPlayButton( + backgroundColor: Colors.black54, + iconColor: Colors.white, + isFinished: state == VideoPlaybackState.completed, + isPlaying: state == VideoPlaybackState.playing, + show: ref.watch(showControlsProvider), + onPressed: togglePlay, + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart deleted file mode 100644 index 3c6d5f2b6..000000000 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ /dev/null @@ -1,396 +0,0 @@ -import 'dart:io'; - -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart'; -import 'package:immich_mobile/modules/map/widgets/map_thumbnail.dart'; -import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; -import 'package:immich_mobile/utils/bytes_units.dart'; -import 'package:maplibre_gl/maplibre_gl.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class ExifBottomSheet extends HookConsumerWidget { - final Asset asset; - - const ExifBottomSheet({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - final exifInfo = (assetWithExif.value ?? asset).exifInfo; - var textColor = context.isDarkTheme ? Colors.white : Colors.black; - - bool hasCoordinates() => - exifInfo != null && - exifInfo.latitude != null && - exifInfo.longitude != null && - exifInfo.latitude != 0 && - exifInfo.longitude != 0; - - String formattedDateTime() { - final (dt, timeZone) = - (assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(dt); - final time = DateFormat.jm().format(dt); - - return '$date โ€ข $time GMT${timeZone.formatAsOffset()}'; - } - - Future createCoordinatesUri() async { - if (!hasCoordinates()) { - return null; - } - - final double latitude = exifInfo!.latitude!; - final double longitude = exifInfo.longitude!; - - const zoomLevel = 16; - - if (Platform.isAndroid) { - Uri uri = Uri( - scheme: 'geo', - host: '$latitude,$longitude', - queryParameters: { - 'z': '$zoomLevel', - 'q': '$latitude,$longitude($formattedDateTime)', - }, - ); - if (await canLaunchUrl(uri)) { - return uri; - } - } else if (Platform.isIOS) { - var params = { - 'll': '$latitude,$longitude', - 'q': formattedDateTime, - 'z': '$zoomLevel', - }; - Uri uri = Uri.https('maps.apple.com', '/', params); - if (await canLaunchUrl(uri)) { - return uri; - } - } - - return Uri( - scheme: 'https', - host: 'openstreetmap.org', - queryParameters: {'mlat': '$latitude', 'mlon': '$longitude'}, - fragment: 'map=$zoomLevel/$latitude/$longitude', - ); - } - - buildMap() { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: LayoutBuilder( - builder: (context, constraints) { - return MapThumbnail( - centre: LatLng( - exifInfo?.latitude ?? 0, - exifInfo?.longitude ?? 0, - ), - height: 150, - width: constraints.maxWidth, - zoom: 12.0, - assetMarkerRemoteId: asset.remoteId, - onTap: (tapPosition, latLong) async { - Uri? uri = await createCoordinatesUri(); - - if (uri == null) { - return; - } - - debugPrint('Opening Map Uri: $uri'); - launchUrl(uri); - }, - ); - }, - ), - ); - } - - buildSizeText(Asset a) { - String resolution = a.width != null && a.height != null - ? "${a.height} x ${a.width} " - : ""; - String fileSize = a.exifInfo?.fileSize != null - ? formatBytes(a.exifInfo!.fileSize!) - : ""; - String text = resolution + fileSize; - return text.isNotEmpty ? text : null; - } - - buildLocation() { - // Guard no lat/lng - if (!hasCoordinates()) { - return asset.isRemote && !asset.isReadOnly - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "exif_bottom_sheet_location_add", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - onTap: () => handleEditLocation( - ref, - context, - [assetWithExif.value ?? asset], - ), - ) - : const SizedBox.shrink(); - } - - return Column( - children: [ - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: - context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote && !asset.isReadOnly) - IconButton( - onPressed: () => handleEditLocation( - ref, - context, - [assetWithExif.value ?? asset], - ), - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ], - ), - buildMap(), - RichText( - text: TextSpan( - style: context.textTheme.labelLarge, - children: [ - if (exifInfo != null && exifInfo.city != null) - TextSpan( - text: exifInfo.city, - ), - if (exifInfo != null && - exifInfo.city != null && - exifInfo.state != null) - const TextSpan( - text: ", ", - ), - if (exifInfo != null && exifInfo.state != null) - TextSpan( - text: "${exifInfo.state}", - ), - ], - ), - ), - Text( - "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(150), - ), - ), - ], - ), - ], - ); - } - - buildDate() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - formattedDateTime(), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - if (asset.isRemote && !asset.isReadOnly) - IconButton( - onPressed: () => handleEditDateTime( - ref, - context, - [assetWithExif.value ?? asset], - ), - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ], - ); - } - - buildImageProperties() { - // Helper to create the ListTile and avoid repeating code - createImagePropertiesListStyle(title, subtitle) => ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon( - Icons.image, - color: textColor.withAlpha(200), - ), - titleAlignment: ListTileTitleAlignment.center, - title: Text( - title, - style: context.textTheme.labelLarge, - ), - subtitle: subtitle, - ); - - final imgSizeString = buildSizeText(asset); - - if (imgSizeString == null && asset.fileName.isNotEmpty) { - // There is only filename - return createImagePropertiesListStyle( - asset.fileName, - null, - ); - } else if (imgSizeString != null && asset.fileName.isNotEmpty) { - // There is both filename and size information - return createImagePropertiesListStyle( - asset.fileName, - Text(imgSizeString, style: context.textTheme.bodySmall), - ); - } else if (imgSizeString != null && asset.fileName.isEmpty) { - // There is only size information - return createImagePropertiesListStyle( - imgSizeString, - null, - ); - } - } - - buildDetail() { - final imgProperties = buildImageProperties(); - - // There are no details - if (imgProperties == null && - (exifInfo == null || exifInfo.make == null)) { - return Container(); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - if (imgProperties != null) imgProperties, - if (exifInfo?.make != null) - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon( - Icons.camera, - color: textColor.withAlpha(200), - ), - title: Text( - "${exifInfo!.make} ${exifInfo.model}", - style: context.textTheme.labelLarge, - ), - subtitle: exifInfo.f != null || - exifInfo.exposureSeconds != null || - exifInfo.mm != null || - exifInfo.iso != null - ? Text( - "ฦ’/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ), - ], - ); - } - - return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16.0), - child: LayoutBuilder( - builder: (context, constraints) { - if (constraints.maxWidth > 600) { - // Two column - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildDate(), - if (asset.isRemote) DescriptionInput(asset: asset), - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: buildLocation(), - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: buildDetail(), - ), - ), - ], - ), - const SizedBox(height: 50), - ], - ); - } - - // One column - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - buildDate(), - assetWithExif.when( - data: (data) => DescriptionInput(asset: data), - error: (error, stackTrace) => Icon( - Icons.image_not_supported_outlined, - color: context.primaryColor, - ), - loading: () => const SizedBox( - width: 75, - height: 75, - child: CircularProgressIndicator.adaptive(), - ), - ), - buildLocation(), - SizedBox(height: hasCoordinates() ? 16.0 : 6.0), - buildDetail(), - const SizedBox(height: 50), - ], - ); - }, - ), - ), - ); - } -} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_bottom_sheet.dart new file mode 100644 index 000000000..00d5a1ae6 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_bottom_sheet.dart @@ -0,0 +1,210 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_detail.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_image_properties.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_location.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_people.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/exif_info.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/utils/selection_handlers.dart'; + +class ExifBottomSheet extends HookConsumerWidget { + final Asset asset; + + const ExifBottomSheet({super.key, required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetWithExif = ref.watch(assetDetailProvider(asset)); + var textColor = context.isDarkTheme ? Colors.white : Colors.black; + final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; + // Format the date time with the timezone + final (dt, timeZone) = + (assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset(); + final date = DateFormat.yMMMEd().format(dt); + final time = DateFormat.jm().format(dt); + + String formattedDateTime = '$date โ€ข $time GMT${timeZone.formatAsOffset()}'; + + final dateWidget = Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + formattedDateTime, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + if (asset.isRemote && !asset.isReadOnly) + IconButton( + onPressed: () => handleEditDateTime( + ref, + context, + [assetWithExif.value ?? asset], + ), + icon: const Icon(Icons.edit_outlined), + iconSize: 20, + ), + ], + ); + + return SingleChildScrollView( + padding: const EdgeInsets.only( + bottom: 50, + ), + child: LayoutBuilder( + builder: (context, constraints) { + final horizontalPadding = constraints.maxWidth > 600 ? 24.0 : 16.0; + if (constraints.maxWidth > 600) { + // Two column + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Column( + children: [ + dateWidget, + if (asset.isRemote) DescriptionInput(asset: asset), + ], + ), + ), + ExifPeople( + asset: asset, + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + ), + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ExifLocation( + asset: asset, + exifInfo: exifInfo, + editLocation: () => handleEditLocation( + ref, + context, + [assetWithExif.value ?? asset], + ), + formattedDateTime: formattedDateTime, + ), + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: ExifDetail(asset: asset, exifInfo: exifInfo), + ), + ), + ], + ), + ), + ], + ); + } + + // One column + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + ), + child: Column( + children: [ + dateWidget, + if (asset.isRemote) DescriptionInput(asset: asset), + Padding( + padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0), + child: ExifLocation( + asset: asset, + exifInfo: exifInfo, + editLocation: () => handleEditLocation( + ref, + context, + [assetWithExif.value ?? asset], + ), + formattedDateTime: formattedDateTime, + ), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: ExifPeople( + asset: asset, + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + ), + ), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: horizontalPadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "exif_bottom_sheet_details", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color + ?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + ), + ExifImageProperties(asset: asset), + if (exifInfo?.make != null) + ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: Icon( + Icons.camera, + color: textColor.withAlpha(200), + ), + title: Text( + "${exifInfo!.make} ${exifInfo.model}", + style: context.textTheme.labelLarge, + ), + subtitle: exifInfo.f != null || + exifInfo.exposureSeconds != null || + exifInfo.mm != null || + exifInfo.iso != null + ? Text( + "ฦ’/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", + style: context.textTheme.bodySmall, + ) + : null, + ), + ], + ), + ), + const SizedBox(height: 50), + ], + ); + }, + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_detail.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_detail.dart new file mode 100644 index 000000000..4f4906620 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_detail.dart @@ -0,0 +1,60 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_image_properties.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/exif_info.dart'; + +class ExifDetail extends StatelessWidget { + final Asset asset; + final ExifInfo? exifInfo; + + const ExifDetail({ + super.key, + required this.asset, + this.exifInfo, + }); + + @override + Widget build(BuildContext context) { + final textColor = context.isDarkTheme ? Colors.white : Colors.black; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "exif_bottom_sheet_details", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + ), + ExifImageProperties(asset: asset), + if (exifInfo?.make != null) + ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: Icon( + Icons.camera, + color: textColor.withAlpha(200), + ), + title: Text( + "${exifInfo?.make} ${exifInfo?.model}", + style: context.textTheme.labelLarge, + ), + subtitle: exifInfo?.f != null || + exifInfo?.exposureSeconds != null || + exifInfo?.mm != null || + exifInfo?.iso != null + ? Text( + "ฦ’/${exifInfo?.fNumber} ${exifInfo?.exposureTime} ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ", + style: context.textTheme.bodySmall, + ) + : null, + ), + ], + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_image_properties.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_image_properties.dart new file mode 100644 index 000000000..4f584d1c9 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_image_properties.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/utils/bytes_units.dart'; + +class ExifImageProperties extends StatelessWidget { + final Asset asset; + + const ExifImageProperties({ + super.key, + required this.asset, + }); + + @override + Widget build(BuildContext context) { + final textColor = context.isDarkTheme ? Colors.white : Colors.black; + + String resolution = asset.width != null && asset.height != null + ? "${asset.height} x ${asset.width} " + : ""; + String fileSize = asset.exifInfo?.fileSize != null + ? formatBytes(asset.exifInfo!.fileSize!) + : ""; + String text = resolution + fileSize; + final imgSizeString = text.isNotEmpty ? text : null; + + String? title; + String? subtitle; + + if (imgSizeString == null && asset.fileName.isNotEmpty) { + // There is only filename + title = asset.fileName; + } else if (imgSizeString != null && asset.fileName.isNotEmpty) { + // There is both filename and size information + title = asset.fileName; + subtitle = imgSizeString; + } else if (imgSizeString != null && asset.fileName.isEmpty) { + title = imgSizeString; + } else { + return const SizedBox.shrink(); + } + + return ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: Icon( + Icons.image, + color: textColor.withAlpha(200), + ), + titleAlignment: ListTileTitleAlignment.center, + title: Text( + title, + style: context.textTheme.labelLarge, + ), + subtitle: subtitle == null ? null : Text(subtitle), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_location.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_location.dart new file mode 100644 index 000000000..c4a8b9d50 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_location.dart @@ -0,0 +1,105 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_map.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/models/exif_info.dart'; + +class ExifLocation extends StatelessWidget { + final Asset asset; + final ExifInfo? exifInfo; + final void Function() editLocation; + final String formattedDateTime; + + const ExifLocation({ + super.key, + required this.asset, + required this.exifInfo, + required this.editLocation, + required this.formattedDateTime, + }); + + @override + Widget build(BuildContext context) { + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + // Guard no lat/lng + if (!hasCoordinates) { + return asset.isRemote && !asset.isReadOnly + ? ListTile( + minLeadingWidth: 0, + contentPadding: const EdgeInsets.all(0), + leading: const Icon(Icons.location_on), + title: Text( + "exif_bottom_sheet_location_add", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + onTap: editLocation, + ) + : const SizedBox.shrink(); + } + + return Column( + children: [ + // Location + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "exif_bottom_sheet_location", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + if (asset.isRemote && !asset.isReadOnly) + IconButton( + onPressed: editLocation, + icon: const Icon(Icons.edit_outlined), + iconSize: 20, + ), + ], + ), + ExifMap( + exifInfo: exifInfo!, + formattedDateTime: formattedDateTime, + markerId: asset.remoteId, + ), + RichText( + text: TextSpan( + style: context.textTheme.labelLarge, + children: [ + if (exifInfo != null && exifInfo?.city != null) + TextSpan( + text: exifInfo!.city, + ), + if (exifInfo != null && + exifInfo?.city != null && + exifInfo?.state != null) + const TextSpan( + text: ", ", + ), + if (exifInfo != null && exifInfo?.state != null) + TextSpan( + text: exifInfo!.state, + ), + ], + ), + ), + Text( + "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), + ), + ), + ], + ), + ], + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart new file mode 100644 index 000000000..6c0050aee --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_map.dart @@ -0,0 +1,94 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:immich_mobile/modules/map/widgets/map_thumbnail.dart'; +import 'package:immich_mobile/shared/models/exif_info.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class ExifMap extends StatelessWidget { + final ExifInfo exifInfo; + final String formattedDateTime; + final String? markerId; + + const ExifMap({ + super.key, + required this.exifInfo, + required this.formattedDateTime, + this.markerId = 'marker', + }); + + @override + Widget build(BuildContext context) { + final hasCoordinates = exifInfo.hasCoordinates; + Future createCoordinatesUri() async { + if (!hasCoordinates) { + return null; + } + + final double latitude = exifInfo.latitude!; + final double longitude = exifInfo.longitude!; + + const zoomLevel = 16; + + if (Platform.isAndroid) { + Uri uri = Uri( + scheme: 'geo', + host: '$latitude,$longitude', + queryParameters: { + 'z': '$zoomLevel', + 'q': '$latitude,$longitude($formattedDateTime)', + }, + ); + if (await canLaunchUrl(uri)) { + return uri; + } + } else if (Platform.isIOS) { + var params = { + 'll': '$latitude,$longitude', + 'q': formattedDateTime, + 'z': '$zoomLevel', + }; + Uri uri = Uri.https('maps.apple.com', '/', params); + if (await canLaunchUrl(uri)) { + return uri; + } + } + + return Uri( + scheme: 'https', + host: 'openstreetmap.org', + queryParameters: {'mlat': '$latitude', 'mlon': '$longitude'}, + fragment: 'map=$zoomLevel/$latitude/$longitude', + ); + } + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16.0), + child: LayoutBuilder( + builder: (context, constraints) { + return MapThumbnail( + centre: LatLng( + exifInfo.latitude ?? 0, + exifInfo.longitude ?? 0, + ), + height: 150, + width: constraints.maxWidth, + zoom: 12.0, + assetMarkerRemoteId: markerId, + onTap: (tapPosition, latLong) async { + Uri? uri = await createCoordinatesUri(); + + if (uri == null) { + return; + } + + debugPrint('Opening Map Uri: $uri'); + launchUrl(uri); + }, + ); + }, + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart new file mode 100644 index 000000000..a94a1239f --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/exif_sheet/exif_people.dart @@ -0,0 +1,94 @@ +import 'dart:math' as math; + +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/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/asset_people.provider.dart'; +import 'package:immich_mobile/modules/search/models/curated_content.dart'; +import 'package:immich_mobile/modules/search/ui/curated_people_row.dart'; +import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; + +class ExifPeople extends ConsumerWidget { + final Asset asset; + final EdgeInsets? padding; + + const ExifPeople({super.key, required this.asset, this.padding}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final peopleProvider = + ref.watch(assetPeopleNotifierProvider(asset).notifier); + final people = ref.watch(assetPeopleNotifierProvider(asset)); + final double imageSize = math.min(context.width / 3, 150); + + showPersonNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ).then((_) { + // ensure the people list is up-to-date. + peopleProvider.refresh(); + }); + } + + if (people.value?.isEmpty ?? true) { + // Empty list or loading + return Container(); + } + + final curatedPeople = people.value + ?.map((p) => CuratedContent(id: p.id, label: p.name)) + .toList() ?? + []; + + return Column( + children: [ + Padding( + padding: padding ?? EdgeInsets.zero, + child: Align( + alignment: Alignment.topLeft, + child: Text( + "exif_bottom_sheet_people", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + ), + ), + SizedBox( + height: imageSize, + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: CuratedPeopleRow( + padding: padding, + content: curatedPeople, + onTap: (content, index) { + context + .pushRoute( + PersonResultRoute( + personId: content.id, + personName: content.label, + ), + ) + .then((_) => peopleProvider.refresh()); + }, + onNameTap: (person, index) => { + showPersonNameEditModel(person.id, person.label), + }, + ), + ), + ), + ], + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/gallery_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/gallery_app_bar.dart new file mode 100644 index 000000000..a16f1f04d --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/gallery_app_bar.dart @@ -0,0 +1,110 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/album/providers/current_album.provider.dart'; +import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; +import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; +import 'package:immich_mobile/modules/home/ui/upload_dialog.dart'; +import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/providers/asset.provider.dart'; +import 'package:immich_mobile/shared/providers/user.provider.dart'; + +class GalleryAppBar extends ConsumerWidget { + final Asset asset; + final void Function() showInfo; + final void Function() onToggleMotionVideo; + final bool isPlayingVideo; + + const GalleryAppBar({ + super.key, + required this.asset, + required this.showInfo, + required this.onToggleMotionVideo, + required this.isPlayingVideo, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final album = ref.watch(currentAlbumProvider); + final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; + + final isPartner = ref + .watch(partnerSharedWithProvider) + .map((e) => e.isarId) + .contains(asset.ownerId); + + toggleFavorite(Asset asset) => + ref.read(assetProvider.notifier).toggleFavorite([asset]); + + handleActivities() { + if (album != null && album.shared && album.remoteId != null) { + context.pushRoute(const ActivitiesRoute()); + } + } + + handleUpload(Asset asset) { + showDialog( + context: context, + builder: (BuildContext _) { + return UploadDialog( + onUpload: () { + ref + .read(manualUploadProvider.notifier) + .uploadAssets(context, [asset]); + }, + ); + }, + ); + } + + addToAlbum(Asset addToAlbumAsset) { + showModalBottomSheet( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), + context: context, + builder: (BuildContext _) { + return AddToAlbumBottomSheet( + assets: [addToAlbumAsset], + ); + }, + ); + } + + return IgnorePointer( + ignoring: !ref.watch(showControlsProvider), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, + child: Container( + color: Colors.black.withOpacity(0.4), + child: TopControlAppBar( + isOwner: isOwner, + isPartner: isPartner, + isPlayingMotionVideo: isPlayingVideo, + asset: asset, + onMoreInfoPressed: showInfo, + onFavorite: toggleFavorite, + onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null, + onDownloadPressed: asset.isLocal + ? null + : () => + ref.read(imageViewerStateProvider.notifier).downloadAsset( + asset, + context, + ), + onToggleMotionVideo: onToggleMotionVideo, + onAddToAlbumPressed: () => addToAlbum(asset), + onActivitiesPressed: handleActivities, + ), + ), + ), + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/video_controls.dart b/mobile/lib/modules/asset_viewer/ui/video_controls.dart new file mode 100644 index 000000000..45a937209 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/video_controls.dart @@ -0,0 +1,125 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart'; + +/// The video controls for the [videPlayerControlsProvider] +class VideoControls extends ConsumerWidget { + const VideoControls({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final duration = + ref.watch(videoPlaybackValueProvider.select((v) => v.duration)); + final position = + ref.watch(videoPlaybackValueProvider.select((v) => v.position)); + + return AnimatedOpacity( + opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, + duration: const Duration(milliseconds: 100), + child: OrientationBuilder( + builder: (context, orientation) => Container( + padding: EdgeInsets.symmetric( + horizontal: orientation == Orientation.portrait ? 12.0 : 64.0, + ), + color: Colors.black.withOpacity(0.4), + child: Padding( + padding: MediaQuery.of(context).orientation == Orientation.portrait + ? const EdgeInsets.symmetric(horizontal: 12.0) + : const EdgeInsets.symmetric(horizontal: 64.0), + child: Row( + children: [ + Text( + _formatDuration(position), + style: TextStyle( + fontSize: 14.0, + color: Colors.white.withOpacity(.75), + fontWeight: FontWeight.normal, + ), + ), + Expanded( + child: Slider( + value: duration == Duration.zero + ? 0.0 + : min( + position.inMicroseconds / + duration.inMicroseconds * + 100, + 100, + ), + min: 0, + max: 100, + thumbColor: Colors.white, + activeColor: Colors.white, + inactiveColor: Colors.white.withOpacity(0.75), + onChanged: (position) { + ref.read(videoPlayerControlsProvider.notifier).position = + position; + }, + ), + ), + Text( + _formatDuration(duration), + style: TextStyle( + fontSize: 14.0, + color: Colors.white.withOpacity(.75), + fontWeight: FontWeight.normal, + ), + ), + IconButton( + icon: Icon( + ref.watch( + videoPlayerControlsProvider.select((value) => value.mute), + ) + ? Icons.volume_off + : Icons.volume_up, + ), + onPressed: () => ref + .read(videoPlayerControlsProvider.notifier) + .toggleMute(), + color: Colors.white, + ), + ], + ), + ), + ), + ), + ); + } + + String _formatDuration(Duration position) { + final ms = position.inMilliseconds; + + int seconds = ms ~/ 1000; + final int hours = seconds ~/ 3600; + seconds = seconds % 3600; + final minutes = seconds ~/ 60; + seconds = seconds % 60; + + final hoursString = hours >= 10 + ? '$hours' + : hours == 0 + ? '00' + : '0$hours'; + + final minutesString = minutes >= 10 + ? '$minutes' + : minutes == 0 + ? '00' + : '0$minutes'; + + final secondsString = seconds >= 10 + ? '$seconds' + : seconds == 0 + ? '00' + : '0$seconds'; + + final formattedTime = + '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; + + return formattedTime; + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/video_player.dart b/mobile/lib/modules/asset_viewer/ui/video_player.dart new file mode 100644 index 000000000..1f856e7d0 --- /dev/null +++ b/mobile/lib/modules/asset_viewer/ui/video_player.dart @@ -0,0 +1,45 @@ +import 'package:chewie/chewie.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/custom_video_player_controls.dart'; +import 'package:video_player/video_player.dart'; + +class VideoPlayerViewer extends HookConsumerWidget { + final VideoPlayerController controller; + final bool isMotionVideo; + final Widget? placeholder; + final Duration hideControlsTimer; + final bool showControls; + final bool showDownloadingIndicator; + + const VideoPlayerViewer({ + super.key, + required this.controller, + required this.isMotionVideo, + this.placeholder, + required this.hideControlsTimer, + required this.showControls, + required this.showDownloadingIndicator, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final chewie = useChewieController( + controller: controller, + controlsSafeAreaMinimum: const EdgeInsets.only( + bottom: 100, + ), + placeholder: SizedBox.expand(child: placeholder), + customControls: CustomVideoPlayerControls( + hideTimerDuration: hideControlsTimer, + ), + showControls: showControls && !isMotionVideo, + hideControlsTimer: hideControlsTimer, + ); + + return Chewie( + controller: chewie, + ); + } +} diff --git a/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart b/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart deleted file mode 100644 index bfc45b8a3..000000000 --- a/mobile/lib/modules/asset_viewer/ui/video_player_controls.dart +++ /dev/null @@ -1,209 +0,0 @@ -import 'dart:async'; - -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/center_play_button.dart'; -import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart'; -import 'package:video_player/video_player.dart'; - -class VideoPlayerControls extends ConsumerStatefulWidget { - const VideoPlayerControls({ - super.key, - }); - - @override - VideoPlayerControlsState createState() => VideoPlayerControlsState(); -} - -class VideoPlayerControlsState extends ConsumerState - with SingleTickerProviderStateMixin { - late VideoPlayerController controller; - late VideoPlayerValue _latestValue; - bool _displayBufferingIndicator = false; - double? _latestVolume; - Timer? _hideTimer; - - ChewieController? _chewieController; - ChewieController get chewieController => _chewieController!; - - @override - Widget build(BuildContext context) { - ref.listen(videoPlayerControlsProvider.select((value) => value.mute), - (_, value) { - _mute(value); - _cancelAndRestartTimer(); - }); - - ref.listen(videoPlayerControlsProvider.select((value) => value.position), - (_, position) { - _seekTo(position); - _cancelAndRestartTimer(); - }); - - if (_latestValue.hasError) { - return chewieController.errorBuilder?.call( - context, - chewieController.videoPlayerController.value.errorDescription!, - ) ?? - const Center( - child: Icon( - Icons.error, - color: Colors.white, - size: 42, - ), - ); - } - - return GestureDetector( - onTap: () => _cancelAndRestartTimer(), - child: AbsorbPointer( - absorbing: !ref.watch(showControlsProvider), - child: Stack( - children: [ - if (_displayBufferingIndicator) - const Center( - child: DelayedLoadingIndicator( - fadeInDuration: Duration(milliseconds: 400), - ), - ) - else - _buildHitArea(), - ], - ), - ), - ); - } - - @override - void dispose() { - _dispose(); - - super.dispose(); - } - - void _dispose() { - controller.removeListener(_updateState); - _hideTimer?.cancel(); - } - - @override - void didChangeDependencies() { - final oldController = _chewieController; - _chewieController = ChewieController.of(context); - controller = chewieController.videoPlayerController; - _latestValue = controller.value; - - if (oldController != chewieController) { - _dispose(); - _initialize(); - } - - super.didChangeDependencies(); - } - - Widget _buildHitArea() { - final bool isFinished = _latestValue.position >= _latestValue.duration; - - return GestureDetector( - onTap: () { - if (!_latestValue.isPlaying) { - _playPause(); - } - ref.read(showControlsProvider.notifier).show = false; - }, - child: CenterPlayButton( - backgroundColor: Colors.black54, - iconColor: Colors.white, - isFinished: isFinished, - isPlaying: controller.value.isPlaying, - show: ref.watch(showControlsProvider), - onPressed: _playPause, - ), - ); - } - - void _cancelAndRestartTimer() { - _hideTimer?.cancel(); - _startHideTimer(); - ref.read(showControlsProvider.notifier).show = true; - } - - Future _initialize() async { - ref.read(showControlsProvider.notifier).show = false; - _mute(ref.read(videoPlayerControlsProvider.select((value) => value.mute))); - - _latestValue = controller.value; - controller.addListener(_updateState); - - if (controller.value.isPlaying || chewieController.autoPlay) { - _startHideTimer(); - } - } - - void _playPause() { - final isFinished = _latestValue.position >= _latestValue.duration; - - setState(() { - if (controller.value.isPlaying) { - ref.read(showControlsProvider.notifier).show = true; - _hideTimer?.cancel(); - controller.pause(); - } else { - _cancelAndRestartTimer(); - - if (!controller.value.isInitialized) { - controller.initialize().then((_) { - controller.play(); - }); - } else { - if (isFinished) { - controller.seekTo(Duration.zero); - } - controller.play(); - } - } - }); - } - - void _startHideTimer() { - final hideControlsTimer = chewieController.hideControlsTimer; - _hideTimer?.cancel(); - _hideTimer = Timer(hideControlsTimer, () { - ref.read(showControlsProvider.notifier).show = false; - }); - } - - void _updateState() { - if (!mounted) return; - - _displayBufferingIndicator = controller.value.isBuffering; - - setState(() { - _latestValue = controller.value; - ref.read(videoPlaybackValueProvider.notifier).value = VideoPlaybackValue( - position: _latestValue.position, - duration: _latestValue.duration, - ); - }); - } - - void _mute(bool mute) { - if (mute) { - _latestVolume = controller.value.volume; - controller.setVolume(0); - } else { - controller.setVolume(_latestVolume ?? 0.5); - } - } - - void _seekTo(double position) { - final Duration pos = controller.value.duration * (position / 100.0); - if (pos != controller.value.position) { - controller.seekTo(pos); - } - } -} diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index dfdfb3284..059c0c976 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -2,46 +2,31 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; -import 'package:easy_localization/easy_localization.dart'; import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; -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/modules/album/providers/current_album.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; -import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart'; -import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/services/asset_stack.service.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/advanced_bottom_sheet.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/top_control_app_bar.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/bottom_gallery_bar.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/exif_sheet/exif_bottom_sheet.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/gallery_app_bar.dart'; import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart'; -import 'package:immich_mobile/modules/backup/providers/manual_upload.provider.dart'; -import 'package:immich_mobile/modules/home/ui/upload_dialog.dart'; -import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; -import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; import 'package:immich_mobile/shared/ui/immich_thumbnail.dart'; -import 'package:immich_mobile/shared/ui/immich_toast.dart'; import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_computed_scale.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/photo_view_scale_state.dart'; import 'package:immich_mobile/shared/ui/photo_view/src/utils/photo_view_hero_attributes.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:isar/isar.dart'; import 'package:openapi/api.dart' show ThumbnailFormat; @@ -73,18 +58,16 @@ class GalleryViewerPage extends HookConsumerWidget { final settings = ref.watch(appSettingsServiceProvider); final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); - final isZoomed = useState(false); - final isPlayingMotionVideo = useState(false); + final isZoomed = useState(false); final isPlayingVideo = useState(false); - Offset? localPosition; + final localPosition = useState(null); final currentIndex = useState(initialIndex); final currentAsset = loadAsset(currentIndex.value); - final isTrashEnabled = - ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); - final navStack = AutoRouter.of(context).stackData; - final isFromTrash = isTrashEnabled && - navStack.length > 2 && - navStack.elementAt(navStack.length - 2).name == TrashRoute.name; + // Update is playing motion video + ref.listen(videoPlaybackValueProvider.select((v) => v.state), (_, state) { + isPlayingVideo.value = state == VideoPlaybackState.playing; + }); + final stackIndex = useState(-1); final stack = showStack && currentAsset.stackChildrenCount > 0 ? ref.watch(assetStackStateProvider(currentAsset)) @@ -92,30 +75,23 @@ 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 album = ref.watch(currentAlbumProvider); - Asset asset() => stackIndex.value == -1 + Asset asset = stackIndex.value == -1 ? currentAsset : stackElements.elementAt(stackIndex.value); - final isOwner = asset().ownerId == ref.watch(currentUserProvider)?.isarId; - final isPartner = ref - .watch(partnerSharedWithProvider) - .map((e) => e.isarId) - .contains(asset().ownerId); - - bool isParent = stackIndex.value == -1 || stackIndex.value == 0; + final isMotionPhoto = asset.livePhotoVideoId != null; // Listen provider to prevent autoDispose when navigating to other routes from within the gallery page ref.listen(currentAssetProvider, (_, __) {}); useEffect( () { // Delay state update to after the execution of build method Future.microtask( - () => ref.read(currentAssetProvider.notifier).set(asset()), + () => ref.read(currentAssetProvider.notifier).set(asset), ); return null; }, - [asset()], + [asset], ); useEffect( @@ -124,15 +100,11 @@ class GalleryViewerPage extends HookConsumerWidget { settings.getSetting(AppSettingsEnum.loadPreview); isLoadOriginal.value = settings.getSetting(AppSettingsEnum.loadOriginal); - isPlayingMotionVideo.value = false; return null; }, [], ); - void toggleFavorite(Asset asset) => - ref.read(assetProvider.notifier).toggleFavorite([asset]); - Future precacheNextImage(int index) async { void onError(Object exception, StackTrace? stackTrace) { // swallow error silently @@ -161,125 +133,39 @@ class GalleryViewerPage extends HookConsumerWidget { context: context, useSafeArea: true, builder: (context) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.viewInsetsOf(context).bottom, + return FractionallySizedBox( + heightFactor: 0.75, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.advancedTroubleshooting) + ? AdvancedBottomSheet(assetDetail: asset) + : ExifBottomSheet(asset: asset), ), - child: ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.advancedTroubleshooting) - ? AdvancedBottomSheet(assetDetail: asset()) - : ExifBottomSheet(asset: asset()), - ); - }, - ); - } - - void removeAssetFromStack() { - if (stackIndex.value > 0 && showStack) { - ref - .read(assetStackStateProvider(currentAsset).notifier) - .removeChild(stackIndex.value - 1); - stackIndex.value = stackIndex.value - 1; - } - } - - void handleDelete(Asset deleteAsset) async { - // Cannot delete readOnly / external assets. They are handled through library offline jobs - if (asset().isReadOnly) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_delete_err_read_only'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - Future onDelete(bool force) async { - final isDeleted = await ref.read(assetProvider.notifier).deleteAssets( - {deleteAsset}, - force: force, - ); - if (isDeleted && isParent) { - if (totalAssets == 1) { - // Handle only one asset - context.popRoute(); - } else { - // Go to next page otherwise - controller.nextPage( - duration: const Duration(milliseconds: 100), - curve: Curves.fastLinearToSlowEaseIn, - ); - } - } - return isDeleted; - } - - // Asset is trashed - if (isTrashEnabled && !isFromTrash) { - final isDeleted = await onDelete(false); - if (isDeleted) { - // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && deleteAsset.isRemote && isParent) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'Asset trashed', - gravity: ToastGravity.BOTTOM, - ); - } - removeAssetFromStack(); - } - return; - } - - // Asset is permanently removed - showDialog( - context: context, - builder: (BuildContext _) { - return DeleteDialog( - onDelete: () async { - final isDeleted = await onDelete(true); - if (isDeleted) { - removeAssetFromStack(); - } - }, - ); - }, - ); - } - - void addToAlbum(Asset addToAlbumAsset) { - showModalBottomSheet( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15.0), - ), - context: context, - builder: (BuildContext _) { - return AddToAlbumBottomSheet( - assets: [addToAlbumAsset], ); }, ); } void handleSwipeUpDown(DragUpdateDetails details) { - int sensitivity = 15; - int dxThreshold = 50; - double ratioThreshold = 3.0; + const int sensitivity = 15; + const int dxThreshold = 50; + const double ratioThreshold = 3.0; if (isZoomed.value) { return; } // Guard [localPosition] null - if (localPosition == null) { + if (localPosition.value == null) { return; } // Check for delta from initial down point - final d = details.localPosition - localPosition!; + final d = details.localPosition - localPosition.value!; // If the magnitude of the dx swipe is large, we probably didn't mean to go down if (d.dx.abs() > dxThreshold) { return; @@ -293,413 +179,6 @@ class GalleryViewerPage extends HookConsumerWidget { } } - shareAsset() { - if (asset().isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - ref.read(imageViewerStateProvider.notifier).shareAsset(asset(), context); - } - - handleArchive(Asset asset) { - ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isParent) { - context.popRoute(); - return; - } - removeAssetFromStack(); - } - - handleUpload(Asset asset) { - showDialog( - context: context, - builder: (BuildContext _) { - return UploadDialog( - onUpload: () { - ref - .read(manualUploadProvider.notifier) - .uploadAssets(context, [asset]); - }, - ); - }, - ); - } - - handleDownload() { - if (asset().isLocal) { - return; - } - if (asset().isOffline) { - ImmichToast.show( - durationInSecond: 1, - context: context, - msg: 'asset_action_share_err_offline'.tr(), - gravity: ToastGravity.BOTTOM, - ); - return; - } - - ref.read(imageViewerStateProvider.notifier).downloadAsset( - asset(), - context, - ); - } - - handleActivities() { - if (album != null && album.shared && album.remoteId != null) { - context.pushRoute(const ActivitiesRoute()); - } - } - - buildAppBar() { - return IgnorePointer( - ignoring: !ref.watch(showControlsProvider), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, - child: Container( - color: Colors.black.withOpacity(0.4), - child: TopControlAppBar( - isOwner: isOwner, - isPartner: isPartner, - isPlayingMotionVideo: isPlayingMotionVideo.value, - asset: asset(), - onMoreInfoPressed: showInfo, - onFavorite: toggleFavorite, - onUploadPressed: - asset().isLocal ? () => handleUpload(asset()) : null, - onDownloadPressed: asset().isLocal - ? null - : () => - ref.read(imageViewerStateProvider.notifier).downloadAsset( - asset(), - context, - ), - onToggleMotionVideo: (() { - isPlayingMotionVideo.value = !isPlayingMotionVideo.value; - }), - onAddToAlbumPressed: () => addToAlbum(asset()), - onActivitiesPressed: handleActivities, - ), - ), - ), - ); - } - - Widget buildProgressBar() { - final playerValue = ref.watch(videoPlaybackValueProvider); - - return Expanded( - child: Slider( - value: playerValue.duration == Duration.zero - ? 0.0 - : min( - playerValue.position.inMicroseconds / - playerValue.duration.inMicroseconds * - 100, - 100, - ), - min: 0, - max: 100, - thumbColor: Colors.white, - activeColor: Colors.white, - inactiveColor: Colors.white.withOpacity(0.75), - onChanged: (position) { - ref.read(videoPlayerControlsProvider.notifier).position = position; - }, - ), - ); - } - - Text buildPosition() { - final position = ref - .watch(videoPlaybackValueProvider.select((value) => value.position)); - - return Text( - _formatDuration(position), - style: TextStyle( - fontSize: 14.0, - color: Colors.white.withOpacity(.75), - fontWeight: FontWeight.normal, - ), - ); - } - - Text buildDuration() { - final duration = ref - .watch(videoPlaybackValueProvider.select((value) => value.duration)); - - return Text( - _formatDuration(duration), - style: TextStyle( - fontSize: 14.0, - color: Colors.white.withOpacity(.75), - fontWeight: FontWeight.normal, - ), - ); - } - - Widget buildMuteButton() { - return IconButton( - icon: Icon( - ref.watch(videoPlayerControlsProvider.select((value) => value.mute)) - ? Icons.volume_off - : Icons.volume_up, - ), - onPressed: () => - ref.read(videoPlayerControlsProvider.notifier).toggleMute(), - color: Colors.white, - ); - } - - Widget buildStackedChildren() { - return ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.horizontal, - itemCount: stackElements.length, - itemBuilder: (context, index) { - final assetId = stackElements.elementAt(index).remoteId; - return Padding( - padding: const EdgeInsets.only(right: 10), - child: GestureDetector( - onTap: () => stackIndex.value = index, - child: Container( - width: 40, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(6), - border: (stackIndex.value == -1 && index == 0) || - index == stackIndex.value - ? Border.all( - color: Colors.white, - width: 2, - ) - : null, - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image( - fit: BoxFit.cover, - image: ImmichRemoteImageProvider(assetId: assetId!), - ), - ), - ), - ), - ); - }, - ); - } - - void showStackActionItems() { - showModalBottomSheet( - context: context, - enableDrag: false, - builder: (BuildContext ctx) { - return SafeArea( - child: Padding( - padding: const EdgeInsets.only(top: 24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (!isParent) - ListTile( - leading: const Icon( - Icons.bookmark_border_outlined, - size: 24, - ), - onTap: () async { - await ref - .read(assetStackServiceProvider) - .updateStackParent( - currentAsset, - stackElements.elementAt(stackIndex.value), - ); - ctx.pop(); - context.popRoute(); - }, - title: const Text( - "viewer_stack_use_as_main_asset", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ListTile( - leading: const Icon( - Icons.copy_all_outlined, - size: 24, - ), - onTap: () async { - if (isParent) { - await ref - .read(assetStackServiceProvider) - .updateStackParent( - currentAsset, - stackElements - .elementAt(1), // Next asset as parent - ); - // Remove itself from stack - await ref.read(assetStackServiceProvider).updateStack( - stackElements.elementAt(1), - childrenToRemove: [currentAsset], - ); - ctx.pop(); - context.popRoute(); - } else { - await ref.read(assetStackServiceProvider).updateStack( - currentAsset, - childrenToRemove: [ - stackElements.elementAt(stackIndex.value), - ], - ); - removeAssetFromStack(); - ctx.pop(); - } - }, - title: const Text( - "viewer_remove_from_stack", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ListTile( - leading: const Icon( - Icons.filter_none_outlined, - size: 18, - ), - onTap: () async { - await ref.read(assetStackServiceProvider).updateStack( - currentAsset, - childrenToRemove: stack, - ); - ctx.pop(); - context.popRoute(); - }, - title: const Text( - "viewer_unstack", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ], - ), - ), - ); - }, - ); - } - - // TODO: Migrate to a custom bottom bar and handle long press to delete - Widget buildBottomBar() { - // !!!! itemsList and actionlist should always be in sync - final itemsList = [ - BottomNavigationBarItem( - icon: Icon( - Platform.isAndroid ? Icons.share_rounded : Icons.ios_share_rounded, - ), - label: 'control_bottom_app_bar_share'.tr(), - tooltip: 'control_bottom_app_bar_share'.tr(), - ), - if (isOwner) - asset().isArchived - ? BottomNavigationBarItem( - icon: const Icon(Icons.unarchive_rounded), - label: 'control_bottom_app_bar_unarchive'.tr(), - tooltip: 'control_bottom_app_bar_unarchive'.tr(), - ) - : BottomNavigationBarItem( - icon: const Icon(Icons.archive_outlined), - label: 'control_bottom_app_bar_archive'.tr(), - tooltip: 'control_bottom_app_bar_archive'.tr(), - ), - if (isOwner && stack.isNotEmpty) - BottomNavigationBarItem( - icon: const Icon(Icons.burst_mode_outlined), - label: 'control_bottom_app_bar_stack'.tr(), - tooltip: 'control_bottom_app_bar_stack'.tr(), - ), - if (isOwner) - BottomNavigationBarItem( - icon: const Icon(Icons.delete_outline), - label: 'control_bottom_app_bar_delete'.tr(), - tooltip: 'control_bottom_app_bar_delete'.tr(), - ), - if (!isOwner) - BottomNavigationBarItem( - icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), - ), - ]; - - List actionslist = [ - (_) => shareAsset(), - if (isOwner) (_) => handleArchive(asset()), - if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(), - if (isOwner) (_) => handleDelete(asset()), - if (!isOwner) (_) => handleDownload(), - ]; - - return IgnorePointer( - ignoring: !ref.watch(showControlsProvider), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 100), - opacity: ref.watch(showControlsProvider) ? 1.0 : 0.0, - child: Column( - children: [ - if (stack.isNotEmpty) - Padding( - padding: const EdgeInsets.only( - left: 10, - bottom: 30, - ), - child: SizedBox( - height: 40, - child: buildStackedChildren(), - ), - ), - Visibility( - visible: !asset().isImage && !isPlayingMotionVideo.value, - child: Container( - color: Colors.black.withOpacity(0.4), - child: Padding( - padding: MediaQuery.of(context).orientation == - Orientation.portrait - ? const EdgeInsets.symmetric(horizontal: 12.0) - : const EdgeInsets.symmetric(horizontal: 64.0), - child: Row( - children: [ - buildPosition(), - buildProgressBar(), - buildDuration(), - buildMuteButton(), - ], - ), - ), - ), - ), - BottomNavigationBar( - backgroundColor: Colors.black.withOpacity(0.4), - unselectedIconTheme: const IconThemeData(color: Colors.white), - selectedIconTheme: const IconThemeData(color: Colors.white), - unselectedLabelStyle: const TextStyle(color: Colors.black), - selectedLabelStyle: const TextStyle(color: Colors.black), - showSelectedLabels: false, - showUnselectedLabels: false, - items: itemsList, - onTap: (index) { - if (index < actionslist.length) { - actionslist[index].call(index); - } - }, - ), - ], - ), - ), - ); - } - useEffect( () { if (ref.read(showControlsProvider)) { @@ -707,6 +186,7 @@ class GalleryViewerPage extends HookConsumerWidget { } else { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); } + isPlayingVideo.value = false; return null; }, [], @@ -735,6 +215,50 @@ class GalleryViewerPage extends HookConsumerWidget { } }); + Widget buildStackedChildren() { + return ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemCount: stackElements.length, + padding: const EdgeInsets.only( + left: 5, + right: 5, + bottom: 30, + ), + itemBuilder: (context, index) { + final assetId = stackElements.elementAt(index).remoteId; + return Padding( + padding: const EdgeInsets.only(right: 5), + child: GestureDetector( + onTap: () => stackIndex.value = index, + child: Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(6), + border: (stackIndex.value == -1 && index == 0) || + index == stackIndex.value + ? Border.all( + color: Colors.white, + width: 2, + ) + : null, + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: Image( + fit: BoxFit.cover, + image: ImmichRemoteImageProvider(assetId: assetId!), + ), + ), + ), + ), + ); + }, + ); + } + return PopScope( canPop: false, onPopInvoked: (_) { @@ -762,7 +286,7 @@ class GalleryViewerPage extends HookConsumerWidget { ), ), ImmichThumbnail( - asset: asset(), + asset: asset, fit: BoxFit.contain, ), ], @@ -782,6 +306,7 @@ class GalleryViewerPage extends HookConsumerWidget { HapticFeedback.selectionClick(); currentIndex.value = value; stackIndex.value = -1; + isPlayingVideo.value = false; // Wait for page change animation to finish await Future.delayed(const Duration(milliseconds: 400)); @@ -790,14 +315,14 @@ class GalleryViewerPage extends HookConsumerWidget { }, builder: (context, index) { final a = - index == currentIndex.value ? asset() : loadAsset(index); + index == currentIndex.value ? asset : loadAsset(index); final ImageProvider provider = ImmichImage.imageProvider(asset: a); - if (a.isImage && !isPlayingMotionVideo.value) { + if (a.isImage && !isPlayingVideo.value) { return PhotoViewGalleryPageOptions( onDragStart: (_, details, __) => - localPosition = details.localPosition, + localPosition.value = details.localPosition, onDragUpdate: (_, details, __) => handleSwipeUpDown(details), onTapDown: (_, __, ___) { @@ -821,7 +346,7 @@ class GalleryViewerPage extends HookConsumerWidget { } else { return PhotoViewGalleryPageOptions.customChild( onDragStart: (_, details, __) => - localPosition = details.localPosition, + localPosition.value = details.localPosition, onDragUpdate: (_, details, __) => handleSwipeUpDown(details), heroAttributes: PhotoViewHeroAttributes( @@ -834,15 +359,9 @@ class GalleryViewerPage extends HookConsumerWidget { minScale: 1.0, basePosition: Alignment.center, child: VideoViewerPage( - onPlaying: () { - isPlayingVideo.value = true; - }, - onPaused: () => - WidgetsBinding.instance.addPostFrameCallback( - (_) => isPlayingVideo.value = false, - ), + key: ValueKey(a), asset: a, - isMotionVideo: isPlayingMotionVideo.value, + isMotionVideo: a.livePhotoVideoId != null, placeholder: Image( image: provider, fit: BoxFit.contain, @@ -850,11 +369,6 @@ class GalleryViewerPage extends HookConsumerWidget { width: context.width, alignment: Alignment.center, ), - onVideoEnded: () { - if (isPlayingMotionVideo.value) { - isPlayingMotionVideo.value = false; - } - }, ), ); } @@ -864,50 +378,41 @@ class GalleryViewerPage extends HookConsumerWidget { top: 0, left: 0, right: 0, - child: buildAppBar(), + child: GalleryAppBar( + asset: asset, + showInfo: showInfo, + isPlayingVideo: isPlayingVideo.value, + onToggleMotionVideo: () => + isPlayingVideo.value = !isPlayingVideo.value, + ), ), Positioned( bottom: 0, left: 0, right: 0, - child: buildBottomBar(), + child: Column( + children: [ + Visibility( + visible: stack.isNotEmpty, + child: SizedBox( + height: 80, + child: buildStackedChildren(), + ), + ), + BottomGalleryBar( + totalAssets: totalAssets, + controller: controller, + showStack: showStack, + stackIndex: stackIndex.value, + asset: asset, + showVideoPlayerControls: !asset.isImage && !isMotionPhoto, + ), + ], + ), ), ], ), ), ); } - - String _formatDuration(Duration position) { - final ms = position.inMilliseconds; - - int seconds = ms ~/ 1000; - final int hours = seconds ~/ 3600; - seconds = seconds % 3600; - final minutes = seconds ~/ 60; - seconds = seconds % 60; - - final hoursString = hours >= 10 - ? '$hours' - : hours == 0 - ? '00' - : '0$hours'; - - final minutesString = minutes >= 10 - ? '$minutes' - : minutes == 0 - ? '00' - : '0$minutes'; - - final secondsString = seconds >= 10 - ? '$seconds' - : seconds == 0 - ? '00' - : '0$seconds'; - - final formattedTime = - '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; - - return formattedTime; - } } diff --git a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart index 0da2bc52d..22f00c001 100644 --- a/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/video_viewer_page.dart @@ -1,21 +1,22 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:chewie/chewie.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:immich_mobile/modules/asset_viewer/hooks/chewiew_controller_hook.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/video_player_controls.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controller_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/providers/video_player_value_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/ui/video_player.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/ui/delayed_loading_indicator.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() // ignore: must_be_immutable -class VideoViewerPage extends HookWidget { +class VideoViewerPage extends HookConsumerWidget { final Asset asset; final bool isMotionVideo; final Widget? placeholder; - final VoidCallback? onVideoEnded; - final VoidCallback? onPlaying; - final VoidCallback? onPaused; final Duration hideControlsTimer; final bool showControls; final bool showDownloadingIndicator; @@ -24,9 +25,6 @@ class VideoViewerPage extends HookWidget { super.key, required this.asset, this.isMotionVideo = false, - this.onVideoEnded, - this.onPlaying, - this.onPaused, this.placeholder, this.showControls = true, this.hideControlsTimer = const Duration(seconds: 5), @@ -34,29 +32,107 @@ class VideoViewerPage extends HookWidget { }); @override - Widget build(BuildContext context) { - final controller = useChewieController( - asset, - controlsSafeAreaMinimum: const EdgeInsets.only( - bottom: 100, - ), - placeholder: placeholder, - showControls: showControls && !isMotionVideo, - hideControlsTimer: hideControlsTimer, - customControls: const VideoPlayerControls(), - onPlaying: onPlaying, - onPaused: onPaused, - onVideoEnded: onVideoEnded, + build(BuildContext context, WidgetRef ref) { + final controller = + ref.watch(videoPlayerControllerProvider(asset: asset)).value; + // The last volume of the video used when mute is toggled + final lastVolume = useState(0.5); + + // When the volume changes, set the volume + ref.listen(videoPlayerControlsProvider.select((value) => value.mute), + (_, mute) { + if (mute) { + controller?.setVolume(0.0); + } else { + controller?.setVolume(lastVolume.value); + } + }); + + // When the position changes, seek to the position + ref.listen(videoPlayerControlsProvider.select((value) => value.position), + (_, position) { + if (controller == null) { + // No seeeking if there is no video + return; + } + + // Find the position to seek to + final Duration seek = controller.value.duration * (position / 100.0); + controller.seekTo(seek); + }); + + // When the custom video controls paus or plays + ref.listen(videoPlayerControlsProvider.select((value) => value.pause), + (lastPause, pause) { + if (pause) { + controller?.pause(); + } else { + controller?.play(); + } + }); + + // Updates the [videoPlaybackValueProvider] with the current + // position and duration of the video from the Chewie [controller] + // Also sets the error if there is an error in the playback + void updateVideoPlayback() { + final videoPlayback = VideoPlaybackValue.fromController(controller); + ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; + final state = videoPlayback.state; + + // Enable the WakeLock while the video is playing + if (state == VideoPlaybackState.playing) { + // Sync with the controls playing + WakelockPlus.enable(); + } else { + // Sync with the controls pause + WakelockPlus.disable(); + } + } + + // Adds and removes the listener to the video player + useEffect( + () { + Future.microtask( + () => ref.read(videoPlayerControlsProvider.notifier).reset(), + ); + // Guard no controller + if (controller == null) { + return null; + } + + // Hide the controls + // Done in a microtask to avoid setting the state while the is building + if (!isMotionVideo) { + Future.microtask(() { + ref.read(showControlsProvider.notifier).show = false; + }); + } + + // Subscribes to listener + controller.addListener(updateVideoPlayback); + return () { + // Removes listener when we dispose + controller.removeListener(updateVideoPlayback); + controller.pause(); + }; + }, + [controller], ); - // Loading + final size = MediaQuery.sizeOf(context); + return PopScope( + onPopInvoked: (pop) { + ref.read(videoPlaybackValueProvider.notifier).value = + VideoPlaybackValue.uninitialized(); + }, child: AnimatedSwitcher( duration: const Duration(milliseconds: 400), - child: Builder( - builder: (context) { - if (controller == null) { - return Stack( + child: Stack( + children: [ + Visibility( + visible: controller == null, + child: Stack( children: [ if (placeholder != null) placeholder!, const Positioned.fill( @@ -67,18 +143,22 @@ class VideoViewerPage extends HookWidget { ), ), ], - ); - } - - final size = MediaQuery.of(context).size; - return SizedBox( - height: size.height, - width: size.width, - child: Chewie( - controller: controller, ), - ); - }, + ), + if (controller != null) + SizedBox( + height: size.height, + width: size.width, + child: VideoPlayerViewer( + controller: controller, + isMotionVideo: isMotionVideo, + placeholder: placeholder, + hideControlsTimer: hideControlsTimer, + showControls: showControls, + showDownloadingIndicator: showDownloadingIndicator, + ), + ), + ], ), ), ); diff --git a/mobile/lib/modules/map/providers/map_state.provider.g.dart b/mobile/lib/modules/map/providers/map_state.provider.g.dart index ca75292e7..d1b3e54b7 100644 --- a/mobile/lib/modules/map/providers/map_state.provider.g.dart +++ b/mobile/lib/modules/map/providers/map_state.provider.g.dart @@ -6,7 +6,7 @@ part of 'map_state.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$mapStateNotifierHash() => r'3b509b57b7400b09817e9caee9debf899172cd52'; +String _$mapStateNotifierHash() => r'6408d616ec9fc0d1ff26e25692417c43504ff754'; /// See also [MapStateNotifier]. @ProviderFor(MapStateNotifier) diff --git a/mobile/lib/modules/memories/ui/memory_card.dart b/mobile/lib/modules/memories/ui/memory_card.dart index af57c272a..5a316db27 100644 --- a/mobile/lib/modules/memories/ui/memory_card.dart +++ b/mobile/lib/modules/memories/ui/memory_card.dart @@ -69,14 +69,16 @@ class MemoryCard extends StatelessWidget { return Hero( tag: 'memory-${asset.id}', child: VideoViewerPage( + key: ValueKey(asset), asset: asset, showDownloadingIndicator: false, - placeholder: ImmichImage( - asset, - fit: fit, + placeholder: SizedBox.expand( + child: ImmichImage( + asset, + fit: fit, + ), ), hideControlsTimer: const Duration(seconds: 2), - onVideoEnded: onVideoEnded, showControls: false, ), ); diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart index aa3403f2a..78dc1af4f 100644 --- a/mobile/lib/modules/search/ui/curated_people_row.dart +++ b/mobile/lib/modules/search/ui/curated_people_row.dart @@ -7,6 +7,7 @@ import 'package:immich_mobile/utils/image_url_builder.dart'; class CuratedPeopleRow extends StatelessWidget { final List content; + final EdgeInsets? padding; /// Callback with the content and the index when tapped final Function(CuratedContent, int)? onTap; @@ -16,6 +17,7 @@ class CuratedPeopleRow extends StatelessWidget { super.key, required this.content, this.onTap, + this.padding, required this.onNameTap, }); @@ -43,11 +45,8 @@ class CuratedPeopleRow extends StatelessWidget { } return ListView.builder( + padding: padding, scrollDirection: Axis.horizontal, - padding: const EdgeInsets.only( - left: 16, - top: 8, - ), itemBuilder: (context, index) { final person = content[index]; final headers = { diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index d6c556ef6..ab114d691 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -78,19 +78,25 @@ class SearchPage extends HookConsumerWidget { height: imageSize, child: curatedPeople.widgetWhen( onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), - onData: (people) => CuratedPeopleRow( - content: people.take(12).toList(), - onTap: (content, index) { - context.pushRoute( - PersonResultRoute( - personId: content.id, - personName: content.label, - ), - ); - }, - onNameTap: (person, index) => { - showNameEditModel(person.id, person.label), - }, + onData: (people) => Padding( + padding: const EdgeInsets.only( + left: 16, + top: 8, + ), + child: CuratedPeopleRow( + content: people.take(12).toList(), + onTap: (content, index) { + context.pushRoute( + PersonResultRoute( + personId: content.id, + personName: content.label, + ), + ); + }, + onNameTap: (person, index) => { + showNameEditModel(person.id, person.label), + }, + ), ), ), ); diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 16ac5efb0..64bd492a7 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -350,9 +350,6 @@ abstract class _$AppRouter extends RootStackRouter { key: args.key, asset: args.asset, isMotionVideo: args.isMotionVideo, - onVideoEnded: args.onVideoEnded, - onPlaying: args.onPlaying, - onPaused: args.onPaused, placeholder: args.placeholder, showControls: args.showControls, hideControlsTimer: args.hideControlsTimer, @@ -1388,12 +1385,9 @@ class VideoViewerRoute extends PageRouteInfo { Key? key, required Asset asset, bool isMotionVideo = false, - void Function()? onVideoEnded, - void Function()? onPlaying, - void Function()? onPaused, Widget? placeholder, bool showControls = true, - Duration hideControlsTimer = const Duration(milliseconds: 1500), + Duration hideControlsTimer = const Duration(seconds: 5), bool showDownloadingIndicator = true, List? children, }) : super( @@ -1402,9 +1396,6 @@ class VideoViewerRoute extends PageRouteInfo { key: key, asset: asset, isMotionVideo: isMotionVideo, - onVideoEnded: onVideoEnded, - onPlaying: onPlaying, - onPaused: onPaused, placeholder: placeholder, showControls: showControls, hideControlsTimer: hideControlsTimer, @@ -1424,12 +1415,9 @@ class VideoViewerRouteArgs { this.key, required this.asset, this.isMotionVideo = false, - this.onVideoEnded, - this.onPlaying, - this.onPaused, this.placeholder, this.showControls = true, - this.hideControlsTimer = const Duration(milliseconds: 1500), + this.hideControlsTimer = const Duration(seconds: 5), this.showDownloadingIndicator = true, }); @@ -1439,12 +1427,6 @@ class VideoViewerRouteArgs { final bool isMotionVideo; - final void Function()? onVideoEnded; - - final void Function()? onPlaying; - - final void Function()? onPaused; - final Widget? placeholder; final bool showControls; @@ -1455,6 +1437,6 @@ class VideoViewerRouteArgs { @override String toString() { - return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, onVideoEnded: $onVideoEnded, onPlaying: $onPlaying, onPaused: $onPaused, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer, showDownloadingIndicator: $showDownloadingIndicator}'; + return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer, showDownloadingIndicator: $showDownloadingIndicator}'; } } diff --git a/mobile/lib/shared/cache/custom_image_cache.dart b/mobile/lib/shared/cache/custom_image_cache.dart index 79338cbda..3f8b35e3f 100644 --- a/mobile/lib/shared/cache/custom_image_cache.dart +++ b/mobile/lib/shared/cache/custom_image_cache.dart @@ -1,5 +1,6 @@ import 'package:flutter/painting.dart'; import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_local_image_provider.dart'; +import 'package:immich_mobile/modules/asset_viewer/image_providers/immich_remote_image_provider.dart'; /// [ImageCache] that uses two caches for small and large images /// so that a single large image does not evict all small iamges @@ -31,9 +32,18 @@ final class CustomImageCache implements ImageCache { _large.clearLiveImages(); } + /// Gets the cache for the given key + ImageCache _cacheForKey(Object key) => + (key is ImmichLocalImageProvider || key is ImmichRemoteImageProvider) + ? _large + : _small; + @override - bool containsKey(Object key) => - (key is ImmichLocalImageProvider ? _large : _small).containsKey(key); + bool containsKey(Object key) { + // [ImmichLocalImageProvider] and [ImmichRemoteImageProvider] are both + // large size images while the other thumbnail providers are small + return _cacheForKey(key).containsKey(key); + } @override int get currentSize => _small.currentSize + _large.currentSize; @@ -43,8 +53,7 @@ final class CustomImageCache implements ImageCache { @override bool evict(Object key, {bool includeLive = true}) => - (key is ImmichLocalImageProvider ? _large : _small) - .evict(key, includeLive: includeLive); + _cacheForKey(key).evict(key, includeLive: includeLive); @override int get liveImageCount => _small.liveImageCount + _large.liveImageCount; @@ -59,10 +68,9 @@ final class CustomImageCache implements ImageCache { ImageStreamCompleter Function() loader, { ImageErrorListener? onError, }) => - (key is ImmichLocalImageProvider ? _large : _small) - .putIfAbsent(key, loader, onError: onError); + _cacheForKey(key).putIfAbsent(key, loader, onError: onError); @override ImageCacheStatus statusForKey(Object key) => - (key is ImmichLocalImageProvider ? _large : _small).statusForKey(key); + _cacheForKey(key).statusForKey(key); } diff --git a/mobile/lib/shared/models/asset.dart b/mobile/lib/shared/models/asset.dart index 3c3c4df82..ea49d0202 100644 --- a/mobile/lib/shared/models/asset.dart +++ b/mobile/lib/shared/models/asset.dart @@ -22,7 +22,7 @@ class Asset { updatedAt = remote.updatedAt, durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0, type = remote.type.toAssetType(), - fileName = p.basename(remote.originalPath), + fileName = remote.originalFileName, height = remote.exifInfo?.exifImageHeight?.toInt(), width = remote.exifInfo?.exifImageWidth?.toInt(), livePhotoVideoId = remote.livePhotoVideoId, diff --git a/mobile/lib/shared/models/exif_info.dart b/mobile/lib/shared/models/exif_info.dart index a61fd2c28..f2bd02375 100644 --- a/mobile/lib/shared/models/exif_info.dart +++ b/mobile/lib/shared/models/exif_info.dart @@ -24,6 +24,10 @@ class ExifInfo { String? country; String? description; + @ignore + bool get hasCoordinates => + latitude != null && longitude != null && latitude != 0 && longitude != 0; + @ignore String get exposureTime { if (exposureSeconds == null) { diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index ad163d5cd..bbe3cefc0 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -329,7 +329,9 @@ final assetDetailProvider = yield await ref.watch(assetServiceProvider).loadExif(asset); final db = ref.watch(dbProvider); await for (final a in db.assets.watchObject(asset.id)) { - if (a != null) yield await ref.watch(assetServiceProvider).loadExif(a); + if (a != null) { + yield await ref.watch(assetServiceProvider).loadExif(a); + } } }); diff --git a/mobile/lib/shared/services/asset.service.dart b/mobile/lib/shared/services/asset.service.dart index 3086ab924..a9a65d263 100644 --- a/mobile/lib/shared/services/asset.service.dart +++ b/mobile/lib/shared/services/asset.service.dart @@ -61,6 +61,27 @@ class AssetService { return (assetDto.map(Asset.remote).toList(), deleted.ids); } + /// Returns the list of people of the given asset id. + // If the server is not reachable `null` is returned. + Future?> getRemotePeopleOfAsset( + String remoteId, + ) async { + try { + final AssetResponseDto? dto = + await _apiService.assetApi.getAssetInfo(remoteId); + + return dto?.people; + } catch (error, stack) { + log.severe( + 'Error while getting remote asset info: ${error.toString()}', + error, + stack, + ); + + return null; + } + } + /// Returns `null` if the server state did not change, else list of assets Future?> _getRemoteAssets(User user) async { const int chunkSize = 10000; diff --git a/mobile/lib/shared/ui/hooks/timer_hook.dart b/mobile/lib/shared/ui/hooks/timer_hook.dart new file mode 100644 index 000000000..a78fed42c --- /dev/null +++ b/mobile/lib/shared/ui/hooks/timer_hook.dart @@ -0,0 +1,48 @@ +import 'package:async/async.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +RestartableTimer useTimer( + Duration duration, + void Function() callback, +) { + return use( + _TimerHook( + duration: duration, + callback: callback, + ), + ); +} + +class _TimerHook extends Hook { + final Duration duration; + final void Function() callback; + + const _TimerHook({ + required this.duration, + required this.callback, + }); + @override + HookState> createState() => + _TimerHookState(); +} + +class _TimerHookState extends HookState { + late RestartableTimer timer; + @override + void initHook() { + super.initHook(); + timer = RestartableTimer(hook.duration, hook.callback); + } + + @override + RestartableTimer build(BuildContext context) { + return timer; + } + + @override + void dispose() { + timer.cancel(); + super.dispose(); + } +} diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index ea413b487..bdd8e1d4b 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -104,6 +104,7 @@ doc/PeopleResponseDto.md doc/PeopleUpdateDto.md doc/PeopleUpdateItem.md doc/PersonApi.md +doc/PersonCreateDto.md doc/PersonResponseDto.md doc/PersonStatisticsResponseDto.md doc/PersonUpdateDto.md @@ -160,6 +161,7 @@ doc/SystemConfigTemplateStorageOptionDto.md doc/SystemConfigThemeDto.md doc/SystemConfigThumbnailDto.md doc/SystemConfigTrashDto.md +doc/SystemConfigUserDto.md doc/TagApi.md doc/TagResponseDto.md doc/TagTypeEnum.md @@ -305,6 +307,7 @@ lib/model/path_type.dart lib/model/people_response_dto.dart lib/model/people_update_dto.dart lib/model/people_update_item.dart +lib/model/person_create_dto.dart lib/model/person_response_dto.dart lib/model/person_statistics_response_dto.dart lib/model/person_update_dto.dart @@ -357,6 +360,7 @@ lib/model/system_config_template_storage_option_dto.dart lib/model/system_config_theme_dto.dart lib/model/system_config_thumbnail_dto.dart lib/model/system_config_trash_dto.dart +lib/model/system_config_user_dto.dart lib/model/tag_response_dto.dart lib/model/tag_type_enum.dart lib/model/thumbnail_format.dart @@ -483,6 +487,7 @@ test/people_response_dto_test.dart test/people_update_dto_test.dart test/people_update_item_test.dart test/person_api_test.dart +test/person_create_dto_test.dart test/person_response_dto_test.dart test/person_statistics_response_dto_test.dart test/person_update_dto_test.dart @@ -539,6 +544,7 @@ test/system_config_template_storage_option_dto_test.dart test/system_config_theme_dto_test.dart test/system_config_thumbnail_dto_test.dart test/system_config_trash_dto_test.dart +test/system_config_user_dto_test.dart test/tag_api_test.dart test/tag_response_dto_test.dart test/tag_type_enum_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b8548c79e..536c671b8 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.97.0 +- API version: 1.98.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements @@ -303,6 +303,7 @@ Class | Method | HTTP request | Description - [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleUpdateDto](doc//PeopleUpdateDto.md) - [PeopleUpdateItem](doc//PeopleUpdateItem.md) + - [PersonCreateDto](doc//PersonCreateDto.md) - [PersonResponseDto](doc//PersonResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) - [PersonUpdateDto](doc//PersonUpdateDto.md) @@ -355,6 +356,7 @@ Class | Method | HTTP request | Description - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) + - [SystemConfigUserDto](doc//SystemConfigUserDto.md) - [TagResponseDto](doc//TagResponseDto.md) - [TagTypeEnum](doc//TagTypeEnum.md) - [ThumbnailFormat](doc//ThumbnailFormat.md) diff --git a/mobile/openapi/doc/CreateUserDto.md b/mobile/openapi/doc/CreateUserDto.md index 0dcc8eca1..743adb3a1 100644 --- a/mobile/openapi/doc/CreateUserDto.md +++ b/mobile/openapi/doc/CreateUserDto.md @@ -13,6 +13,7 @@ Name | Type | Description | Notes **name** | **String** | | **password** | **String** | | **quotaSizeInBytes** | **int** | | [optional] +**shouldChangePassword** | **bool** | | [optional] **storageLabel** | **String** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/MapMarkerResponseDto.md b/mobile/openapi/doc/MapMarkerResponseDto.md index 94f253d38..81d8224db 100644 --- a/mobile/openapi/doc/MapMarkerResponseDto.md +++ b/mobile/openapi/doc/MapMarkerResponseDto.md @@ -8,9 +8,12 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**city** | **String** | | +**country** | **String** | | **id** | **String** | | **lat** | **double** | | **lon** | **double** | | +**state** | **String** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/PersonApi.md b/mobile/openapi/doc/PersonApi.md index f9e310018..48c1c3cc4 100644 --- a/mobile/openapi/doc/PersonApi.md +++ b/mobile/openapi/doc/PersonApi.md @@ -22,7 +22,7 @@ Method | HTTP request | Description # **createPerson** -> PersonResponseDto createPerson() +> PersonResponseDto createPerson(personCreateDto) @@ -45,9 +45,10 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = PersonApi(); +final personCreateDto = PersonCreateDto(); // PersonCreateDto | try { - final result = api_instance.createPerson(); + final result = api_instance.createPerson(personCreateDto); print(result); } catch (e) { print('Exception when calling PersonApi->createPerson: $e\n'); @@ -55,7 +56,10 @@ try { ``` ### Parameters -This endpoint does not need any parameter. + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **personCreateDto** | [**PersonCreateDto**](PersonCreateDto.md)| | ### Return type @@ -67,7 +71,7 @@ This endpoint does not need any parameter. ### HTTP request headers - - **Content-Type**: Not defined + - **Content-Type**: application/json - **Accept**: application/json [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) @@ -110,7 +114,7 @@ try { Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - **withHidden** | **bool**| | [optional] [default to false] + **withHidden** | **bool**| | [optional] ### Return type diff --git a/mobile/openapi/doc/PersonCreateDto.md b/mobile/openapi/doc/PersonCreateDto.md new file mode 100644 index 000000000..427c23382 --- /dev/null +++ b/mobile/openapi/doc/PersonCreateDto.md @@ -0,0 +1,17 @@ +# openapi.model.PersonCreateDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**birthDate** | [**DateTime**](DateTime.md) | Person date of birth. Note: the mobile app cannot currently set the birth date to null. | [optional] +**isHidden** | **bool** | Person visibility | [optional] +**name** | **String** | Person name. | [optional] + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/doc/ScanLibraryDto.md b/mobile/openapi/doc/ScanLibraryDto.md index 39f55290d..e2c489d85 100644 --- a/mobile/openapi/doc/ScanLibraryDto.md +++ b/mobile/openapi/doc/ScanLibraryDto.md @@ -8,7 +8,7 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**refreshAllFiles** | **bool** | | [optional] [default to false] +**refreshAllFiles** | **bool** | | [optional] **refreshModifiedFiles** | **bool** | | [optional] [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/ServerConfigDto.md b/mobile/openapi/doc/ServerConfigDto.md index 317431b9b..7261965bf 100644 --- a/mobile/openapi/doc/ServerConfigDto.md +++ b/mobile/openapi/doc/ServerConfigDto.md @@ -14,6 +14,7 @@ Name | Type | Description | Notes **loginPageMessage** | **String** | | **oauthButtonText** | **String** | | **trashDays** | **int** | | +**userDeleteDelay** | **int** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SharedLinkCreateDto.md b/mobile/openapi/doc/SharedLinkCreateDto.md index 8f845dfa4..78e208912 100644 --- a/mobile/openapi/doc/SharedLinkCreateDto.md +++ b/mobile/openapi/doc/SharedLinkCreateDto.md @@ -10,7 +10,7 @@ Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **albumId** | **String** | | [optional] **allowDownload** | **bool** | | [optional] [default to true] -**allowUpload** | **bool** | | [optional] [default to false] +**allowUpload** | **bool** | | [optional] **assetIds** | **List** | | [optional] [default to const []] **description** | **String** | | [optional] **expiresAt** | [**DateTime**](DateTime.md) | | [optional] diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index 51bf203ff..ad1afbe9f 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -23,6 +23,7 @@ Name | Type | Description | Notes **theme** | [**SystemConfigThemeDto**](SystemConfigThemeDto.md) | | **thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | | **trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.md) | | +**user** | [**SystemConfigUserDto**](SystemConfigUserDto.md) | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/doc/SystemConfigUserDto.md b/mobile/openapi/doc/SystemConfigUserDto.md new file mode 100644 index 000000000..c295954a8 --- /dev/null +++ b/mobile/openapi/doc/SystemConfigUserDto.md @@ -0,0 +1,15 @@ +# openapi.model.SystemConfigUserDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**deleteDelay** | **int** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 56bd907e0..0a093e453 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -138,6 +138,7 @@ part 'model/path_type.dart'; part 'model/people_response_dto.dart'; part 'model/people_update_dto.dart'; part 'model/people_update_item.dart'; +part 'model/person_create_dto.dart'; part 'model/person_response_dto.dart'; part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; @@ -190,6 +191,7 @@ part 'model/system_config_template_storage_option_dto.dart'; part 'model/system_config_theme_dto.dart'; part 'model/system_config_thumbnail_dto.dart'; part 'model/system_config_trash_dto.dart'; +part 'model/system_config_user_dto.dart'; part 'model/tag_response_dto.dart'; part 'model/tag_type_enum.dart'; part 'model/thumbnail_format.dart'; diff --git a/mobile/openapi/lib/api/person_api.dart b/mobile/openapi/lib/api/person_api.dart index 02dae625b..411c75d71 100644 --- a/mobile/openapi/lib/api/person_api.dart +++ b/mobile/openapi/lib/api/person_api.dart @@ -17,18 +17,21 @@ class PersonApi { final ApiClient apiClient; /// Performs an HTTP 'POST /person' operation and returns the [Response]. - Future createPersonWithHttpInfo() async { + /// Parameters: + /// + /// * [PersonCreateDto] personCreateDto (required): + Future createPersonWithHttpInfo(PersonCreateDto personCreateDto,) async { // ignore: prefer_const_declarations final path = r'/person'; // ignore: prefer_final_locals - Object? postBody; + Object? postBody = personCreateDto; final queryParams = []; final headerParams = {}; final formParams = {}; - const contentTypes = []; + const contentTypes = ['application/json']; return apiClient.invokeAPI( @@ -42,8 +45,11 @@ class PersonApi { ); } - Future createPerson() async { - final response = await createPersonWithHttpInfo(); + /// Parameters: + /// + /// * [PersonCreateDto] personCreateDto (required): + Future createPerson(PersonCreateDto personCreateDto,) async { + final response = await createPersonWithHttpInfo(personCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 24cffb7cf..5e5f70299 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -358,6 +358,8 @@ class ApiClient { return PeopleUpdateDto.fromJson(value); case 'PeopleUpdateItem': return PeopleUpdateItem.fromJson(value); + case 'PersonCreateDto': + return PersonCreateDto.fromJson(value); case 'PersonResponseDto': return PersonResponseDto.fromJson(value); case 'PersonStatisticsResponseDto': @@ -462,6 +464,8 @@ class ApiClient { return SystemConfigThumbnailDto.fromJson(value); case 'SystemConfigTrashDto': return SystemConfigTrashDto.fromJson(value); + case 'SystemConfigUserDto': + return SystemConfigUserDto.fromJson(value); case 'TagResponseDto': return TagResponseDto.fromJson(value); case 'TagTypeEnum': diff --git a/mobile/openapi/lib/model/create_user_dto.dart b/mobile/openapi/lib/model/create_user_dto.dart index f272842cb..2dada4f67 100644 --- a/mobile/openapi/lib/model/create_user_dto.dart +++ b/mobile/openapi/lib/model/create_user_dto.dart @@ -18,6 +18,7 @@ class CreateUserDto { required this.name, required this.password, this.quotaSizeInBytes, + this.shouldChangePassword, this.storageLabel, }); @@ -37,6 +38,14 @@ class CreateUserDto { int? quotaSizeInBytes; + /// + /// 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? shouldChangePassword; + String? storageLabel; @override @@ -46,6 +55,7 @@ class CreateUserDto { other.name == name && other.password == password && other.quotaSizeInBytes == quotaSizeInBytes && + other.shouldChangePassword == shouldChangePassword && other.storageLabel == storageLabel; @override @@ -56,10 +66,11 @@ class CreateUserDto { (name.hashCode) + (password.hashCode) + (quotaSizeInBytes == null ? 0 : quotaSizeInBytes!.hashCode) + + (shouldChangePassword == null ? 0 : shouldChangePassword!.hashCode) + (storageLabel == null ? 0 : storageLabel!.hashCode); @override - String toString() => 'CreateUserDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, storageLabel=$storageLabel]'; + String toString() => 'CreateUserDto[email=$email, memoriesEnabled=$memoriesEnabled, name=$name, password=$password, quotaSizeInBytes=$quotaSizeInBytes, shouldChangePassword=$shouldChangePassword, storageLabel=$storageLabel]'; Map toJson() { final json = {}; @@ -76,6 +87,11 @@ class CreateUserDto { } else { // json[r'quotaSizeInBytes'] = null; } + if (this.shouldChangePassword != null) { + json[r'shouldChangePassword'] = this.shouldChangePassword; + } else { + // json[r'shouldChangePassword'] = null; + } if (this.storageLabel != null) { json[r'storageLabel'] = this.storageLabel; } else { @@ -97,6 +113,7 @@ class CreateUserDto { name: mapValueOfType(json, r'name')!, password: mapValueOfType(json, r'password')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), + shouldChangePassword: mapValueOfType(json, r'shouldChangePassword'), storageLabel: mapValueOfType(json, r'storageLabel'), ); } diff --git a/mobile/openapi/lib/model/map_marker_response_dto.dart b/mobile/openapi/lib/model/map_marker_response_dto.dart index e4e099d62..8331f0679 100644 --- a/mobile/openapi/lib/model/map_marker_response_dto.dart +++ b/mobile/openapi/lib/model/map_marker_response_dto.dart @@ -13,38 +13,68 @@ part of openapi.api; class MapMarkerResponseDto { /// Returns a new [MapMarkerResponseDto] instance. MapMarkerResponseDto({ + required this.city, + required this.country, required this.id, required this.lat, required this.lon, + required this.state, }); + String? city; + + String? country; + String id; double lat; double lon; + String? state; + @override bool operator ==(Object other) => identical(this, other) || other is MapMarkerResponseDto && + other.city == city && + other.country == country && other.id == id && other.lat == lat && - other.lon == lon; + other.lon == lon && + other.state == state; @override int get hashCode => // ignore: unnecessary_parenthesis + (city == null ? 0 : city!.hashCode) + + (country == null ? 0 : country!.hashCode) + (id.hashCode) + (lat.hashCode) + - (lon.hashCode); + (lon.hashCode) + + (state == null ? 0 : state!.hashCode); @override - String toString() => 'MapMarkerResponseDto[id=$id, lat=$lat, lon=$lon]'; + String toString() => 'MapMarkerResponseDto[city=$city, country=$country, id=$id, lat=$lat, lon=$lon, state=$state]'; 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; + } json[r'id'] = this.id; json[r'lat'] = this.lat; json[r'lon'] = this.lon; + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } return json; } @@ -56,9 +86,12 @@ class MapMarkerResponseDto { final json = value.cast(); return MapMarkerResponseDto( + city: mapValueOfType(json, r'city'), + country: mapValueOfType(json, r'country'), id: mapValueOfType(json, r'id')!, lat: (mapValueOfType(json, r'lat')!).toDouble(), lon: (mapValueOfType(json, r'lon')!).toDouble(), + state: mapValueOfType(json, r'state'), ); } return null; @@ -106,9 +139,12 @@ class MapMarkerResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'city', + 'country', 'id', 'lat', 'lon', + 'state', }; } diff --git a/mobile/openapi/lib/model/person_create_dto.dart b/mobile/openapi/lib/model/person_create_dto.dart new file mode 100644 index 000000000..4811de3ef --- /dev/null +++ b/mobile/openapi/lib/model/person_create_dto.dart @@ -0,0 +1,138 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 PersonCreateDto { + /// Returns a new [PersonCreateDto] instance. + PersonCreateDto({ + this.birthDate, + this.isHidden, + this.name, + }); + + /// Person date of birth. Note: the mobile app cannot currently set the birth date to null. + DateTime? birthDate; + + /// Person visibility + /// + /// 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? isHidden; + + /// Person name. + /// + /// 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? name; + + @override + bool operator ==(Object other) => identical(this, other) || other is PersonCreateDto && + other.birthDate == birthDate && + other.isHidden == isHidden && + other.name == name; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (birthDate == null ? 0 : birthDate!.hashCode) + + (isHidden == null ? 0 : isHidden!.hashCode) + + (name == null ? 0 : name!.hashCode); + + @override + String toString() => 'PersonCreateDto[birthDate=$birthDate, isHidden=$isHidden, name=$name]'; + + Map toJson() { + final json = {}; + if (this.birthDate != null) { + json[r'birthDate'] = _dateFormatter.format(this.birthDate!.toUtc()); + } else { + // json[r'birthDate'] = null; + } + if (this.isHidden != null) { + json[r'isHidden'] = this.isHidden; + } else { + // json[r'isHidden'] = null; + } + if (this.name != null) { + json[r'name'] = this.name; + } else { + // json[r'name'] = null; + } + return json; + } + + /// Returns a new [PersonCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PersonCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PersonCreateDto( + birthDate: mapDateTime(json, r'birthDate', r''), + isHidden: mapValueOfType(json, r'isHidden'), + name: mapValueOfType(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 = PersonCreateDto.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 = PersonCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PersonCreateDto-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] = PersonCreateDto.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/scan_library_dto.dart b/mobile/openapi/lib/model/scan_library_dto.dart index 2b34e2bbe..0f5dedf64 100644 --- a/mobile/openapi/lib/model/scan_library_dto.dart +++ b/mobile/openapi/lib/model/scan_library_dto.dart @@ -13,11 +13,17 @@ part of openapi.api; class ScanLibraryDto { /// Returns a new [ScanLibraryDto] instance. ScanLibraryDto({ - this.refreshAllFiles = false, + this.refreshAllFiles, this.refreshModifiedFiles, }); - 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? refreshAllFiles; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -35,7 +41,7 @@ class ScanLibraryDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (refreshAllFiles.hashCode) + + (refreshAllFiles == null ? 0 : refreshAllFiles!.hashCode) + (refreshModifiedFiles == null ? 0 : refreshModifiedFiles!.hashCode); @override @@ -43,7 +49,11 @@ class ScanLibraryDto { 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 { @@ -60,7 +70,7 @@ class ScanLibraryDto { final json = value.cast(); return ScanLibraryDto( - refreshAllFiles: mapValueOfType(json, r'refreshAllFiles') ?? false, + refreshAllFiles: mapValueOfType(json, r'refreshAllFiles'), refreshModifiedFiles: mapValueOfType(json, r'refreshModifiedFiles'), ); } diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index 1509c1bbe..faa167c73 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -19,6 +19,7 @@ class ServerConfigDto { required this.loginPageMessage, required this.oauthButtonText, required this.trashDays, + required this.userDeleteDelay, }); String externalDomain; @@ -33,6 +34,8 @@ class ServerConfigDto { int trashDays; + int userDeleteDelay; + @override bool operator ==(Object other) => identical(this, other) || other is ServerConfigDto && other.externalDomain == externalDomain && @@ -40,7 +43,8 @@ class ServerConfigDto { other.isOnboarded == isOnboarded && other.loginPageMessage == loginPageMessage && other.oauthButtonText == oauthButtonText && - other.trashDays == trashDays; + other.trashDays == trashDays && + other.userDeleteDelay == userDeleteDelay; @override int get hashCode => @@ -50,10 +54,11 @@ class ServerConfigDto { (isOnboarded.hashCode) + (loginPageMessage.hashCode) + (oauthButtonText.hashCode) + - (trashDays.hashCode); + (trashDays.hashCode) + + (userDeleteDelay.hashCode); @override - String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, oauthButtonText=$oauthButtonText, trashDays=$trashDays]'; + String toString() => 'ServerConfigDto[externalDomain=$externalDomain, isInitialized=$isInitialized, isOnboarded=$isOnboarded, loginPageMessage=$loginPageMessage, oauthButtonText=$oauthButtonText, trashDays=$trashDays, userDeleteDelay=$userDeleteDelay]'; Map toJson() { final json = {}; @@ -63,6 +68,7 @@ class ServerConfigDto { json[r'loginPageMessage'] = this.loginPageMessage; json[r'oauthButtonText'] = this.oauthButtonText; json[r'trashDays'] = this.trashDays; + json[r'userDeleteDelay'] = this.userDeleteDelay; return json; } @@ -80,6 +86,7 @@ class ServerConfigDto { loginPageMessage: mapValueOfType(json, r'loginPageMessage')!, oauthButtonText: mapValueOfType(json, r'oauthButtonText')!, trashDays: mapValueOfType(json, r'trashDays')!, + userDeleteDelay: mapValueOfType(json, r'userDeleteDelay')!, ); } return null; @@ -133,6 +140,7 @@ class ServerConfigDto { 'loginPageMessage', 'oauthButtonText', 'trashDays', + 'userDeleteDelay', }; } diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 14dc109a9..920e62e52 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -15,7 +15,7 @@ class SharedLinkCreateDto { SharedLinkCreateDto({ this.albumId, this.allowDownload = true, - this.allowUpload = false, + this.allowUpload, this.assetIds = const [], this.description, this.expiresAt, @@ -34,7 +34,13 @@ class SharedLinkCreateDto { bool allowDownload; - bool allowUpload; + /// + /// 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? allowUpload; List assetIds; @@ -77,7 +83,7 @@ class SharedLinkCreateDto { // ignore: unnecessary_parenthesis (albumId == null ? 0 : albumId!.hashCode) + (allowDownload.hashCode) + - (allowUpload.hashCode) + + (allowUpload == null ? 0 : allowUpload!.hashCode) + (assetIds.hashCode) + (description == null ? 0 : description!.hashCode) + (expiresAt == null ? 0 : expiresAt!.hashCode) + @@ -96,7 +102,11 @@ class SharedLinkCreateDto { // json[r'albumId'] = null; } json[r'allowDownload'] = this.allowDownload; + if (this.allowUpload != null) { json[r'allowUpload'] = this.allowUpload; + } else { + // json[r'allowUpload'] = null; + } json[r'assetIds'] = this.assetIds; if (this.description != null) { json[r'description'] = this.description; @@ -128,7 +138,7 @@ class SharedLinkCreateDto { return SharedLinkCreateDto( albumId: mapValueOfType(json, r'albumId'), allowDownload: mapValueOfType(json, r'allowDownload') ?? true, - allowUpload: mapValueOfType(json, r'allowUpload') ?? false, + allowUpload: mapValueOfType(json, r'allowUpload'), assetIds: json[r'assetIds'] is Iterable ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 26387a163..0b5f64fc2 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -28,6 +28,7 @@ class SystemConfigDto { required this.theme, required this.thumbnail, required this.trash, + required this.user, }); SystemConfigFFmpegDto ffmpeg; @@ -60,6 +61,8 @@ class SystemConfigDto { SystemConfigTrashDto trash; + SystemConfigUserDto user; + @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto && other.ffmpeg == ffmpeg && @@ -76,7 +79,8 @@ class SystemConfigDto { other.storageTemplate == storageTemplate && other.theme == theme && other.thumbnail == thumbnail && - other.trash == trash; + other.trash == trash && + other.user == user; @override int get hashCode => @@ -95,10 +99,11 @@ class SystemConfigDto { (storageTemplate.hashCode) + (theme.hashCode) + (thumbnail.hashCode) + - (trash.hashCode); + (trash.hashCode) + + (user.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash, user=$user]'; Map toJson() { final json = {}; @@ -117,6 +122,7 @@ class SystemConfigDto { json[r'theme'] = this.theme; json[r'thumbnail'] = this.thumbnail; json[r'trash'] = this.trash; + json[r'user'] = this.user; return json; } @@ -143,6 +149,7 @@ class SystemConfigDto { theme: SystemConfigThemeDto.fromJson(json[r'theme'])!, thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!, trash: SystemConfigTrashDto.fromJson(json[r'trash'])!, + user: SystemConfigUserDto.fromJson(json[r'user'])!, ); } return null; @@ -205,6 +212,7 @@ class SystemConfigDto { 'theme', 'thumbnail', 'trash', + 'user', }; } diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart new file mode 100644 index 000000000..08d939c48 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 SystemConfigUserDto { + /// Returns a new [SystemConfigUserDto] instance. + SystemConfigUserDto({ + required this.deleteDelay, + }); + + int deleteDelay; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigUserDto && + other.deleteDelay == deleteDelay; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (deleteDelay.hashCode); + + @override + String toString() => 'SystemConfigUserDto[deleteDelay=$deleteDelay]'; + + Map toJson() { + final json = {}; + json[r'deleteDelay'] = this.deleteDelay; + return json; + } + + /// Returns a new [SystemConfigUserDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigUserDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigUserDto( + deleteDelay: mapValueOfType(json, r'deleteDelay')!, + ); + } + 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 = SystemConfigUserDto.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 = SystemConfigUserDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigUserDto-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] = SystemConfigUserDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'deleteDelay', + }; +} + diff --git a/mobile/openapi/test/create_user_dto_test.dart b/mobile/openapi/test/create_user_dto_test.dart index 9658c02c8..da1d5fac7 100644 --- a/mobile/openapi/test/create_user_dto_test.dart +++ b/mobile/openapi/test/create_user_dto_test.dart @@ -41,6 +41,11 @@ void main() { // TODO }); + // bool shouldChangePassword + test('to test the property `shouldChangePassword`', () async { + // TODO + }); + // String storageLabel test('to test the property `storageLabel`', () async { // TODO diff --git a/mobile/openapi/test/map_marker_response_dto_test.dart b/mobile/openapi/test/map_marker_response_dto_test.dart index f8308116f..966826083 100644 --- a/mobile/openapi/test/map_marker_response_dto_test.dart +++ b/mobile/openapi/test/map_marker_response_dto_test.dart @@ -16,6 +16,16 @@ void main() { // final instance = MapMarkerResponseDto(); group('test MapMarkerResponseDto', () { + // String city + test('to test the property `city`', () async { + // TODO + }); + + // String country + test('to test the property `country`', () async { + // TODO + }); + // String id test('to test the property `id`', () async { // TODO @@ -31,6 +41,11 @@ void main() { // TODO }); + // String state + test('to test the property `state`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/person_api_test.dart b/mobile/openapi/test/person_api_test.dart index dd112eeaa..959230cc5 100644 --- a/mobile/openapi/test/person_api_test.dart +++ b/mobile/openapi/test/person_api_test.dart @@ -17,7 +17,7 @@ void main() { // final instance = PersonApi(); group('tests for PersonApi', () { - //Future createPerson() async + //Future createPerson(PersonCreateDto personCreateDto) async test('test createPerson', () async { // TODO }); diff --git a/mobile/openapi/test/person_create_dto_test.dart b/mobile/openapi/test/person_create_dto_test.dart new file mode 100644 index 000000000..96f1fe6d3 --- /dev/null +++ b/mobile/openapi/test/person_create_dto_test.dart @@ -0,0 +1,40 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for PersonCreateDto +void main() { + // final instance = PersonCreateDto(); + + group('test PersonCreateDto', () { + // Person date of birth. Note: the mobile app cannot currently set the birth date to null. + // DateTime birthDate + test('to test the property `birthDate`', () async { + // TODO + }); + + // Person visibility + // bool isHidden + test('to test the property `isHidden`', () async { + // TODO + }); + + // Person name. + // String name + test('to test the property `name`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/openapi/test/scan_library_dto_test.dart b/mobile/openapi/test/scan_library_dto_test.dart index 975a6d757..2b3c75867 100644 --- a/mobile/openapi/test/scan_library_dto_test.dart +++ b/mobile/openapi/test/scan_library_dto_test.dart @@ -16,7 +16,7 @@ void main() { // final instance = ScanLibraryDto(); group('test ScanLibraryDto', () { - // bool refreshAllFiles (default value: false) + // bool refreshAllFiles test('to test the property `refreshAllFiles`', () async { // TODO }); diff --git a/mobile/openapi/test/server_config_dto_test.dart b/mobile/openapi/test/server_config_dto_test.dart index 813ac2565..f76556c50 100644 --- a/mobile/openapi/test/server_config_dto_test.dart +++ b/mobile/openapi/test/server_config_dto_test.dart @@ -46,6 +46,11 @@ void main() { // TODO }); + // int userDeleteDelay + test('to test the property `userDeleteDelay`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/shared_link_create_dto_test.dart b/mobile/openapi/test/shared_link_create_dto_test.dart index df57e089f..982d72a14 100644 --- a/mobile/openapi/test/shared_link_create_dto_test.dart +++ b/mobile/openapi/test/shared_link_create_dto_test.dart @@ -26,7 +26,7 @@ void main() { // TODO }); - // bool allowUpload (default value: false) + // bool allowUpload test('to test the property `allowUpload`', () async { // TODO }); diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 5f4154987..b41d07e5f 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -91,6 +91,11 @@ void main() { // TODO }); + // SystemConfigUserDto user + test('to test the property `user`', () async { + // TODO + }); + }); diff --git a/mobile/openapi/test/system_config_user_dto_test.dart b/mobile/openapi/test/system_config_user_dto_test.dart new file mode 100644 index 000000000..d3c7be050 --- /dev/null +++ b/mobile/openapi/test/system_config_user_dto_test.dart @@ -0,0 +1,27 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.12 + +// 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 + +import 'package:openapi/api.dart'; +import 'package:test/test.dart'; + +// tests for SystemConfigUserDto +void main() { + // final instance = SystemConfigUserDto(); + + group('test SystemConfigUserDto', () { + // int deleteDelay + test('to test the property `deleteDelay`', () async { + // TODO + }); + + + }); + +} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index f27351898..f7a57bb2b 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -50,7 +50,7 @@ packages: source: hosted version: "2.4.2" async: - dependency: transitive + dependency: "direct main" description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 04056977a..d0ab2a8ac 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.97.0+125 +version: 1.98.0+126 isar_version: &isar_version 3.1.0+1 environment: @@ -58,6 +58,7 @@ dependencies: timezone: ^0.9.2 octo_image: ^2.0.0 thumbhash: 0.1.0+1 + async: ^2.11.0 openapi: path: openapi diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4010336fd..8819825b9 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4012,7 +4012,6 @@ "required": false, "in": "query", "schema": { - "default": false, "type": "boolean" } } @@ -4047,6 +4046,16 @@ "post": { "operationId": "createPerson", "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonCreateDto" + } + } + }, + "required": true + }, "responses": { "201": { "content": { @@ -6467,7 +6476,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.97.0", + "version": "1.98.0", "contact": {} }, "tags": [], @@ -7672,6 +7681,9 @@ "nullable": true, "type": "integer" }, + "shouldChangePassword": { + "type": "boolean" + }, "storageLabel": { "nullable": true, "type": "string" @@ -8291,6 +8303,14 @@ }, "MapMarkerResponseDto": { "properties": { + "city": { + "nullable": true, + "type": "string" + }, + "country": { + "nullable": true, + "type": "string" + }, "id": { "type": "string" }, @@ -8301,12 +8321,19 @@ "lon": { "format": "double", "type": "number" + }, + "state": { + "nullable": true, + "type": "string" } }, "required": [ + "city", + "country", "id", "lat", - "lon" + "lon", + "state" ], "type": "object" }, @@ -8702,6 +8729,25 @@ ], "type": "object" }, + "PersonCreateDto": { + "properties": { + "birthDate": { + "description": "Person date of birth.\nNote: the mobile app cannot currently set the birth date to null.", + "format": "date", + "nullable": true, + "type": "string" + }, + "isHidden": { + "description": "Person visibility", + "type": "boolean" + }, + "name": { + "description": "Person name.", + "type": "string" + } + }, + "type": "object" + }, "PersonResponseDto": { "properties": { "birthDate": { @@ -8890,7 +8936,6 @@ "ScanLibraryDto": { "properties": { "refreshAllFiles": { - "default": false, "type": "boolean" }, "refreshModifiedFiles": { @@ -9072,6 +9117,9 @@ }, "trashDays": { "type": "integer" + }, + "userDeleteDelay": { + "type": "integer" } }, "required": [ @@ -9080,7 +9128,8 @@ "isOnboarded", "loginPageMessage", "oauthButtonText", - "trashDays" + "trashDays", + "userDeleteDelay" ], "type": "object" }, @@ -9295,7 +9344,6 @@ "type": "boolean" }, "allowUpload": { - "default": false, "type": "boolean" }, "assetIds": { @@ -9643,6 +9691,9 @@ }, "trash": { "$ref": "#/components/schemas/SystemConfigTrashDto" + }, + "user": { + "$ref": "#/components/schemas/SystemConfigUserDto" } }, "required": [ @@ -9660,7 +9711,8 @@ "storageTemplate", "theme", "thumbnail", - "trash" + "trash", + "user" ], "type": "object" }, @@ -10144,6 +10196,17 @@ ], "type": "object" }, + "SystemConfigUserDto": { + "properties": { + "deleteDelay": { + "type": "integer" + } + }, + "required": [ + "deleteDelay" + ], + "type": "object" + }, "TagResponseDto": { "properties": { "id": { diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index f231662d0..ff333b7f7 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.97.0", + "version": "1.98.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.97.0", + "version": "1.98.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@oazapfts/runtime": "^1.0.0", @@ -21,9 +21,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dev": true, "dependencies": { "undici-types": "~5.26.4" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index ea1b08540..2bd1cfddd 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.97.0", + "version": "1.98.0", "description": "", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 6f4937e54..e9ce46712 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.97.0 + * 1.98.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ @@ -260,9 +260,12 @@ export type AssetJobsDto = { name: AssetJobName; }; export type MapMarkerResponseDto = { + city: string | null; + country: string | null; id: string; lat: number; lon: number; + state: string | null; }; export type MemoryLaneResponseDto = { assets: AssetResponseDto[]; @@ -526,6 +529,15 @@ export type PeopleResponseDto = { people: PersonResponseDto[]; total: number; }; +export type PersonCreateDto = { + /** Person date of birth. + Note: the mobile app cannot currently set the birth date to null. */ + birthDate?: string | null; + /** Person visibility */ + isHidden?: boolean; + /** Person name. */ + name?: string; +}; export type PeopleUpdateItem = { /** Person date of birth. Note: the mobile app cannot currently set the birth date to null. */ @@ -702,6 +714,7 @@ export type ServerConfigDto = { loginPageMessage: string; oauthButtonText: string; trashDays: number; + userDeleteDelay: number; }; export type ServerFeaturesDto = { configFile: boolean; @@ -915,6 +928,9 @@ export type SystemConfigTrashDto = { days: number; enabled: boolean; }; +export type SystemConfigUserDto = { + deleteDelay: number; +}; export type SystemConfigDto = { ffmpeg: SystemConfigFFmpegDto; job: SystemConfigJobDto; @@ -931,6 +947,7 @@ export type SystemConfigDto = { theme: SystemConfigThemeDto; thumbnail: SystemConfigThumbnailDto; trash: SystemConfigTrashDto; + user: SystemConfigUserDto; }; export type SystemConfigTemplateStorageOptionDto = { dayOptions: string[]; @@ -955,6 +972,7 @@ export type CreateUserDto = { name: string; password: string; quotaSizeInBytes?: number | null; + shouldChangePassword?: boolean; storageLabel?: string | null; }; export type UpdateUserDto = { @@ -2042,14 +2060,17 @@ export function getAllPeople({ withHidden }: { ...opts })); } -export function createPerson(opts?: Oazapfts.RequestOpts) { +export function createPerson({ personCreateDto }: { + personCreateDto: PersonCreateDto; +}, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 201; data: PersonResponseDto; - }>("/person", { + }>("/person", oazapfts.json({ ...opts, - method: "POST" - })); + method: "POST", + body: personCreateDto + }))); } export function updatePeople({ peopleUpdateDto }: { peopleUpdateDto: PeopleUpdateDto; diff --git a/server/.eslintrc.js b/server/.eslintrc.js index f1e6564d8..3673add3c 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -25,6 +25,12 @@ module.exports = { 'unicorn/prefer-top-level-await': 'off', 'unicorn/prefer-event-target': 'off', 'unicorn/no-thenable': 'off', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + // Note: you must disable the base rule as it can report incorrect errors + 'require-await': 'off', + '@typescript-eslint/require-await': 'error', curly: 2, 'prettier/prettier': 0, }, diff --git a/server/Dockerfile b/server/Dockerfile index 0ebd5c44c..e6af34fd6 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240227@sha256:b1e212c106ce2318a587e0b2ef377215c958e877f61993ed9310534e4589cce4 as dev +FROM ghcr.io/immich-app/base-server-dev:20240305@sha256:99ca204d84284dac24dbec59ffeaea07c02f4bd9b06b09e1aa9aacc4f3ece92e as dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -40,7 +40,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240227@sha256:d47f5f7f2b6c53957c6353352b2fa24f2845da50e6491a7c74eb779ace10628c +FROM ghcr.io/immich-app/base-server-prod:20240305@sha256:d0bcac4e77f1371d6c4b8ecc415c390cc348d09e48504d4455f38f2968e41c1c WORKDIR /usr/src/app ENV NODE_ENV=production \ diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 748418718..6badd4c67 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -14,6 +14,7 @@ import { AssetEntity, AssetStackEntity, AssetType, SharedLinkType } from '@app/i import { AssetRepository } from '@app/infra/repositories'; import { INestApplication } from '@nestjs/common'; import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { assetApi } from 'e2e/client/asset-api'; import { randomBytes } from 'node:crypto'; import request from 'supertest'; import { api } from '../../client'; @@ -532,6 +533,23 @@ describe(`${AssetController.name} (e2e)`, () => { } }); } + + it('should return stack data', async () => { + const parentId = asset1.id; + const childIds = [asset2.id, asset3.id]; + await request(server) + .put('/asset') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ stackParentId: parentId, ids: childIds }); + + const body = await assetApi.getAllAssets(server, user1.accessToken); + // Response includes parent with stack children count + const parentDto = body.find((a) => a.id == parentId); + expect(parentDto?.stackCount).toEqual(3); + + // Response includes children at the root level + expect.arrayContaining([expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })]); + }); }); describe('POST /asset/upload', () => { @@ -591,6 +609,42 @@ describe(`${AssetController.name} (e2e)`, () => { expect(asset).toMatchObject({ id: body.id, isFavorite: true }); }); + it('should have correct original file name and extension (simple)', async () => { + const { body, status } = await request(server) + .post('/asset/upload') + .set('Authorization', `Bearer ${user1.accessToken}`) + .field('deviceAssetId', 'example-image') + .field('deviceId', 'TEST') + .field('fileCreatedAt', new Date().toISOString()) + .field('fileModifiedAt', new Date().toISOString()) + .field('isFavorite', 'true') + .field('duration', '0:00:00.000000') + .attach('assetData', randomBytes(32), 'example.jpg'); + expect(status).toBe(201); + expect(body).toEqual({ id: expect.any(String), duplicate: false }); + + const asset = await api.assetApi.get(server, user1.accessToken, body.id); + expect(asset).toMatchObject({ id: body.id, originalFileName: 'example.jpg' }); + }); + + it('should have correct original file name and extension (complex)', async () => { + const { body, status } = await request(server) + .post('/asset/upload') + .set('Authorization', `Bearer ${user1.accessToken}`) + .field('deviceAssetId', 'example-image') + .field('deviceId', 'TEST') + .field('fileCreatedAt', new Date().toISOString()) + .field('fileModifiedAt', new Date().toISOString()) + .field('isFavorite', 'true') + .field('duration', '0:00:00.000000') + .attach('assetData', randomBytes(32), 'example.complex.ext.jpg'); + expect(status).toBe(201); + expect(body).toEqual({ id: expect.any(String), duplicate: false }); + + const asset = await api.assetApi.get(server, user1.accessToken, body.id); + expect(asset).toMatchObject({ id: body.id, originalFileName: 'example.complex.ext.jpg' }); + }); + it('should not upload the same asset twice', async () => { const content = randomBytes(32); await api.assetApi.upload(server, user1.accessToken, 'example-image', { content }); diff --git a/server/e2e/api/specs/search.e2e-spec.ts b/server/e2e/api/specs/search.e2e-spec.ts deleted file mode 100644 index 0e5cc428c..000000000 --- a/server/e2e/api/specs/search.e2e-spec.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { - AssetResponseDto, - IAssetRepository, - ISearchRepository, - LibraryResponseDto, - LoginResponseDto, - mapAsset, -} from '@app/domain'; -import { SearchController } from '@app/immich'; -import { INestApplication } from '@nestjs/common'; -import { errorStub, searchStub } from '@test/fixtures'; -import request from 'supertest'; -import { api } from '../../client'; -import { generateAsset, testApp } from '../utils'; - -describe(`${SearchController.name}`, () => { - let app: INestApplication; - let server: any; - let loginResponse: LoginResponseDto; - let accessToken: string; - let libraries: LibraryResponseDto[]; - let assetRepository: IAssetRepository; - let smartInfoRepository: ISearchRepository; - let asset1: AssetResponseDto; - - beforeAll(async () => { - app = await testApp.create(); - server = app.getHttpServer(); - assetRepository = app.get(IAssetRepository); - smartInfoRepository = app.get(ISearchRepository); - }); - - afterAll(async () => { - await testApp.teardown(); - }); - - beforeEach(async () => { - await testApp.reset(); - await api.authApi.adminSignUp(server); - loginResponse = await api.authApi.adminLogin(server); - accessToken = loginResponse.accessToken; - libraries = await api.libraryApi.getAll(server, accessToken); - }); - - describe('GET /search (exif)', () => { - beforeEach(async () => { - const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries)); - await assetRepository.upsertExif({ assetId, ...searchStub.exif }); - - const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true }); - if (!assetWithMetadata) { - throw new Error('Asset not found'); - } - asset1 = mapAsset(assetWithMetadata); - }); - - it('should require authentication', async () => { - const { status, body } = await request(server).get('/search'); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should return assets when searching by exif', async () => { - if (!asset1?.exifInfo?.make) { - throw new Error('Asset 1 does not have exif info'); - } - - const { status, body } = await request(server) - .get('/search') - .set('Authorization', `Bearer ${accessToken}`) - .query({ q: asset1.exifInfo.make }); - - expect(status).toBe(200); - expect(body).toMatchObject({ - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: 1, - count: 1, - items: [ - { - id: asset1.id, - exifInfo: { - make: asset1.exifInfo.make, - }, - }, - ], - facets: [], - }, - }); - }); - - it('should be case-insensitive for metadata search', async () => { - if (!asset1?.exifInfo?.make) { - throw new Error('Asset 1 does not have exif info'); - } - - const { status, body } = await request(server) - .get('/search') - .set('Authorization', `Bearer ${accessToken}`) - .query({ q: asset1.exifInfo.make.toLowerCase() }); - - expect(status).toBe(200); - expect(body).toMatchObject({ - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: 1, - count: 1, - items: [ - { - id: asset1.id, - exifInfo: { - make: asset1.exifInfo.make, - }, - }, - ], - facets: [], - }, - }); - }); - - it('should be whitespace-insensitive for metadata search', async () => { - if (!asset1?.exifInfo?.make) { - throw new Error('Asset 1 does not have exif info'); - } - - const { status, body } = await request(server) - .get('/search') - .set('Authorization', `Bearer ${accessToken}`) - .query({ q: ` ${asset1.exifInfo.make} ` }); - - expect(status).toBe(200); - expect(body).toMatchObject({ - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: 1, - count: 1, - items: [ - { - id: asset1.id, - exifInfo: { - make: asset1.exifInfo.make, - }, - }, - ], - facets: [], - }, - }); - }); - }); - - describe('GET /search (smart info)', () => { - beforeEach(async () => { - const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries)); - await assetRepository.upsertExif({ assetId, ...searchStub.exif }); - await smartInfoRepository.upsert({ assetId, ...searchStub.smartInfo }, Array.from({ length: 512 }, Math.random)); - - const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true, smartInfo: true }); - if (!assetWithMetadata) { - throw new Error('Asset not found'); - } - asset1 = mapAsset(assetWithMetadata); - }); - - it('should return assets when searching by object', async () => { - if (!asset1?.smartInfo?.objects) { - throw new Error('Asset 1 does not have smart info'); - } - - const { status, body } = await request(server) - .get('/search') - .set('Authorization', `Bearer ${accessToken}`) - .query({ q: asset1.smartInfo.objects[0] }); - - expect(status).toBe(200); - expect(body).toMatchObject({ - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: 1, - count: 1, - items: [ - { - id: asset1.id, - smartInfo: { - objects: asset1.smartInfo.objects, - tags: asset1.smartInfo.tags, - }, - }, - ], - facets: [], - }, - }); - }); - }); - - describe('GET /search (file name)', () => { - beforeEach(async () => { - const { id: assetId } = await assetRepository.create(generateAsset(loginResponse.userId, libraries)); - await assetRepository.upsertExif({ assetId, ...searchStub.exif }); - - const assetWithMetadata = await assetRepository.getById(assetId, { exifInfo: true }); - if (!assetWithMetadata) { - throw new Error('Asset not found'); - } - asset1 = mapAsset(assetWithMetadata); - }); - - it('should return assets when searching by file name', async () => { - if (asset1?.originalFileName.length === 0) { - throw new Error('Asset 1 does not have an original file name'); - } - - const { status, body } = await request(server) - .get('/search') - .set('Authorization', `Bearer ${accessToken}`) - .query({ q: asset1.originalFileName }); - - expect(status).toBe(200); - expect(body).toMatchObject({ - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: 1, - count: 1, - items: [ - { - id: asset1.id, - originalFileName: asset1.originalFileName, - }, - ], - facets: [], - }, - }); - }); - - it('should return assets when searching by file name with extension', async () => { - if (asset1?.originalFileName.length === 0) { - throw new Error('Asset 1 does not have an original file name'); - } - - const { status, body } = await request(server) - .get('/search') - .set('Authorization', `Bearer ${accessToken}`) - .query({ q: asset1.originalFileName + '.jpg' }); - - expect(status).toBe(200); - expect(body).toMatchObject({ - albums: { - total: 0, - count: 0, - items: [], - facets: [], - }, - assets: { - total: 1, - count: 1, - items: [ - { - id: asset1.id, - originalFileName: asset1.originalFileName, - }, - ], - facets: [], - }, - }); - }); - }); -}); diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts index 93f716353..475787689 100644 --- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts +++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts @@ -1,4 +1,4 @@ -import { LibraryResponseDto, LibraryService, LoginResponseDto } from '@app/domain'; +import { LibraryResponseDto, LibraryService, LoginResponseDto, StorageEventType } from '@app/domain'; import { AssetType, LibraryType } from '@app/infra/entities'; import fs from 'node:fs/promises'; import path from 'node:path'; @@ -33,7 +33,7 @@ describe(`Library watcher (e2e)`, () => { }); afterEach(async () => { - await libraryService.unwatchAll(); + await libraryService.teardown(); }); afterAll(async () => { @@ -57,7 +57,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEventType.ADD); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(1); @@ -84,10 +84,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`, ); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEventType.ADD, 4); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(4); @@ -99,7 +96,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEventType.ADD); const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(originalAssets.length).toEqual(1); @@ -109,7 +106,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`, ); - await waitForEvent(libraryService, 'change'); + await waitForEvent(libraryService, StorageEventType.CHANGE); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets).toEqual([ @@ -161,9 +158,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`, ); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEventType.ADD, 3); const assets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(assets.length).toEqual(3); @@ -175,14 +170,14 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEventType.ADD); const addedAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(addedAssets.length).toEqual(1); await fs.unlink(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`); - await waitForEvent(libraryService, 'unlink'); + await waitForEvent(libraryService, StorageEventType.UNLINK); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets[0].isOffline).toEqual(true); @@ -208,7 +203,7 @@ describe(`Library watcher (e2e)`, () => { await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true }); }); - it('should use an updated import paths', async () => { + it('should use an updated import path', async () => { await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, { recursive: true }); await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [ @@ -220,7 +215,7 @@ describe(`Library watcher (e2e)`, () => { `${IMMICH_TEST_ASSET_TEMP_PATH}/dir4/file.jpg`, ); - await waitForEvent(libraryService, 'add'); + await waitForEvent(libraryService, StorageEventType.ADD); const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken); expect(afterAssets.length).toEqual(1); diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts index 0657227f8..6dca783c4 100644 --- a/server/e2e/jobs/specs/library.e2e-spec.ts +++ b/server/e2e/jobs/specs/library.e2e-spec.ts @@ -368,7 +368,7 @@ describe(`${LibraryController.name} (e2e)`, () => { expect(body).toEqual(errorStub.unauthorized); }); - it('should remvove offline files', async () => { + it('should remove offline files', async () => { await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, { recursive: true, }); diff --git a/server/package-lock.json b/server/package-lock.json index 4ba12e263..54dd09902 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.97.0", + "version": "1.98.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.97.0", + "version": "1.98.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@babel/runtime": "^7.22.11", @@ -23,7 +23,7 @@ "@nestjs/websockets": "^10.2.2", "@socket.io/postgres-adapter": "^0.3.1", "@types/picomatch": "^2.3.3", - "archiver": "^6.0.0", + "archiver": "^7.0.0", "async-lock": "^1.4.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", @@ -876,9 +876,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -1067,9 +1067,9 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1101,13 +1101,13 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" }, "engines": { @@ -1128,9 +1128,9 @@ } }, "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "node_modules/@img/sharp-darwin-arm64": { @@ -2800,12 +2800,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@testcontainers/postgresql": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", - "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.2.tgz", + "integrity": "sha512-BB1C4SUDhWYY4X9afHtVYXDcYpvtRz7M4CtNEI3gA5A8Zm09msg5NUmhDSJiNAO8/xILv5LHcMJ0NZPBjgM9Jg==", "dev": true, "dependencies": { - "testcontainers": "^10.7.1" + "testcontainers": "^10.7.2" } }, "node_modules/@tsconfig/node10": { @@ -2953,9 +2953,9 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "node_modules/@types/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", "dev": true, "dependencies": { "@types/express": "*" @@ -2986,9 +2986,9 @@ } }, "node_modules/@types/dockerode": { - "version": "3.3.23", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", - "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", + "version": "3.3.24", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.24.tgz", + "integrity": "sha512-679y69OYusf7Fr2HtdjXPUF6hnHxSA9K4EsuagsMuPno/XpJHjXxCOy2I5YL8POnWbzjsQAi0pyKIYM9HSpQog==", "dev": true, "dependencies": { "@types/docker-modem": "*", @@ -3179,9 +3179,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "dependencies": { "undici-types": "~5.26.4" } @@ -3398,16 +3398,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", - "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/type-utils": "7.0.2", - "@typescript-eslint/utils": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3433,15 +3433,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", - "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" }, "engines": { @@ -3461,13 +3461,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", - "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3478,13 +3478,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", - "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3505,9 +3505,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", - "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3518,13 +3518,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", - "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3570,17 +3570,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" }, "engines": { @@ -3595,12 +3595,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", - "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -3780,6 +3780,17 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3978,28 +3989,28 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "node_modules/archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-R9HM9egs8FfktSqUqyjlKmvF4U+CWNqm/2tlROV+lOFg79MLdT67ae1l3hU47pGy8twSXxHoiefMCh43w0BriQ==", "dependencies": { - "archiver-utils": "^4.0.1", + "archiver-utils": "^5.0.0", "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "zip-stream": "^6.0.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" } }, "node_modules/archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.1.tgz", + "integrity": "sha512-MMAoLdMvT/nckofX1tCLrf7uJce4jTNkiT6smA2u57AOImc1nce7mR3EDujxL5yv6/MnILuQH4sAsPtDS8kTvg==", "dependencies": { - "glob": "^8.0.0", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash": "^4.17.15", @@ -4007,44 +4018,53 @@ "readable-stream": "^3.6.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" } }, - "node_modules/archiver-utils/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "node_modules/archiver/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], "dependencies": { - "balanced-match": "^1.0.0" + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "node_modules/archiver-utils/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "node_modules/archiver/node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/archiver/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/archiver-utils/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/are-we-there-yet": { @@ -4252,6 +4272,43 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/bare-events": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.1.tgz", + "integrity": "sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==", + "dev": true, + "optional": true + }, + "node_modules/bare-fs": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.1.tgz", + "integrity": "sha512-+CjmZANQDFZWy4PGbVdmALIwmt33aJg8qTkVjClU6X4WmZkTPBDxRHiBn7fpqEWEfF3AC2io++erpViAIQbSjg==", + "dev": true, + "optional": true, + "dependencies": { + "bare-events": "^2.0.0", + "bare-os": "^2.0.0", + "bare-path": "^2.0.0", + "streamx": "^2.13.0" + } + }, + "node_modules/bare-os": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.0.tgz", + "integrity": "sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==", + "dev": true, + "optional": true + }, + "node_modules/bare-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.0.tgz", + "integrity": "sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==", + "dev": true, + "optional": true, + "dependencies": { + "bare-os": "^2.1.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -4468,6 +4525,7 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, "engines": { "node": "*" } @@ -4980,17 +5038,55 @@ "dev": true }, "node_modules/compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.1.tgz", + "integrity": "sha512-l7occIJn8YwlCEbWUCrG6gPms9qnJTCZSaznCa5HaV+yJMH4kM8BDc7q9NyoQuoiB2O6jKgTcTeY462qw6MyHw==", "dependencies": { "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", + "crc32-stream": "^6.0.0", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" + } + }, + "node_modules/compress-commons/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/compress-commons/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/concat-map": { @@ -5160,15 +5256,53 @@ } }, "node_modules/crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "dependencies": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" + } + }, + "node_modules/crc32-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/crc32-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, "node_modules/create-jest": { @@ -5417,9 +5551,9 @@ "dev": true }, "node_modules/docker-compose": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz", - "integrity": "sha512-2/WLvA7UZ6A2LDLQrYW0idKipmNBWhtfvrn2yzjC5PnHDzuFVj1zAZN6MJxVMKP0zZH8uzAK6OwVZYHGuyCmTw==", + "version": "0.24.6", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.6.tgz", + "integrity": "sha512-VidlUyNzXMaVsuM79sjSvwC4nfojkP2VneL+Zfs538M2XFnffZDhx6veqnz/evCNIYGyz5O+1fgL6+g0NLWTBA==", "dev": true, "dependencies": { "yaml": "^2.2.2" @@ -5661,16 +5795,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -5959,11 +6093,18 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, "engines": { "node": ">=0.8.x" } @@ -8151,9 +8292,9 @@ } }, "node_modules/jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==", + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==", "funding": { "url": "https://github.com/sponsors/panva" } @@ -9092,7 +9233,7 @@ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz", "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", "dependencies": { - "jose": "^4.15.4", + "jose": "^4.15.5", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -9669,6 +9810,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -11145,14 +11294,17 @@ } }, "node_modules/tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", "dev": true, "dependencies": { - "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0" } }, "node_modules/tar-stream": { @@ -11311,25 +11463,25 @@ } }, "node_modules/testcontainers": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", - "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.2.tgz", + "integrity": "sha512-7d+LVd/4YKp/cutiVMLL5cnj/8p8oYELAVRRyNUM4FyUDz1OLQuwW868nDl7Vd1ZAQxzGeCR+F86FlR9Yw9fMA==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.21", + "@types/dockerode": "^3.3.24", "archiver": "^5.3.2", - "async-lock": "^1.4.0", + "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.4", - "docker-compose": "^0.24.2", + "docker-compose": "^0.24.6", "dockerode": "^3.3.5", "get-port": "^5.1.1", "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.4", + "tar-fs": "^3.0.5", "tmp": "^0.2.1" } }, @@ -11435,21 +11587,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/testcontainers/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/testcontainers/node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -11482,15 +11619,12 @@ } }, "node_modules/testcontainers/node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/testcontainers/node_modules/zip-stream": { @@ -12584,16 +12718,54 @@ } }, "node_modules/zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.0.tgz", + "integrity": "sha512-X0WFquRRDtL9HR9hc1OrabOP/VKJEX7gAr2geayt3b7dLgXgSXI6ucC4CphLQP/aQt2GyHIYgmXxtC+dVdghAQ==", "dependencies": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.0", + "readable-stream": "^4.0.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 14" + } + }, + "node_modules/zip-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/zip-stream/node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } } }, @@ -13157,9 +13329,9 @@ } }, "@babel/runtime": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.9.tgz", - "integrity": "sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==", + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz", + "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==", "requires": { "regenerator-runtime": "^0.14.0" } @@ -13314,9 +13486,9 @@ } }, "@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true }, "@golevelup/nestjs-discovery": { @@ -13341,13 +13513,13 @@ } }, "@humanwhocodes/config-array": { - "version": "0.11.13", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz", - "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==", + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", + "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "dev": true, "requires": { - "@humanwhocodes/object-schema": "^2.0.1", - "debug": "^4.1.1", + "@humanwhocodes/object-schema": "^2.0.2", + "debug": "^4.3.1", "minimatch": "^3.0.5" } }, @@ -13358,9 +13530,9 @@ "dev": true }, "@humanwhocodes/object-schema": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz", - "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", + "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "dev": true }, "@img/sharp-darwin-arm64": { @@ -14360,12 +14532,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@testcontainers/postgresql": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.1.tgz", - "integrity": "sha512-2tlrD7vRNdi+nynFCNaGbjTTE7aUNk9Pipcu7PIkPGc8v1AxJdc1BnmI07I1yfW18kOqRi7fo7x4gOlqzAOXJQ==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.7.2.tgz", + "integrity": "sha512-BB1C4SUDhWYY4X9afHtVYXDcYpvtRz7M4CtNEI3gA5A8Zm09msg5NUmhDSJiNAO8/xILv5LHcMJ0NZPBjgM9Jg==", "dev": true, "requires": { - "testcontainers": "^10.7.1" + "testcontainers": "^10.7.2" } }, "@tsconfig/node10": { @@ -14504,9 +14676,9 @@ "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" }, "@types/cookie-parser": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.6.tgz", - "integrity": "sha512-KoooCrD56qlLskXPLGUiJxOMnv5l/8m7cQD2OxJ73NPMhuSz9PmvwRD6EpjDyKBVrdJDdQ4bQK7JFNHnNmax0w==", + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-Fvuyi354Z+uayxzIGCwYTayFKocfV7TuDYZClCdIP9ckhvAu/ixDtCB6qx2TT0FKjPLf1f3P/J1rgf6lPs64mw==", "dev": true, "requires": { "@types/express": "*" @@ -14537,9 +14709,9 @@ } }, "@types/dockerode": { - "version": "3.3.23", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", - "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", + "version": "3.3.24", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.24.tgz", + "integrity": "sha512-679y69OYusf7Fr2HtdjXPUF6hnHxSA9K4EsuagsMuPno/XpJHjXxCOy2I5YL8POnWbzjsQAi0pyKIYM9HSpQog==", "dev": true, "requires": { "@types/docker-modem": "*", @@ -14730,9 +14902,9 @@ } }, "@types/node": { - "version": "20.11.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", - "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", + "version": "20.11.24", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", + "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", "requires": { "undici-types": "~5.26.4" } @@ -14936,16 +15108,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz", - "integrity": "sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.1.0.tgz", + "integrity": "sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/type-utils": "7.0.2", - "@typescript-eslint/utils": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/type-utils": "7.1.0", + "@typescript-eslint/utils": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -14955,54 +15127,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.0.2.tgz", - "integrity": "sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.1.0.tgz", + "integrity": "sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz", - "integrity": "sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.1.0.tgz", + "integrity": "sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2" + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0" } }, "@typescript-eslint/type-utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz", - "integrity": "sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.1.0.tgz", + "integrity": "sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.0.2", - "@typescript-eslint/utils": "7.0.2", + "@typescript-eslint/typescript-estree": "7.1.0", + "@typescript-eslint/utils": "7.1.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.0.2.tgz", - "integrity": "sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.1.0.tgz", + "integrity": "sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz", - "integrity": "sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.1.0.tgz", + "integrity": "sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/visitor-keys": "7.0.2", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/visitor-keys": "7.1.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -15032,27 +15204,27 @@ } }, "@typescript-eslint/utils": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.0.2.tgz", - "integrity": "sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.1.0.tgz", + "integrity": "sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "7.0.2", - "@typescript-eslint/types": "7.0.2", - "@typescript-eslint/typescript-estree": "7.0.2", + "@typescript-eslint/scope-manager": "7.1.0", + "@typescript-eslint/types": "7.1.0", + "@typescript-eslint/typescript-estree": "7.1.0", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz", - "integrity": "sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.1.0.tgz", + "integrity": "sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.0.2", + "@typescript-eslint/types": "7.1.0", "eslint-visitor-keys": "^3.4.1" } }, @@ -15225,6 +15397,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -15360,60 +15540,58 @@ "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" }, "archiver": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-6.0.1.tgz", - "integrity": "sha512-CXGy4poOLBKptiZH//VlWdFuUC1RESbdZjGjILwBuZ73P7WkAUN0htfSfBq/7k6FRFlpu7bg4JOkj1vU9G6jcQ==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.0.tgz", + "integrity": "sha512-R9HM9egs8FfktSqUqyjlKmvF4U+CWNqm/2tlROV+lOFg79MLdT67ae1l3hU47pGy8twSXxHoiefMCh43w0BriQ==", "requires": { - "archiver-utils": "^4.0.1", + "archiver-utils": "^5.0.0", "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", "readdir-glob": "^1.1.2", "tar-stream": "^3.0.0", - "zip-stream": "^5.0.1" + "zip-stream": "^6.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==" + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } } }, "archiver-utils": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-4.0.1.tgz", - "integrity": "sha512-Q4Q99idbvzmgCTEAAhi32BkOyq8iVI5EwdO0PmBDSGIzzjYNdcFn7Q7k3OzbLy4kLUPXfJtG6fO2RjftXbobBg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.1.tgz", + "integrity": "sha512-MMAoLdMvT/nckofX1tCLrf7uJce4jTNkiT6smA2u57AOImc1nce7mR3EDujxL5yv6/MnILuQH4sAsPtDS8kTvg==", "requires": { - "glob": "^8.0.0", + "glob": "^10.0.0", "graceful-fs": "^4.2.0", "lazystream": "^1.0.0", "lodash": "^4.17.15", "normalize-path": "^3.0.0", "readable-stream": "^3.6.0" - }, - "dependencies": { - "brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "requires": { - "balanced-match": "^1.0.0" - } - }, - "glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - } - }, - "minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "requires": { - "brace-expansion": "^2.0.1" - } - } } }, "are-we-there-yet": { @@ -15590,6 +15768,43 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "bare-events": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.2.1.tgz", + "integrity": "sha512-9GYPpsPFvrWBkelIhOhTWtkeZxVxZOdb3VnFTCzlOo3OjvmTvzLoZFUT8kNFACx0vJej6QPney1Cf9BvzCNE/A==", + "dev": true, + "optional": true + }, + "bare-fs": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-2.2.1.tgz", + "integrity": "sha512-+CjmZANQDFZWy4PGbVdmALIwmt33aJg8qTkVjClU6X4WmZkTPBDxRHiBn7fpqEWEfF3AC2io++erpViAIQbSjg==", + "dev": true, + "optional": true, + "requires": { + "bare-events": "^2.0.0", + "bare-os": "^2.0.0", + "bare-path": "^2.0.0", + "streamx": "^2.13.0" + } + }, + "bare-os": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-2.2.0.tgz", + "integrity": "sha512-hD0rOPfYWOMpVirTACt4/nK8mC55La12K5fY1ij8HAdfQakD62M+H4o4tpfKzVGLgRDTuk3vjA4GqGXXCeFbag==", + "dev": true, + "optional": true + }, + "bare-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-2.1.0.tgz", + "integrity": "sha512-DIIg7ts8bdRKwJRJrUMy/PICEaQZaPGZ26lsSx9MJSwIhSrcdHn7/C8W+XmnG/rKi6BaRcz+JO00CjZteybDtw==", + "dev": true, + "optional": true, + "requires": { + "bare-os": "^2.1.0" + } + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -15738,7 +15953,8 @@ "buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true }, "buffer-from": { "version": "1.1.2", @@ -16102,14 +16318,37 @@ "dev": true }, "compress-commons": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-5.0.1.tgz", - "integrity": "sha512-MPh//1cERdLtqwO3pOFLeXtpuai0Y2WCd5AhtKxznqM7WtaMYaOEMSgn45d9D10sIHSfIKE603HlOp8OPGrvag==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.1.tgz", + "integrity": "sha512-l7occIJn8YwlCEbWUCrG6gPms9qnJTCZSaznCa5HaV+yJMH4kM8BDc7q9NyoQuoiB2O6jKgTcTeY462qw6MyHw==", "requires": { "crc-32": "^1.2.0", - "crc32-stream": "^5.0.0", + "crc32-stream": "^6.0.0", "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } } }, "concat-map": { @@ -16233,12 +16472,35 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==" }, "crc32-stream": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-5.0.0.tgz", - "integrity": "sha512-B0EPa1UK+qnpBZpG+7FgPCu0J2ETLpXq09o9BkLkEAhdB6Z61Qo4pJ3JYu0c+Qi+/SAL7QThqnzS06pmSSyZaw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", "requires": { "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } } }, "create-jest": { @@ -16420,9 +16682,9 @@ "dev": true }, "docker-compose": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz", - "integrity": "sha512-2/WLvA7UZ6A2LDLQrYW0idKipmNBWhtfvrn2yzjC5PnHDzuFVj1zAZN6MJxVMKP0zZH8uzAK6OwVZYHGuyCmTw==", + "version": "0.24.6", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.6.tgz", + "integrity": "sha512-VidlUyNzXMaVsuM79sjSvwC4nfojkP2VneL+Zfs538M2XFnffZDhx6veqnz/evCNIYGyz5O+1fgL6+g0NLWTBA==", "dev": true, "requires": { "yaml": "^2.2.2" @@ -16612,16 +16874,16 @@ "dev": true }, "eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -16809,11 +17071,15 @@ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" }, "execa": { "version": "5.1.1", @@ -18465,9 +18731,9 @@ } }, "jose": { - "version": "4.15.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.4.tgz", - "integrity": "sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==" + "version": "4.15.5", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.5.tgz", + "integrity": "sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==" }, "js-tokens": { "version": "4.0.0", @@ -19209,7 +19475,7 @@ "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz", "integrity": "sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==", "requires": { - "jose": "^4.15.4", + "jose": "^4.15.5", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -19621,6 +19887,11 @@ } } }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -20766,12 +21037,13 @@ } }, "tar-fs": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.4.tgz", - "integrity": "sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.5.tgz", + "integrity": "sha512-JOgGAmZyMgbqpLwct7ZV8VzkEB6pxXFBVErLtb+XCOqzc6w1xiWKI9GVd6bwk68EX7eJ4DWmfXVmq8K2ziZTGg==", "dev": true, "requires": { - "mkdirp-classic": "^0.5.2", + "bare-fs": "^2.1.1", + "bare-path": "^2.1.0", "pump": "^3.0.0", "tar-stream": "^3.1.5" } @@ -20869,25 +21141,25 @@ } }, "testcontainers": { - "version": "10.7.1", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.1.tgz", - "integrity": "sha512-JarbT6o7fv1siUts4tGv3wBoYrWKxjla69+5QWG9+bcd4l+ECJ3ikfGD/hpXRmRBsnjzeWyV+tL9oWOBRzk+lA==", + "version": "10.7.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.7.2.tgz", + "integrity": "sha512-7d+LVd/4YKp/cutiVMLL5cnj/8p8oYELAVRRyNUM4FyUDz1OLQuwW868nDl7Vd1ZAQxzGeCR+F86FlR9Yw9fMA==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.21", + "@types/dockerode": "^3.3.24", "archiver": "^5.3.2", - "async-lock": "^1.4.0", + "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.4", - "docker-compose": "^0.24.2", + "docker-compose": "^0.24.6", "dockerode": "^3.3.5", "get-port": "^5.1.1", "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.4", + "tar-fs": "^3.0.5", "tmp": "^0.2.1" }, "dependencies": { @@ -20977,15 +21249,6 @@ "path-is-absolute": "^1.0.0" } }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -21015,13 +21278,10 @@ } }, "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "dev": true, - "requires": { - "rimraf": "^3.0.0" - } + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true }, "zip-stream": { "version": "4.1.1", @@ -21732,13 +21992,36 @@ "dev": true }, "zip-stream": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-5.0.1.tgz", - "integrity": "sha512-UfZ0oa0C8LI58wJ+moL46BDIMgCQbnsb+2PoiJYtonhBsMh2bq1eRBVkvjfVsqbEHd9/EgKPUuL9saSSsec8OA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.0.tgz", + "integrity": "sha512-X0WFquRRDtL9HR9hc1OrabOP/VKJEX7gAr2geayt3b7dLgXgSXI6ucC4CphLQP/aQt2GyHIYgmXxtC+dVdghAQ==", "requires": { - "archiver-utils": "^4.0.1", - "compress-commons": "^5.0.1", - "readable-stream": "^3.6.0" + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.0", + "readable-stream": "^4.0.0" + }, + "dependencies": { + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "requires": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + } + } } } } diff --git a/server/package.json b/server/package.json index d09854ded..da9cb0e44 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.97.0", + "version": "1.98.0", "description": "", "author": "", "private": true, @@ -48,7 +48,7 @@ "@nestjs/websockets": "^10.2.2", "@socket.io/postgres-adapter": "^0.3.1", "@types/picomatch": "^2.3.3", - "archiver": "^6.0.0", + "archiver": "^7.0.0", "async-lock": "^1.4.0", "bcrypt": "^5.1.1", "bullmq": "^4.8.0", diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 860270107..7063cb49a 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -262,16 +262,25 @@ export class AccessCore { } case Permission.LIBRARY_READ: { + if (auth.user.isAdmin) { + return new Set(ids); + } const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids); const isPartner = await this.repository.library.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); return setUnion(isOwner, isPartner); } case Permission.LIBRARY_UPDATE: { + if (auth.user.isAdmin) { + return new Set(ids); + } return await this.repository.library.checkOwnerAccess(auth.user.id, ids); } case Permission.LIBRARY_DELETE: { + if (auth.user.isAdmin) { + return new Set(ids); + } return await this.repository.library.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/domain/activity/activity.spec.ts index 79c466b92..10a4c0725 100644 --- a/server/src/domain/activity/activity.spec.ts +++ b/server/src/domain/activity/activity.spec.ts @@ -11,7 +11,7 @@ describe(ActivityService.name, () => { let accessMock: IAccessRepositoryMock; let activityMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); activityMock = newActivityRepositoryMock(); diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index 10b6dde5e..fa0852d8c 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -23,7 +23,7 @@ describe(AlbumService.name, () => { let jobMock: jest.Mocked; let userMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); albumMock = newAlbumRepositoryMock(); assetMock = newAssetRepositoryMock(); diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts index 3b1858ba1..1b6c754f0 100644 --- a/server/src/domain/album/dto/album-update.dto.ts +++ b/server/src/domain/album/dto/album-update.dto.ts @@ -1,5 +1,5 @@ -import { IsBoolean, IsString } from 'class-validator'; -import { Optional, ValidateUUID } from '../../domain.util'; +import { IsString } from 'class-validator'; +import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; export class UpdateAlbumDto { @Optional() @@ -13,7 +13,6 @@ export class UpdateAlbumDto { @ValidateUUID({ optional: true }) albumThumbnailAssetId?: string; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isActivityEnabled?: boolean; } diff --git a/server/src/domain/album/dto/album.dto.ts b/server/src/domain/album/dto/album.dto.ts index d1fc701a0..b7aad98b5 100644 --- a/server/src/domain/album/dto/album.dto.ts +++ b/server/src/domain/album/dto/album.dto.ts @@ -1,10 +1,6 @@ -import { Transform } from 'class-transformer'; -import { IsBoolean } from 'class-validator'; -import { Optional, toBoolean } from '../../domain.util'; +import { ValidateBoolean } from '../../domain.util'; export class AlbumInfoDto { - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) withoutAssets?: boolean; } diff --git a/server/src/domain/album/dto/get-albums.dto.ts b/server/src/domain/album/dto/get-albums.dto.ts index ce037e189..2628a3fc7 100644 --- a/server/src/domain/album/dto/get-albums.dto.ts +++ b/server/src/domain/album/dto/get-albums.dto.ts @@ -1,13 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsBoolean } from 'class-validator'; -import { Optional, toBoolean, ValidateUUID } from '../../domain.util'; +import { ValidateBoolean, ValidateUUID } from '../../domain.util'; export class GetAlbumsDto { - @Optional() - @IsBoolean() - @Transform(toBoolean) - @ApiProperty() + @ValidateBoolean({ optional: true }) /** * true: only shared albums * false: only non-shared own albums diff --git a/server/src/domain/api-key/api-key.service.spec.ts b/server/src/domain/api-key/api-key.service.spec.ts index f6d650c41..f3b291084 100644 --- a/server/src/domain/api-key/api-key.service.spec.ts +++ b/server/src/domain/api-key/api-key.service.spec.ts @@ -8,7 +8,7 @@ describe(APIKeyService.name, () => { let keyMock: jest.Mocked; let cryptoMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { cryptoMock = newCryptoRepositoryMock(); keyMock = newKeyRepositoryMock(); sut = new APIKeyService(cryptoMock, keyMock); diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 9f077835a..0b8dea717 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -169,7 +169,7 @@ describe(AssetService.name, () => { expect(sut).toBeDefined(); }); - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); communicationMock = newCommunicationRepositoryMock(); @@ -286,27 +286,22 @@ describe(AssetService.name, () => { describe('getMapMarkers', () => { it('should get geo information of assets', async () => { + const asset = assetStub.withLocation; + const marker = { + id: asset.id, + lat: asset.exifInfo!.latitude!, + lon: asset.exifInfo!.longitude!, + city: asset.exifInfo!.city, + state: asset.exifInfo!.state, + country: asset.exifInfo!.country, + }; partnerMock.getAll.mockResolvedValue([]); - assetMock.getMapMarkers.mockResolvedValue( - [assetStub.withLocation].map((asset) => ({ - id: asset.id, - - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - lat: asset.exifInfo!.latitude!, - - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ - lon: asset.exifInfo!.longitude!, - })), - ); + assetMock.getMapMarkers.mockResolvedValue([marker]); const markers = await sut.getMapMarkers(authStub.user1, {}); expect(markers).toHaveLength(1); - expect(markers[0]).toEqual({ - id: assetStub.withLocation.id, - lat: 100, - lon: 100, - }); + expect(markers[0]).toEqual(marker); }); }); diff --git a/server/src/domain/asset/dto/asset-statistics.dto.ts b/server/src/domain/asset/dto/asset-statistics.dto.ts index a53e774f4..c313ccdf4 100644 --- a/server/src/domain/asset/dto/asset-statistics.dto.ts +++ b/server/src/domain/asset/dto/asset-statistics.dto.ts @@ -1,24 +1,16 @@ import { AssetType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsBoolean } from 'class-validator'; -import { Optional, toBoolean } from '../../domain.util'; +import { ValidateBoolean } from '../../domain.util'; import { AssetStats } from '../../repositories'; export class AssetStatsDto { - @IsBoolean() - @Transform(toBoolean) - @Optional() + @ValidateBoolean({ optional: true }) isArchived?: boolean; - @IsBoolean() - @Transform(toBoolean) - @Optional() + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @IsBoolean() - @Transform(toBoolean) - @Optional() + @ValidateBoolean({ optional: true }) isTrashed?: boolean; } diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts index 0244ecd90..8b5c675d8 100644 --- a/server/src/domain/asset/dto/asset.dto.ts +++ b/server/src/domain/asset/dto/asset.dto.ts @@ -1,6 +1,5 @@ import { Type } from 'class-transformer'; import { - IsBoolean, IsDateString, IsInt, IsLatitude, @@ -10,7 +9,7 @@ import { IsString, ValidateIf, } from 'class-validator'; -import { Optional, ValidateUUID } from '../../domain.util'; +import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util'; import { BulkIdsDto } from '../response-dto'; export class DeviceIdDto { @@ -28,23 +27,13 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) => o.latitude !== undefined || o.longitude !== undefined; const ValidateGPS = () => ValidateIf(hasGPS); -export class AssetBulkUpdateDto extends BulkIdsDto { - @Optional() - @IsBoolean() +export class UpdateAssetBase { + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isArchived?: boolean; - @Optional() - @ValidateUUID() - stackParentId?: string; - - @Optional() - @IsBoolean() - removeParent?: boolean; - @Optional() @IsDateString() dateTimeOriginal?: string; @@ -60,32 +49,21 @@ export class AssetBulkUpdateDto extends BulkIdsDto { longitude?: number; } -export class UpdateAssetDto { - @Optional() - @IsBoolean() - isFavorite?: boolean; +export class AssetBulkUpdateDto extends UpdateAssetBase { + @ValidateUUID({ each: true }) + ids!: string[]; - @Optional() - @IsBoolean() - isArchived?: boolean; + @ValidateUUID({ optional: true }) + stackParentId?: string; + @ValidateBoolean({ optional: true }) + removeParent?: boolean; +} + +export class UpdateAssetDto extends UpdateAssetBase { @Optional() @IsString() description?: string; - - @Optional() - @IsDateString() - dateTimeOriginal?: string; - - @ValidateGPS() - @IsLatitude() - @IsNotEmpty() - latitude?: number; - - @ValidateGPS() - @IsLongitude() - @IsNotEmpty() - longitude?: number; } export class RandomAssetsDto { @@ -97,7 +75,6 @@ export class RandomAssetsDto { } export class AssetBulkDeleteDto extends BulkIdsDto { - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) force?: boolean; } diff --git a/server/src/domain/asset/dto/map-marker.dto.ts b/server/src/domain/asset/dto/map-marker.dto.ts index b703d6e73..4fe6c16b8 100644 --- a/server/src/domain/asset/dto/map-marker.dto.ts +++ b/server/src/domain/asset/dto/map-marker.dto.ts @@ -1,34 +1,18 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsBoolean, IsDate } from 'class-validator'; -import { Optional, toBoolean } from '../../domain.util'; +import { ValidateBoolean, ValidateDate } from '../../domain.util'; export class MapMarkerDto { - @ApiProperty() - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isArchived?: boolean; - @ApiProperty() - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @Optional() - @IsDate() - @Type(() => Date) + @ValidateDate({ optional: true }) fileCreatedAfter?: Date; - @Optional() - @IsDate() - @Type(() => Date) + @ValidateDate({ optional: true }) fileCreatedBefore?: Date; - @ApiProperty() - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) withPartners?: boolean; } diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/domain/asset/dto/time-bucket.dto.ts index 849b8713f..597a5de35 100644 --- a/server/src/domain/asset/dto/time-bucket.dto.ts +++ b/server/src/domain/asset/dto/time-bucket.dto.ts @@ -1,7 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional, ValidateUUID, toBoolean } from '../../domain.util'; +import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { ValidateBoolean, ValidateUUID } from '../../domain.util'; import { TimeBucketSize } from '../../repositories'; export class TimeBucketDto { @@ -19,34 +18,23 @@ export class TimeBucketDto { @ValidateUUID({ optional: true }) personId?: string; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isArchived?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isTrashed?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) withStacked?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) withPartners?: boolean; } export class TimeBucketAssetDto extends TimeBucketDto { @IsString() - @IsNotEmpty() timeBucket!: string; } diff --git a/server/src/domain/asset/response-dto/map-marker-response.dto.ts b/server/src/domain/asset/response-dto/map-marker-response.dto.ts index 48c7b0149..f5148883f 100644 --- a/server/src/domain/asset/response-dto/map-marker-response.dto.ts +++ b/server/src/domain/asset/response-dto/map-marker-response.dto.ts @@ -9,4 +9,13 @@ export class MapMarkerResponseDto { @ApiProperty({ format: 'double' }) lon!: number; + + @ApiProperty() + city!: string | null; + + @ApiProperty() + state!: string | null; + + @ApiProperty() + country!: string | null; } diff --git a/server/src/domain/audit/audit.dto.ts b/server/src/domain/audit/audit.dto.ts index d941f9a1d..0f3f04dab 100644 --- a/server/src/domain/audit/audit.dto.ts +++ b/server/src/domain/audit/audit.dto.ts @@ -1,14 +1,13 @@ import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsArray, IsDate, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { Optional, ValidateUUID } from '../domain.util'; +import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; +import { Optional, ValidateDate, ValidateUUID } from '../domain.util'; const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); export class AuditDeletesDto { - @IsDate() - @Type(() => Date) + @ValidateDate() after!: Date; @ApiProperty({ enum: EntityType, enumName: 'EntityType' }) diff --git a/server/src/domain/audit/audit.service.spec.ts b/server/src/domain/audit/audit.service.spec.ts index d2f8bb6bc..861e0edc1 100644 --- a/server/src/domain/audit/audit.service.spec.ts +++ b/server/src/domain/audit/audit.service.spec.ts @@ -31,7 +31,7 @@ describe(AuditService.name, () => { let storageMock: jest.Mocked; let userMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index 359b28a00..214b6748e 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -74,7 +74,7 @@ describe('AuthService', () => { let callbackMock: jest.Mock; let userinfoMock: jest.Mock; - beforeEach(async () => { + beforeEach(() => { callbackMock = jest.fn().mockReturnValue({ access_token: 'access-token' }); userinfoMock = jest.fn().mockResolvedValue({ sub, email }); diff --git a/server/src/domain/database/database.service.spec.ts b/server/src/domain/database/database.service.spec.ts index 703805b06..14464c0cd 100644 --- a/server/src/domain/database/database.service.spec.ts +++ b/server/src/domain/database/database.service.spec.ts @@ -13,7 +13,7 @@ describe(DatabaseService.name, () => { let sut: DatabaseService; let databaseMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { databaseMock = newDatabaseRepositoryMock(); sut = new DatabaseService(databaseMock); @@ -31,7 +31,7 @@ describe(DatabaseService.name, () => { let errorLog: jest.SpyInstance; let warnLog: jest.SpyInstance; - beforeEach(async () => { + beforeEach(() => { fatalLog = jest.spyOn(ImmichLogger.prototype, 'fatal'); errorLog = jest.spyOn(ImmichLogger.prototype, 'error'); warnLog = jest.spyOn(ImmichLogger.prototype, 'warn'); diff --git a/server/src/domain/database/database.service.ts b/server/src/domain/database/database.service.ts index d697d032b..946c6dac8 100644 --- a/server/src/domain/database/database.service.ts +++ b/server/src/domain/database/database.service.ts @@ -1,6 +1,5 @@ import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; -import { QueryFailedError } from 'typeorm'; import { Version, VersionType } from '../domain.constant'; import { DatabaseExtension, @@ -61,7 +60,9 @@ export class DatabaseService { } private async createVectorExtension() { - await this.databaseRepository.createExtension(this.vectorExt).catch(async (error: QueryFailedError) => { + try { + await this.databaseRepository.createExtension(this.vectorExt); + } catch (error) { const otherExt = this.vectorExt === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; this.logger.fatal(` @@ -78,7 +79,7 @@ export class DatabaseService { In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup. `); throw error; - }); + } } private async updateVectorExtension() { diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index 5fdfd7ec5..a079ff6bf 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -1,6 +1,7 @@ -import { applyDecorators } from '@nestjs/common'; +import { ImmichLogger } from '@app/infra/logger'; +import { BadRequestException, applyDecorators } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; +import { Transform } from 'class-transformer'; import { IsArray, IsBoolean, @@ -11,6 +12,7 @@ import { IsUUID, ValidateIf, ValidationOptions, + isDateString, } from 'class-validator'; import { CronJob } from 'cron'; import _ from 'lodash'; @@ -39,14 +41,10 @@ export interface OpenGraphTags { imageUrl?: string; } -export type Options = { - optional?: boolean; - each?: boolean; -}; - export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; -export function ValidateUUID(options?: Options) { +type UUIDOptions = { optional?: boolean; each?: boolean }; +export const ValidateUUID = (options?: UUIDOptions) => { const { optional, each } = { optional: false, each: false, ...options }; return applyDecorators( IsUUID('4', { each }), @@ -54,7 +52,58 @@ export function ValidateUUID(options?: Options) { optional ? Optional() : IsNotEmpty(), each ? IsArray() : IsString(), ); -} +}; + +type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' }; +export const ValidateDate = (options?: DateOptions) => { + const { optional, nullable, format } = { optional: false, nullable: false, format: 'date-time', ...options }; + + const decorators = [ + ApiProperty({ format }), + IsDate(), + optional ? Optional({ nullable: true }) : IsNotEmpty(), + Transform(({ key, value }) => { + if (value === null || value === undefined) { + return value; + } + + if (!isDateString(value)) { + throw new BadRequestException(`${key} must be a date string`); + } + + return new Date(value as string); + }), + ]; + + if (optional) { + decorators.push(Optional({ nullable })); + } + + return applyDecorators(...decorators); +}; + +type BooleanOptions = { optional?: boolean }; +export const ValidateBoolean = (options?: BooleanOptions) => { + const { optional } = { optional: false, ...options }; + const decorators = [ + // ApiProperty(), + IsBoolean(), + Transform(({ value }) => { + if (value == 'true') { + return true; + } else if (value == 'false') { + return false; + } + return value; + }), + ]; + + if (optional) { + decorators.push(Optional()); + } + + return applyDecorators(...decorators); +}; export function validateCronExpression(expression: string) { try { @@ -66,34 +115,7 @@ export function validateCronExpression(expression: string) { return true; } -interface IValue { - value?: string; -} - -export const QueryBoolean = ({ optional }: { optional?: boolean }) => { - const decorators = [IsBoolean(), Transform(toBoolean)]; - if (optional) { - decorators.push(Optional()); - } - return applyDecorators(...decorators); -}; - -export const QueryDate = ({ optional }: { optional?: boolean }) => { - const decorators = [IsDate(), Type(() => Date)]; - if (optional) { - decorators.push(Optional()); - } - return applyDecorators(...decorators); -}; - -export const toBoolean = ({ value }: IValue) => { - if (value == 'true') { - return true; - } else if (value == 'false') { - return false; - } - return value; -}; +type IValue = { value: string }; export const toEmail = ({ value }: IValue) => value?.toLowerCase(); @@ -157,7 +179,7 @@ export type Paginated = Promise>; export async function* usePagination( pageSize: number, - getNextPage: (pagination: PaginationOptions) => Paginated, + getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated, ) { let hasNextPage = true; @@ -252,3 +274,7 @@ export const setIsSuperset = (set: Set, subset: Set): boolean => { export const setIsEqual = (setA: Set, setB: Set): boolean => { return setA.size === setB.size && setIsSuperset(setA, setB); }; + +export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => { + promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack)); +}; diff --git a/server/src/domain/download/download.service.spec.ts b/server/src/domain/download/download.service.spec.ts index f59374d70..fb9ae9567 100644 --- a/server/src/domain/download/download.service.spec.ts +++ b/server/src/domain/download/download.service.spec.ts @@ -34,7 +34,7 @@ describe(DownloadService.name, () => { expect(sut).toBeDefined(); }); - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); storageMock = newStorageRepositoryMock(); @@ -90,7 +90,10 @@ describe(DownloadService.name, () => { }; accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - assetMock.getByIds.mockResolvedValue([assetStub.noResizePath, assetStub.noWebpPath]); + assetMock.getByIds.mockResolvedValue([ + { ...assetStub.noResizePath, id: 'asset-1' }, + { ...assetStub.noWebpPath, id: 'asset-2' }, + ]); storageMock.createZipStream.mockReturnValue(archiveMock); await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ @@ -110,7 +113,33 @@ describe(DownloadService.name, () => { }; accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); - assetMock.getByIds.mockResolvedValue([assetStub.noResizePath, assetStub.noResizePath]); + assetMock.getByIds.mockResolvedValue([ + { ...assetStub.noResizePath, id: 'asset-1' }, + { ...assetStub.noResizePath, id: 'asset-2' }, + ]); + storageMock.createZipStream.mockReturnValue(archiveMock); + + await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ + stream: archiveMock.stream, + }); + + expect(archiveMock.addFile).toHaveBeenCalledTimes(2); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); + expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg'); + }); + + it('should be deterministic', async () => { + const archiveMock = { + addFile: jest.fn(), + finalize: jest.fn(), + stream: new Readable(), + }; + + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); + assetMock.getByIds.mockResolvedValue([ + { ...assetStub.noResizePath, id: 'asset-2' }, + { ...assetStub.noResizePath, id: 'asset-1' }, + ]); storageMock.createZipStream.mockReturnValue(archiveMock); await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1', 'asset-2'] })).resolves.toEqual({ diff --git a/server/src/domain/download/download.service.ts b/server/src/domain/download/download.service.ts index 03bd6fee6..fcad2b6e7 100644 --- a/server/src/domain/download/download.service.ts +++ b/server/src/domain/download/download.service.ts @@ -1,6 +1,6 @@ import { AssetEntity } from '@app/infra/entities'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { extname } from 'node:path'; +import { parse } from 'node:path'; import { AccessCore, Permission } from '../access'; import { AssetIdsDto } from '../asset'; import { AuthDto } from '../auth'; @@ -81,15 +81,23 @@ export class DownloadService { const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); + const assetMap = new Map(assets.map((asset) => [asset.id, asset])); const paths: Record = {}; - for (const { originalPath, originalFileName } of assets) { - const extension = extname(originalPath); - let filename = `${originalFileName}${extension}`; + for (const assetId of dto.assetIds) { + const asset = assetMap.get(assetId); + if (!asset) { + continue; + } + + const { originalPath, originalFileName } = asset; + + let filename = originalFileName; const count = paths[filename] || 0; paths[filename] = count + 1; if (count !== 0) { - filename = `${originalFileName}+${count}${extension}`; + const parsedFilename = parse(originalFileName); + filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`; } zip.addFile(originalPath, filename); @@ -107,9 +115,7 @@ export class DownloadService { const assetIds = dto.assetIds; await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); const assets = await this.assetRepository.getByIds(assetIds); - return (async function* () { - yield assets; - })(); + return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); } if (dto.albumId) { diff --git a/server/src/domain/job/job.dto.ts b/server/src/domain/job/job.dto.ts index db0bd8dc4..87be1332f 100644 --- a/server/src/domain/job/job.dto.ts +++ b/server/src/domain/job/job.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum, IsNotEmpty } from 'class-validator'; -import { Optional } from '../domain.util'; +import { IsEnum, IsNotEmpty } from 'class-validator'; +import { ValidateBoolean } from '../domain.util'; import { JobCommand, QueueName } from './job.constants'; export class JobIdParamDto { @@ -16,8 +16,7 @@ export class JobCommandDto { @ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' }) command!: JobCommand; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) force!: boolean; } diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/domain/job/job.service.spec.ts index 5a4d26b3c..9fe38a2ff 100644 --- a/server/src/domain/job/job.service.spec.ts +++ b/server/src/domain/job/job.service.spec.ts @@ -37,7 +37,7 @@ describe(JobService.name, () => { let jobMock: jest.Mocked; let personMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); communicationMock = newCommunicationRepositoryMock(); diff --git a/server/src/domain/library/library.dto.ts b/server/src/domain/library/library.dto.ts index b57d56e7b..b11bc9998 100644 --- a/server/src/domain/library/library.dto.ts +++ b/server/src/domain/library/library.dto.ts @@ -1,7 +1,7 @@ import { LibraryEntity, LibraryType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { Optional, ValidateUUID } from '../domain.util'; +import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { Optional, ValidateBoolean, ValidateUUID } from '../domain.util'; export class CreateLibraryDto { @IsEnum(LibraryType) @@ -16,8 +16,7 @@ export class CreateLibraryDto { @IsNotEmpty() name?: string; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isVisible?: boolean; @Optional() @@ -34,8 +33,7 @@ export class CreateLibraryDto { @ArrayMaxSize(128) exclusionPatterns?: string[]; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isWatched?: boolean; } @@ -45,8 +43,7 @@ export class UpdateLibraryDto { @IsNotEmpty() name?: string; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isVisible?: boolean; @Optional() @@ -102,13 +99,11 @@ export class LibrarySearchDto { } export class ScanLibraryDto { - @IsBoolean() - @Optional() + @ValidateBoolean({ optional: true }) refreshModifiedFiles?: boolean; - @IsBoolean() - @Optional() - refreshAllFiles?: boolean = false; + @ValidateBoolean({ optional: true }) + refreshAllFiles?: boolean; } export class SearchLibraryDto { diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index ba1dd8374..a44624c43 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -9,24 +9,26 @@ import { newAccessRepositoryMock, newAssetRepositoryMock, newCryptoRepositoryMock, + newDatabaseRepositoryMock, newJobRepositoryMock, newLibraryRepositoryMock, newStorageRepositoryMock, newSystemConfigRepositoryMock, - newUserRepositoryMock, systemConfigStub, userStub, } from '@test'; +import { when } from 'jest-when'; import { Stats } from 'node:fs'; import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job'; import { IAssetRepository, ICryptoRepository, + IDatabaseRepository, IJobRepository, ILibraryRepository, IStorageRepository, ISystemConfigRepository, - IUserRepository, + StorageEventType, } from '../repositories'; import { SystemConfigCore } from '../system-config/system-config.core'; import { mapLibrary } from './library.dto'; @@ -39,23 +41,23 @@ describe(LibraryService.name, () => { let assetMock: jest.Mocked; let configMock: jest.Mocked; let cryptoMock: jest.Mocked; - let userMock: jest.Mocked; let jobMock: jest.Mocked; let libraryMock: jest.Mocked; let storageMock: jest.Mocked; + let databaseMock: jest.Mocked; beforeEach(() => { accessMock = newAccessRepositoryMock(); configMock = newSystemConfigRepositoryMock(); libraryMock = newLibraryRepositoryMock(); - userMock = newUserRepositoryMock(); assetMock = newAssetRepositoryMock(); jobMock = newJobRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); storageMock = newStorageRepositoryMock(); + databaseMock = newDatabaseRepositoryMock(); // Always validate owner access for library. - accessMock.library.checkOwnerAccess.mockImplementation(async (_, libraryIds) => libraryIds); + accessMock.library.checkOwnerAccess.mockImplementation((_, libraryIds) => Promise.resolve(libraryIds)); sut = new LibraryService( accessMock, @@ -65,8 +67,10 @@ describe(LibraryService.name, () => { jobMock, libraryMock, storageMock, - userMock, + databaseMock, ); + + databaseMock.tryLock.mockResolvedValue(true); }); it('should work', () => { @@ -106,19 +110,13 @@ describe(LibraryService.name, () => { configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - libraryMock.get.mockImplementation(async (id) => { - switch (id) { - case libraryStub.externalLibraryWithImportPaths1.id: { - return libraryStub.externalLibraryWithImportPaths1; - } - case libraryStub.externalLibraryWithImportPaths2.id: { - return libraryStub.externalLibraryWithImportPaths2; - } - default: { - return null; - } - } - }); + when(libraryMock.get) + .calledWith(libraryStub.externalLibraryWithImportPaths1.id) + .mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); + + when(libraryMock.get) + .calledWith(libraryStub.externalLibraryWithImportPaths2.id) + .mockResolvedValue(libraryStub.externalLibraryWithImportPaths2); await sut.init(); @@ -130,13 +128,22 @@ describe(LibraryService.name, () => { ); }); - it('should not initialize when watching is disabled', async () => { + it('should not initialize watcher when watching is disabled', async () => { configMock.load.mockResolvedValue(systemConfigStub.libraryWatchDisabled); await sut.init(); expect(storageMock.watch).not.toHaveBeenCalled(); }); + + it('should not initialize watcher when lock is taken', async () => { + configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); + databaseMock.tryLock.mockResolvedValue(false); + + await sut.init(); + + expect(storageMock.watch).not.toHaveBeenCalled(); + }); }); describe('handleQueueAssetRefresh', () => { @@ -149,8 +156,8 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']); + assetMock.getPathsNotInLibrary.mockResolvedValue(['/data/user1/photo.jpg']); assetMock.getByLibraryId.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.admin); await sut.handleQueueAssetRefresh(mockLibraryJob); @@ -177,7 +184,6 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); storageMock.crawl.mockResolvedValue(['/data/user1/photo.jpg']); assetMock.getByLibraryId.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.admin); await sut.handleQueueAssetRefresh(mockLibraryJob); @@ -228,7 +234,6 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); storageMock.crawl.mockResolvedValue([]); assetMock.getByLibraryId.mockResolvedValue([]); - userMock.get.mockResolvedValue(userStub.externalPathRoot); await sut.handleQueueAssetRefresh(mockLibraryJob); @@ -244,7 +249,6 @@ describe(LibraryService.name, () => { beforeEach(() => { mockUser = userStub.admin; - userMock.get.mockResolvedValue(mockUser); storageMock.stat.mockResolvedValue({ size: 100, @@ -1171,7 +1175,9 @@ describe(LibraryService.name, () => { it('should handle a new file event', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); + storageMock.watch.mockImplementation( + makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }), + ); await sut.watchAll(); @@ -1192,7 +1198,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }), + makeMockWatcher({ items: [{ event: StorageEventType.CHANGE, value: '/foo/photo.jpg' }] }), ); await sut.watchAll(); @@ -1215,7 +1221,7 @@ describe(LibraryService.name, () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external); storageMock.watch.mockImplementation( - makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }), + makeMockWatcher({ items: [{ event: StorageEventType.UNLINK, value: '/foo/photo.jpg' }] }), ); await sut.watchAll(); @@ -1229,17 +1235,19 @@ describe(LibraryService.name, () => { libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); storageMock.watch.mockImplementation( makeMockWatcher({ - items: [{ event: 'error', value: 'Error!' }], + items: [{ event: StorageEventType.ERROR, value: 'Error!' }], }), ); - await sut.watchAll(); + await expect(sut.watchAll()).rejects.toThrow('Error!'); }); it('should ignore unknown extensions', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]); - storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] })); + storageMock.watch.mockImplementation( + makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }), + ); await sut.watchAll(); @@ -1249,7 +1257,9 @@ describe(LibraryService.name, () => { it('should ignore excluded paths', async () => { libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); - storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/dir1/photo.txt' }] })); + storageMock.watch.mockImplementation( + makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/dir1/photo.txt' }] }), + ); await sut.watchAll(); @@ -1259,7 +1269,9 @@ describe(LibraryService.name, () => { it('should ignore excluded paths without case sensitivity', async () => { libraryMock.get.mockResolvedValue(libraryStub.patternPath); libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]); - storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/DIR1/photo.txt' }] })); + storageMock.watch.mockImplementation( + makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/DIR1/photo.txt' }] }), + ); await sut.watchAll(); @@ -1268,7 +1280,7 @@ describe(LibraryService.name, () => { }); }); - describe('tearDown', () => { + describe('teardown', () => { it('should tear down all watchers', async () => { libraryMock.getAll.mockResolvedValue([ libraryStub.externalLibraryWithImportPaths1, @@ -1278,25 +1290,19 @@ describe(LibraryService.name, () => { configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - libraryMock.get.mockImplementation(async (id) => { - switch (id) { - case libraryStub.externalLibraryWithImportPaths1.id: { - return libraryStub.externalLibraryWithImportPaths1; - } - case libraryStub.externalLibraryWithImportPaths2.id: { - return libraryStub.externalLibraryWithImportPaths2; - } - default: { - return null; - } - } - }); + when(libraryMock.get) + .calledWith(libraryStub.externalLibraryWithImportPaths1.id) + .mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); + + when(libraryMock.get) + .calledWith(libraryStub.externalLibraryWithImportPaths2.id) + .mockResolvedValue(libraryStub.externalLibraryWithImportPaths2); const mockClose = jest.fn(); storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); await sut.init(); - await sut.unwatchAll(); + await sut.teardown(); expect(mockClose).toHaveBeenCalledTimes(2); }); @@ -1304,9 +1310,8 @@ describe(LibraryService.name, () => { describe('handleDeleteLibrary', () => { it('should not delete a nonexistent library', async () => { - libraryMock.get.mockImplementation(async () => { - return null; - }); + libraryMock.get.mockResolvedValue(null); + libraryMock.getAssetIds.mockResolvedValue([]); libraryMock.delete.mockImplementation(async () => {}); diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 4d8912685..c74e97ea3 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -9,18 +9,20 @@ import picomatch from 'picomatch'; import { AccessCore, Permission } from '../access'; import { AuthDto } from '../auth'; import { mimeTypes } from '../domain.constant'; -import { usePagination, validateCronExpression } from '../domain.util'; +import { handlePromiseError, usePagination, validateCronExpression } from '../domain.util'; import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job'; import { + DatabaseLock, IAccessRepository, IAssetRepository, ICryptoRepository, + IDatabaseRepository, IJobRepository, ILibraryRepository, IStorageRepository, ISystemConfigRepository, - IUserRepository, + StorageEventType, WithProperty, } from '../repositories'; import { SystemConfigCore } from '../system-config'; @@ -43,7 +45,8 @@ export class LibraryService extends EventEmitter { private access: AccessCore; private configCore: SystemConfigCore; private watchLibraries = false; - private watchers: Record void> = {}; + private watchLock = false; + private watchers: Record Promise> = {}; constructor( @Inject(IAccessRepository) accessRepository: IAccessRepository, @@ -53,7 +56,7 @@ export class LibraryService extends EventEmitter { @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ILibraryRepository) private repository: ILibraryRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, - @Inject(IUserRepository) private userRepository: IUserRepository, + @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, ) { super(); this.access = AccessCore.create(accessRepository); @@ -68,12 +71,23 @@ export class LibraryService extends EventEmitter { async init() { const config = await this.configCore.getConfig(); + const { watch, scan } = config.library; - this.watchLibraries = watch.enabled; + + // This ensures that library watching only occurs in one microservice + // TODO: we could make the lock be per-library instead of global + this.watchLock = await this.databaseRepository.tryLock(DatabaseLock.LibraryWatch); + + this.watchLibraries = this.watchLock && watch.enabled; + this.jobRepository.addCronJob( 'libraryScan', scan.cronExpression, - () => this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }), + () => + handlePromiseError( + this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force: false } }), + this.logger, + ), scan.enabled, ); @@ -81,12 +95,13 @@ export class LibraryService extends EventEmitter { await this.watchAll(); } - this.configCore.config$.subscribe(async ({ library }) => { + this.configCore.config$.subscribe(({ library }) => { this.jobRepository.updateCronJob('libraryScan', library.scan.cronExpression, library.scan.enabled); if (library.watch.enabled !== this.watchLibraries) { + // Watch configuration changed, update accordingly this.watchLibraries = library.watch.enabled; - await (this.watchLibraries ? this.watchAll() : this.unwatchAll()); + handlePromiseError(this.watchLibraries ? this.watchAll() : this.unwatchAll(), this.logger); } }); } @@ -124,32 +139,41 @@ export class LibraryService extends EventEmitter { }, { onReady: () => _resolve(), - onAdd: async (path) => { - 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); - } - this.emit('add', path); + onAdd: (path) => { + 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); + } + this.emit(StorageEventType.ADD, path); + }; + return handlePromiseError(handler(), this.logger); }, - onChange: async (path) => { - this.logger.debug(`Detected file change for ${path} in library ${library.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); - } - this.emit('change', path); + onChange: (path) => { + const handler = async () => { + this.logger.debug(`Detected file change for ${path} in library ${library.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); + } + this.emit(StorageEventType.CHANGE, path); + }; + return handlePromiseError(handler(), this.logger); }, - onUnlink: async (path) => { - 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.save({ id: asset.id, isOffline: true }); - } - this.emit('unlink', path); + onUnlink: (path) => { + 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.save({ id: asset.id, isOffline: true }); + } + this.emit(StorageEventType.UNLINK, path); + }; + return handlePromiseError(handler(), this.logger); }, onError: (error) => { - // TODO: should we log, or throw an exception? this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`); + this.emit(StorageEventType.ERROR, error); }, }, ); @@ -167,13 +191,25 @@ export class LibraryService extends EventEmitter { } } - async unwatchAll() { + async teardown() { + await this.unwatchAll(); + } + + private async unwatchAll() { + if (!this.watchLock) { + return false; + } + for (const id in this.watchers) { await this.unwatch(id); } } async watchAll() { + if (!this.watchLock) { + return false; + } + const libraries = await this.repository.getAll(false, LibraryType.EXTERNAL); for (const library of libraries) { @@ -254,7 +290,7 @@ export class LibraryService extends EventEmitter { this.logger.log(`Creating ${dto.type} library for user ${auth.user.name}`); - if (dto.type === LibraryType.EXTERNAL && this.watchLibraries) { + if (dto.type === LibraryType.EXTERNAL) { await this.watch(library.id); } @@ -608,29 +644,18 @@ export class LibraryService extends EventEmitter { pathsToCrawl: validImportPaths, exclusionPatterns: library.exclusionPatterns, }); - const crawledAssetPaths = rawPaths.map((filePath) => path.normalize(filePath)); this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`); - const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]); - const onlineFiles = new Set(crawledAssetPaths); - const offlineAssetIds = assetsInLibrary - .filter((asset) => !onlineFiles.has(asset.originalPath)) - .filter((asset) => !asset.isOffline) - .map((asset) => asset.id); - this.logger.debug(`Marking ${offlineAssetIds.length} assets as offline`); - await this.assetRepository.updateAll(offlineAssetIds, { isOffline: true }); + await this.assetRepository.updateOfflineLibraryAssets(library.id, crawledAssetPaths); if (crawledAssetPaths.length > 0) { let filteredPaths: string[] = []; if (job.refreshAllFiles || job.refreshModifiedFiles) { filteredPaths = crawledAssetPaths; } else { - const onlinePathsInLibrary = new Set( - assetsInLibrary.filter((asset) => !asset.isOffline).map((asset) => asset.originalPath), - ); - filteredPaths = crawledAssetPaths.filter((assetPath) => !onlinePathsInLibrary.has(assetPath)); + filteredPaths = await this.assetRepository.getPathsNotInLibrary(library.id, crawledAssetPaths); this.logger.debug(`Will import ${filteredPaths.length} new asset(s)`); } diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index 094401637..244978d09 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -48,7 +48,7 @@ describe(MediaService.name, () => { let storageMock: jest.Mocked; let cryptoMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); jobMock = newJobRepositoryMock(); diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index 6eafc176b..3da9ba371 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -56,7 +56,7 @@ describe(MetadataService.name, () => { let databaseMock: jest.Mocked; let sut: MetadataService; - beforeEach(async () => { + beforeEach(() => { albumMock = newAlbumRepositoryMock(); assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 562568adf..7d2248511 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { Duration } from 'luxon'; import { constants } from 'node:fs/promises'; import { Subscription } from 'rxjs'; -import { usePagination } from '../domain.util'; +import { handlePromiseError, usePagination } from '../domain.util'; import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job'; import { ClientEvent, @@ -124,7 +124,7 @@ export class MetadataService { async init() { if (!this.subscription) { - this.subscription = this.configCore.config$.subscribe(() => this.init()); + this.subscription = this.configCore.config$.subscribe(() => handlePromiseError(this.init(), this.logger)); } const { reverseGeocoding } = await this.configCore.getConfig(); diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/domain/partner/partner.service.spec.ts index 2bc5f3ca9..6e9c10c5c 100644 --- a/server/src/domain/partner/partner.service.spec.ts +++ b/server/src/domain/partner/partner.service.spec.ts @@ -49,7 +49,7 @@ describe(PartnerService.name, () => { let partnerMock: jest.Mocked; let accessMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { partnerMock = newPartnerRepositoryMock(); sut = new PartnerService(partnerMock, accessMock); }); diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index b8ad8f045..a00971c6b 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -1,11 +1,11 @@ import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; +import { Type } from 'class-transformer'; +import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator'; import { AuthDto } from '../auth'; -import { Optional, ValidateUUID, toBoolean } from '../domain.util'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../domain.util'; -export class PersonUpdateDto { +export class PersonCreateDto { /** * Person name. */ @@ -17,25 +17,24 @@ export class PersonUpdateDto { * Person date of birth. * Note: the mobile app cannot currently set the birth date to null. */ - @Optional({ nullable: true }) - @IsDate() - @Type(() => Date) - @ApiProperty({ format: 'date' }) + @MaxDate(() => new Date(), { message: 'Birth date cannot be in the future' }) + @ValidateDate({ optional: true, nullable: true, format: 'date' }) birthDate?: Date | null; + /** + * Person visibility + */ + @ValidateBoolean({ optional: true }) + isHidden?: boolean; +} + +export class PersonUpdateDto extends PersonCreateDto { /** * Asset is used to get the feature face thumbnail. */ @Optional() @IsString() featureFaceAssetId?: string; - - /** - * Person visibility - */ - @Optional() - @IsBoolean() - isHidden?: boolean; } export class PeopleUpdateDto { @@ -60,9 +59,8 @@ export class MergePersonDto { } export class PersonSearchDto { - @IsBoolean() - @Transform(toBoolean) - withHidden?: boolean = false; + @ValidateBoolean({ optional: true }) + withHidden?: boolean; } export class PersonResponseDto { diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index ffda9034b..191356d2c 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -80,7 +80,7 @@ describe(PersonService.name, () => { let cryptoMock: jest.Mocked; let sut: PersonService; - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); @@ -227,8 +227,7 @@ describe(PersonService.name, () => { }); it('should throw an error when personId is invalid', async () => { - personMock.getById.mockResolvedValue(null); - accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set()); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); @@ -237,20 +236,17 @@ describe(PersonService.name, () => { }); it("should update a person's name", async () => { - personMock.getById.mockResolvedValue(personStub.noName); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); - expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' }); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's date of birth", async () => { - personMock.getById.mockResolvedValue(personStub.noBirthDate); personMock.update.mockResolvedValue(personStub.withBirthDate); personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); @@ -262,71 +258,24 @@ describe(PersonService.name, () => { thumbnailPath: '/path/to/thumbnail.jpg', isHidden: false, }); - expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(jobMock.queue).not.toHaveBeenCalled(); expect(jobMock.queueAll).not.toHaveBeenCalled(); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); - it('should throw BadRequestException if birthDate is in the future', async () => { - personMock.getById.mockResolvedValue(personStub.noBirthDate); - personMock.update.mockResolvedValue(personStub.withBirthDate); - personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - - const futureDate = new Date(); - futureDate.setMinutes(futureDate.getMinutes() + 1); // Set birthDate to one minute in the future - - await expect(sut.update(authStub.admin, 'person-1', { birthDate: futureDate })).rejects.toThrow( - new BadRequestException('Date of birth cannot be in the future'), - ); - }); - - it('should not throw an error if birthdate is in the past', async () => { - const pastDate = new Date(); - pastDate.setMinutes(pastDate.getMinutes() - 1); // Set birthDate to one minute in the past - - personMock.getById.mockResolvedValue(personStub.noBirthDate); - personMock.update.mockResolvedValue({ ...personStub.withBirthDate, birthDate: pastDate }); - personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - - const result = await sut.update(authStub.admin, 'person-1', { birthDate: pastDate }); - - expect(result.birthDate).toEqual(pastDate); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: pastDate }); - }); - - it('should not throw an error if birthdate is today', async () => { - const today = new Date(); // Set birthDate to now() - - personMock.getById.mockResolvedValue(personStub.noBirthDate); - personMock.update.mockResolvedValue({ ...personStub.withBirthDate, birthDate: today }); - personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); - - const result = await sut.update(authStub.admin, 'person-1', { birthDate: today }); - - expect(result.birthDate).toEqual(today); - expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: today }); - }); - it('should update a person visibility', async () => { - personMock.getById.mockResolvedValue(personStub.hidden); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); - expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false }); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); it("should update a person's thumbnailPath", async () => { - personMock.getById.mockResolvedValue(personStub.withName); personMock.update.mockResolvedValue(personStub.withName); personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id])); @@ -336,7 +285,6 @@ describe(PersonService.name, () => { sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }), ).resolves.toEqual(responseDto); - expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id }); expect(personMock.getFacesByIds).toHaveBeenCalledWith([ { @@ -362,12 +310,11 @@ describe(PersonService.name, () => { describe('updateAll', () => { it('should throw an error when personId is invalid', async () => { - personMock.getById.mockResolvedValue(null); - accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set()); - await expect( - sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }), - ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]); + await expect(sut.updateAll(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] })).resolves.toEqual([ + { error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }, + ]); expect(personMock.update).not.toHaveBeenCalled(); expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1'])); }); @@ -488,10 +435,10 @@ describe(PersonService.name, () => { describe('createPerson', () => { it('should create a new person', async () => { personMock.create.mockResolvedValue(personStub.primaryPerson); - personMock.getFaceById.mockResolvedValue(faceStub.face1); - accessMock.person.checkFaceOwnerAccess.mockResolvedValue(new Set([faceStub.face1.id])); - await expect(sut.createPerson(authStub.admin)).resolves.toBe(personStub.primaryPerson); + await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson); + + expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id }); }); }); @@ -709,7 +656,7 @@ describe(PersonService.name, () => { }, { enabled: true, - maxDistance: 0.6, + maxDistance: 0.5, minScore: 0.7, minFaces: 3, modelName: 'buffalo_l', diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 6300cc743..235867314 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -36,6 +36,7 @@ import { MergePersonDto, PeopleResponseDto, PeopleUpdateDto, + PersonCreateDto, PersonResponseDto, PersonSearchDto, PersonStatisticsResponseDto, @@ -91,10 +92,6 @@ export class PersonService { }; } - createPerson(auth: AuthDto): Promise { - return this.repository.create({ ownerId: auth.user.id }); - } - async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); const person = await this.findOrFail(personId); @@ -199,21 +196,21 @@ export class PersonService { return assets.map((asset) => mapAsset(asset)); } + create(auth: AuthDto, dto: PersonCreateDto): Promise { + return this.repository.create({ + ownerId: auth.user.id, + name: dto.name, + birthDate: dto.birthDate, + isHidden: dto.isHidden, + }); + } + async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); - let person = await this.findOrFail(id); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; - - // Check if the birthDate is in the future - if (birthDate && new Date(birthDate) > new Date()) { - throw new BadRequestException('Date of birth cannot be in the future'); - } - - if (name !== undefined || birthDate !== undefined || isHidden !== undefined) { - person = await this.repository.update({ id, name, birthDate, isHidden }); - } - + // TODO: set by faceId directly + let faceId: string | undefined = undefined; if (assetId) { await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); @@ -221,14 +218,19 @@ export class PersonService { throw new BadRequestException('Invalid assetId for feature face'); } - person = await this.repository.update({ id, faceAssetId: face.id }); + faceId = face.id; + } + + const person = await this.repository.update({ id, faceAssetId: faceId, name, birthDate, isHidden }); + + if (assetId) { await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } }); } return mapPerson(person); } - async updatePeople(auth: AuthDto, dto: PeopleUpdateDto): Promise { + async updateAll(auth: AuthDto, dto: PeopleUpdateDto): Promise { const results: BulkIdResponseDto[] = []; for (const person of dto.people) { try { diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 7a2941ab9..dd5e76577 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -1,4 +1,9 @@ -import { AssetSearchOneToOneRelationOptions, AssetSearchOptions, SearchExploreItem } from '@app/domain'; +import { + AssetSearchOneToOneRelationOptions, + AssetSearchOptions, + ReverseGeocodeResult, + SearchExploreItem, +} from '@app/domain'; import { AssetEntity, AssetJobStatusEntity, AssetType, ExifEntity } from '@app/infra/entities'; import { FindOptionsRelations, FindOptionsSelect } from 'typeorm'; import { Paginated, PaginationOptions } from '../domain.util'; @@ -25,7 +30,7 @@ export interface MapMarkerSearchOptions { fileCreatedAfter?: Date; } -export interface MapMarker { +export interface MapMarker extends ReverseGeocodeResult { id: string; lat: number; lon: number; @@ -131,6 +136,8 @@ export interface IAssetRepository { getLastUpdatedAssetForAlbumId(albumId: string): Promise; getByLibraryId(libraryIds: string[]): Promise; getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; + getPathsNotInLibrary(libraryId: string, originalPaths: string[]): Promise; + updateOfflineLibraryAssets(libraryId: string, originalPaths: string[]): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; getAllByFileCreationDate( diff --git a/server/src/domain/repositories/communication.repository.ts b/server/src/domain/repositories/communication.repository.ts index daf0aef0a..4a3bc552c 100644 --- a/server/src/domain/repositories/communication.repository.ts +++ b/server/src/domain/repositories/communication.repository.ts @@ -34,7 +34,7 @@ export interface ClientEventMap { [ClientEvent.NEW_RELEASE]: ReleaseNotification; } -export type OnConnectCallback = (userId: string) => Promise; +export type OnConnectCallback = (userId: string) => void | Promise; export type OnServerEventCallback = () => Promise; export interface ICommunicationRepository { diff --git a/server/src/domain/repositories/database.repository.ts b/server/src/domain/repositories/database.repository.ts index d32939fe6..55911e7ce 100644 --- a/server/src/domain/repositories/database.repository.ts +++ b/server/src/domain/repositories/database.repository.ts @@ -19,6 +19,7 @@ export enum DatabaseLock { Migrations = 200, StorageTemplateMigration = 420, CLIPDimSize = 512, + LibraryWatch = 1337, } export const extName: Record = { @@ -46,6 +47,7 @@ export interface IDatabaseRepository { shouldReindex(name: VectorIndex): Promise; runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise; withLock(lock: DatabaseLock, callback: () => Promise): Promise; + tryLock(lock: DatabaseLock): Promise; isBusy(lock: DatabaseLock): boolean; wait(lock: DatabaseLock): Promise; } diff --git a/server/src/domain/repositories/search.repository.ts b/server/src/domain/repositories/search.repository.ts index c9fec3cf7..10182a44e 100644 --- a/server/src/domain/repositories/search.repository.ts +++ b/server/src/domain/repositories/search.repository.ts @@ -92,12 +92,12 @@ export interface SearchStatusOptions { export interface SearchOneToOneRelationOptions { withExif?: boolean; withSmartInfo?: boolean; + withStacked?: boolean; } export interface SearchRelationOptions extends SearchOneToOneRelationOptions { withFaces?: boolean; withPeople?: boolean; - withStacked?: boolean; } export interface SearchDateOptions { diff --git a/server/src/domain/repositories/storage.repository.ts b/server/src/domain/repositories/storage.repository.ts index c88095b17..f4f8cab7b 100644 --- a/server/src/domain/repositories/storage.repository.ts +++ b/server/src/domain/repositories/storage.repository.ts @@ -31,6 +31,14 @@ export interface WatchEvents { onError(error: Error): void; } +export enum StorageEventType { + READY = 'ready', + ADD = 'add', + CHANGE = 'change', + UNLINK = 'unlink', + ERROR = 'error', +} + export interface IStorageRepository { createZipStream(): ImmichZipStream; createReadStream(filepath: string, mimeType?: string | null): Promise; @@ -47,6 +55,6 @@ export interface IStorageRepository { crawl(crawlOptions: CrawlOptionsDto): Promise; copyFile(source: string, target: string): Promise; rename(source: string, target: string): Promise; - watch(paths: string[], options: WatchOptions, events: Partial): () => void; + watch(paths: string[], options: WatchOptions, events: Partial): () => Promise; utimes(filepath: string, atime: Date, mtime: Date): Promise; } diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index c529f6887..9fa7d8e8b 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -1,9 +1,9 @@ import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; import { AssetType, GeodataPlacesEntity } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; -import { Optional, QueryBoolean, QueryDate, ValidateUUID, toBoolean } from '../../domain.util'; +import { Type } from 'class-transformer'; +import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../../domain.util'; class BaseSearchDto { @ValidateUUID({ optional: true }) @@ -19,62 +19,62 @@ class BaseSearchDto { @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) type?: AssetType; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isArchived?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) @ApiProperty({ default: false }) withArchived?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isEncoded?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isExternal?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isMotion?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isOffline?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isReadOnly?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isVisible?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) withDeleted?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) withExif?: boolean; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) createdBefore?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) createdAfter?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) updatedBefore?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) updatedAfter?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) trashedBefore?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) trashedAfter?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) takenBefore?: Date; - @QueryDate({ optional: true }) + @ValidateDate({ optional: true }) takenAfter?: Date; @IsString() @@ -120,10 +120,10 @@ class BaseSearchDto { @Optional() size?: number; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) isNotInAlbum?: boolean; - @Optional() + @ValidateUUID({ each: true, optional: true }) personIds?: string[]; } @@ -141,10 +141,10 @@ export class MetadataSearchDto extends BaseSearchDto { @Optional() checksum?: string; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) withStacked?: boolean; - @QueryBoolean({ optional: true }) + @ValidateBoolean({ optional: true }) withPeople?: boolean; @IsString() @@ -197,34 +197,24 @@ export class SearchDto { @Optional() query?: string; - @IsBoolean() - @Optional() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) smart?: boolean; /** @deprecated */ - @IsBoolean() - @Optional() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) clip?: boolean; @IsEnum(AssetType) @Optional() type?: AssetType; - @IsBoolean() - @Optional() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) recent?: boolean; - @IsBoolean() - @Optional() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) motion?: boolean; - @IsBoolean() - @Optional() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) withArchived?: boolean; @IsInt() @@ -252,9 +242,7 @@ export class SearchPeopleDto { @IsNotEmpty() name!: string; - @IsBoolean() - @Transform(toBoolean) - @Optional() + @ValidateBoolean({ optional: true }) withHidden?: boolean; } diff --git a/server/src/domain/search/search.service.ts b/server/src/domain/search/search.service.ts index 5b5639998..00c5e883e 100644 --- a/server/src/domain/search/search.service.ts +++ b/server/src/domain/search/search.service.ts @@ -1,5 +1,4 @@ import { AssetEntity } from '@app/infra/entities'; -import { ImmichLogger } from '@app/infra/logger'; import { Inject, Injectable } from '@nestjs/common'; import { AssetOrder, AssetResponseDto, mapAsset } from '../asset'; import { AuthDto } from '../auth'; @@ -30,7 +29,6 @@ import { SearchResponseDto } from './response-dto'; @Injectable() export class SearchService { - private logger = new ImmichLogger(SearchService.name); private configCore: SystemConfigCore; constructor( @@ -181,7 +179,7 @@ export class SearchService { return userIds; } - private async mapResponse(assets: AssetEntity[], nextPage: string | null): Promise { + private mapResponse(assets: AssetEntity[], nextPage: string | null): SearchResponseDto { return { albums: { total: 0, count: 0, items: [], facets: [] }, assets: { diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/domain/server-info/server-info.dto.ts index b3ef426da..99d4f1566 100644 --- a/server/src/domain/server-info/server-info.dto.ts +++ b/server/src/domain/server-info/server-info.dto.ts @@ -88,6 +88,8 @@ export class ServerConfigDto { loginPageMessage!: string; @ApiProperty({ type: 'integer' }) trashDays!: number; + @ApiProperty({ type: 'integer' }) + userDeleteDelay!: number; isInitialized!: boolean; isOnboarded!: boolean; externalDomain!: string; diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/domain/server-info/server-info.service.spec.ts index e097509e6..8c90f8107 100644 --- a/server/src/domain/server-info/server-info.service.spec.ts +++ b/server/src/domain/server-info/server-info.service.spec.ts @@ -196,6 +196,7 @@ describe(ServerInfoService.name, () => { loginPageMessage: '', oauthButtonText: 'Login with OAuth', trashDays: 30, + userDeleteDelay: 7, isInitialized: undefined, isOnboarded: false, externalDomain: '', diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/domain/server-info/server-info.service.ts index 51d26b2c3..04b3c4b6e 100644 --- a/server/src/domain/server-info/server-info.service.ts +++ b/server/src/domain/server-info/server-info.service.ts @@ -96,6 +96,7 @@ export class ServerInfoService { return { loginPageMessage: config.server.loginPageMessage, trashDays: config.trash.days, + userDeleteDelay: config.user.deleteDelay, oauthButtonText: config.oauth.buttonText, isInitialized, isOnboarded: onboarding?.isOnboarded || false, @@ -149,7 +150,7 @@ export class ServerInfoService { } // check once per hour (max) - if (this.releaseVersionCheckedAt && this.releaseVersionCheckedAt.diffNow().as('minutes') < 60) { + if (this.releaseVersionCheckedAt && DateTime.now().diff(this.releaseVersionCheckedAt).as('minutes') < 60) { return true; } @@ -170,7 +171,7 @@ export class ServerInfoService { return true; } - private async handleConnect(userId: string) { + private handleConnect(userId: string) { this.communicationRepository.send(ClientEvent.SERVER_VERSION, userId, serverVersion); this.newReleaseNotification(userId); } diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts index bb5b61820..550ed70ea 100644 --- a/server/src/domain/shared-link/shared-link.dto.ts +++ b/server/src/domain/shared-link/shared-link.dto.ts @@ -1,8 +1,7 @@ import { SharedLinkType } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsBoolean, IsDate, IsEnum, IsString } from 'class-validator'; -import { Optional, ValidateUUID } from '../domain.util'; +import { IsEnum, IsString } from 'class-validator'; +import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../domain.util'; export class SharedLinkCreateDto { @IsEnum(SharedLinkType) @@ -23,21 +22,16 @@ export class SharedLinkCreateDto { @Optional() password?: string; - @IsDate() - @Type(() => Date) - @Optional({ nullable: true }) + @ValidateDate({ optional: true, nullable: true }) expiresAt?: Date | null = null; - @Optional() - @IsBoolean() - allowUpload?: boolean = false; + @ValidateBoolean({ optional: true }) + allowUpload?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) allowDownload?: boolean = true; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) showMetadata?: boolean = true; } @@ -54,10 +48,10 @@ export class SharedLinkEditDto { @Optional() allowUpload?: boolean; - @Optional() + @ValidateBoolean({ optional: true }) allowDownload?: boolean; - @Optional() + @ValidateBoolean({ optional: true }) showMetadata?: boolean; /** @@ -65,8 +59,7 @@ export class SharedLinkEditDto { * Setting this flag and not sending expiryAt is considered as null instead. * Clients that can send null values can ignore this. */ - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) changeExpiryTime?: boolean; } diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index 6d95d2831..f0d0715a3 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -22,7 +22,7 @@ describe(SharedLinkService.name, () => { let cryptoMock: jest.Mocked; let shareMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); cryptoMock = newCryptoRepositoryMock(); shareMock = newSharedLinkRepositoryMock(); diff --git a/server/src/domain/smart-info/dto/model-config.dto.ts b/server/src/domain/smart-info/dto/model-config.dto.ts index 64f8c1d00..b9e27669f 100644 --- a/server/src/domain/smart-info/dto/model-config.dto.ts +++ b/server/src/domain/smart-info/dto/model-config.dto.ts @@ -1,11 +1,11 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; -import { Optional } from '../../domain.util'; +import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; +import { Optional, ValidateBoolean } from '../../domain.util'; import { CLIPMode, ModelType } from '../../repositories'; export class ModelConfig { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @IsString() diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/domain/smart-info/smart-info.service.spec.ts index 9835ea1a5..712c2b6a7 100644 --- a/server/src/domain/smart-info/smart-info.service.spec.ts +++ b/server/src/domain/smart-info/smart-info.service.spec.ts @@ -35,7 +35,7 @@ describe(SmartInfoService.name, () => { let machineMock: jest.Mocked; let databaseMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { assetMock = newAssetRepositoryMock(); configMock = newSystemConfigRepositoryMock(); searchMock = newSearchRepositoryMock(); diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/domain/storage-template/storage-template.service.spec.ts index 67d2bd222..1db312d78 100644 --- a/server/src/domain/storage-template/storage-template.service.spec.ts +++ b/server/src/domain/storage-template/storage-template.service.spec.ts @@ -45,7 +45,7 @@ describe(StorageTemplateService.name, () => { expect(sut).toBeDefined(); }); - beforeEach(async () => { + beforeEach(() => { configMock = newSystemConfigRepositoryMock(); assetMock = newAssetRepositoryMock(); albumMock = newAlbumRepositoryMock(); diff --git a/server/src/domain/storage/storage.service.spec.ts b/server/src/domain/storage/storage.service.spec.ts index 0c5531e5f..785891086 100644 --- a/server/src/domain/storage/storage.service.spec.ts +++ b/server/src/domain/storage/storage.service.spec.ts @@ -6,7 +6,7 @@ describe(StorageService.name, () => { let sut: StorageService; let storageMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { storageMock = newStorageRepositoryMock(); sut = new StorageService(storageMock); }); diff --git a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts index 2783e35e6..3a219888f 100644 --- a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts +++ b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts @@ -1,7 +1,8 @@ import { AudioCodec, CQMode, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsBoolean, IsEnum, IsInt, IsString, Max, Min } from 'class-validator'; +import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigFFmpegDto { @IsInt() @@ -68,14 +69,14 @@ export class SystemConfigFFmpegDto { @ApiProperty({ type: 'integer' }) npl!: number; - @IsBoolean() + @ValidateBoolean() temporalAQ!: boolean; @IsEnum(CQMode) @ApiProperty({ enumName: 'CQMode', enum: CQMode }) cqMode!: CQMode; - @IsBoolean() + @ValidateBoolean() twoPass!: boolean; @IsString() diff --git a/server/src/domain/system-config/dto/system-config-library.dto.ts b/server/src/domain/system-config/dto/system-config-library.dto.ts index fdbae600f..85ab62634 100644 --- a/server/src/domain/system-config/dto/system-config-library.dto.ts +++ b/server/src/domain/system-config/dto/system-config-library.dto.ts @@ -1,7 +1,5 @@ -import { validateCronExpression } from '@app/domain'; import { Type } from 'class-transformer'; import { - IsBoolean, IsNotEmpty, IsObject, IsString, @@ -11,6 +9,7 @@ import { ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; +import { ValidateBoolean, validateCronExpression } from '../../domain.util'; const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; @@ -22,7 +21,7 @@ class CronValidator implements ValidatorConstraintInterface { } export class SystemConfigLibraryScanDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @ValidateIf(isEnabled) @@ -33,7 +32,7 @@ export class SystemConfigLibraryScanDto { } export class SystemConfigLibraryWatchDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; } diff --git a/server/src/domain/system-config/dto/system-config-logging.dto.ts b/server/src/domain/system-config/dto/system-config-logging.dto.ts index d280df535..09f78fc86 100644 --- a/server/src/domain/system-config/dto/system-config-logging.dto.ts +++ b/server/src/domain/system-config/dto/system-config-logging.dto.ts @@ -1,9 +1,10 @@ import { LogLevel } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum } from 'class-validator'; +import { IsEnum } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigLoggingDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) diff --git a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts index ed331eb6a..435e68826 100644 --- a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts +++ b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts @@ -1,9 +1,10 @@ -import { CLIPConfig, RecognitionConfig } from '@app/domain'; import { Type } from 'class-transformer'; -import { IsBoolean, IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator'; +import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; +import { CLIPConfig, RecognitionConfig } from '../../smart-info/dto/model-config.dto'; export class SystemConfigMachineLearningDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @IsUrl({ require_tld: false, allow_underscores: true }) diff --git a/server/src/domain/system-config/dto/system-config-map.dto.ts b/server/src/domain/system-config/dto/system-config-map.dto.ts index 07700d98c..9e21e2d5d 100644 --- a/server/src/domain/system-config/dto/system-config-map.dto.ts +++ b/server/src/domain/system-config/dto/system-config-map.dto.ts @@ -1,7 +1,8 @@ -import { IsBoolean, IsString } from 'class-validator'; +import { IsString } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigMapDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @IsString() diff --git a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts index c27673924..379f5643d 100644 --- a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts +++ b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts @@ -1,6 +1,6 @@ -import { IsBoolean } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigNewVersionCheckDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; } diff --git a/server/src/domain/system-config/dto/system-config-oauth.dto.ts b/server/src/domain/system-config/dto/system-config-oauth.dto.ts index 04159b8d3..99779bdfe 100644 --- a/server/src/domain/system-config/dto/system-config-oauth.dto.ts +++ b/server/src/domain/system-config/dto/system-config-oauth.dto.ts @@ -1,13 +1,14 @@ -import { IsBoolean, IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator'; +import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; const isEnabled = (config: SystemConfigOAuthDto) => config.enabled; const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; export class SystemConfigOAuthDto { - @IsBoolean() + @ValidateBoolean() autoLaunch!: boolean; - @IsBoolean() + @ValidateBoolean() autoRegister!: boolean; @IsString() @@ -27,7 +28,7 @@ export class SystemConfigOAuthDto { @Min(0) defaultStorageQuota!: number; - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @ValidateIf(isEnabled) @@ -35,7 +36,7 @@ export class SystemConfigOAuthDto { @IsString() issuerUrl!: string; - @IsBoolean() + @ValidateBoolean() mobileOverrideEnabled!: boolean; @ValidateIf(isOverrideEnabled) diff --git a/server/src/domain/system-config/dto/system-config-password-login.dto.ts b/server/src/domain/system-config/dto/system-config-password-login.dto.ts index 119de65f6..279bcc5a6 100644 --- a/server/src/domain/system-config/dto/system-config-password-login.dto.ts +++ b/server/src/domain/system-config/dto/system-config-password-login.dto.ts @@ -1,6 +1,6 @@ -import { IsBoolean } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigPasswordLoginDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; } diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts index aa224ccc6..11e0ae289 100644 --- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts +++ b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts @@ -1,6 +1,6 @@ -import { IsBoolean } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigReverseGeocodingDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; } diff --git a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts b/server/src/domain/system-config/dto/system-config-storage-template.dto.ts index c09b5564a..615fd8521 100644 --- a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts +++ b/server/src/domain/system-config/dto/system-config-storage-template.dto.ts @@ -1,10 +1,13 @@ -import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigStorageTemplateDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; - @IsBoolean() + + @ValidateBoolean() hashVerificationEnabled!: boolean; + @IsNotEmpty() @IsString() template!: string; diff --git a/server/src/domain/system-config/dto/system-config-trash.dto.ts b/server/src/domain/system-config/dto/system-config-trash.dto.ts index bfbdb3941..482410703 100644 --- a/server/src/domain/system-config/dto/system-config-trash.dto.ts +++ b/server/src/domain/system-config/dto/system-config-trash.dto.ts @@ -1,9 +1,10 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsBoolean, IsInt, Min } from 'class-validator'; +import { IsInt, Min } from 'class-validator'; +import { ValidateBoolean } from '../../domain.util'; export class SystemConfigTrashDto { - @IsBoolean() + @ValidateBoolean() enabled!: boolean; @IsInt() diff --git a/server/src/domain/system-config/dto/system-config-user.dto.ts b/server/src/domain/system-config/dto/system-config-user.dto.ts new file mode 100644 index 000000000..22d6ef5fc --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-user.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, Min } from 'class-validator'; + +export class SystemConfigUserDto { + @IsInt() + @Min(1) + @Type(() => Number) + @ApiProperty({ type: 'integer' }) + deleteDelay!: number; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index 122d78ca6..4906e293e 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -16,6 +16,7 @@ import { SystemConfigStorageTemplateDto } from './system-config-storage-template import { SystemConfigThemeDto } from './system-config-theme.dto'; import { SystemConfigThumbnailDto } from './system-config-thumbnail.dto'; import { SystemConfigTrashDto } from './system-config-trash.dto'; +import { SystemConfigUserDto } from './system-config-user.dto'; export class SystemConfigDto implements SystemConfig { @Type(() => SystemConfigFFmpegDto) @@ -92,6 +93,11 @@ export class SystemConfigDto implements SystemConfig { @ValidateNested() @IsObject() server!: SystemConfigServerDto; + + @Type(() => SystemConfigUserDto) + @ValidateNested() + @IsObject() + user!: SystemConfigUserDto; } export function mapConfig(config: SystemConfig): SystemConfigDto { diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index a9d41d76d..644d5c3cb 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -75,7 +75,7 @@ export const defaults = Object.freeze({ enabled: true, modelName: 'buffalo_l', minScore: 0.7, - maxDistance: 0.6, + maxDistance: 0.5, minFaces: 3, }, }, @@ -140,6 +140,9 @@ export const defaults = Object.freeze({ externalDomain: '', loginPageMessage: '', }, + user: { + deleteDelay: 7, + }, }); export enum FeatureFlag { diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index a3d29b1ee..91c095cb7 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -23,6 +23,7 @@ const updates: SystemConfigEntity[] = [ { key: SystemConfigKey.FFMPEG_CRF, value: 30 }, { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true }, { key: SystemConfigKey.TRASH_DAYS, value: 10 }, + { key: SystemConfigKey.USER_DELETE_DELAY, value: 15 }, ]; const updatedConfig = Object.freeze({ @@ -75,7 +76,7 @@ const updatedConfig = Object.freeze({ enabled: true, modelName: 'buffalo_l', minScore: 0.7, - maxDistance: 0.6, + maxDistance: 0.5, minFaces: 3, }, }, @@ -140,6 +141,9 @@ const updatedConfig = Object.freeze({ enabled: false, }, }, + user: { + deleteDelay: 15, + }, }); describe(SystemConfigService.name, () => { @@ -148,7 +152,7 @@ describe(SystemConfigService.name, () => { let communicationMock: jest.Mocked; let smartInfoMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { delete process.env.IMMICH_CONFIG_FILE; configMock = newSystemConfigRepositoryMock(); communicationMock = newCommunicationRepositoryMock(); @@ -199,6 +203,7 @@ describe(SystemConfigService.name, () => { { key: SystemConfigKey.FFMPEG_CRF, value: 30 }, { key: SystemConfigKey.OAUTH_AUTO_LAUNCH, value: true }, { key: SystemConfigKey.TRASH_DAYS, value: 10 }, + { key: SystemConfigKey.USER_DELETE_DELAY, value: 15 }, ]); await expect(sut.getConfig()).resolves.toEqual(updatedConfig); @@ -206,7 +211,12 @@ describe(SystemConfigService.name, () => { it('should load the config from a file', async () => { process.env.IMMICH_CONFIG_FILE = 'immich-config.json'; - const partialConfig = { ffmpeg: { crf: 30 }, oauth: { autoLaunch: true }, trash: { days: 10 } }; + const partialConfig = { + ffmpeg: { crf: 30 }, + oauth: { autoLaunch: true }, + trash: { days: 10 }, + user: { deleteDelay: 15 }, + }; configMock.readFile.mockResolvedValue(JSON.stringify(partialConfig)); await expect(sut.getConfig()).resolves.toEqual(updatedConfig); diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 39a3ea1df..54d113cf6 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -118,7 +118,7 @@ export class SystemConfigService { await this.core.refreshConfig(); } - private async setLogLevel({ logging }: SystemConfig) { + private setLogLevel({ logging }: SystemConfig) { const envLevel = this.getEnvLogLevel(); const configLevel = logging.enabled ? logging.level : false; const level = envLevel ?? configLevel; @@ -130,7 +130,7 @@ export class SystemConfigService { return process.env.LOG_LEVEL as LogLevel; } - private async validateConfig(newConfig: SystemConfig, oldConfig: SystemConfig) { + private validateConfig(newConfig: SystemConfig, oldConfig: SystemConfig) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.'); } diff --git a/server/src/domain/trash/trash.service.spec.ts b/server/src/domain/trash/trash.service.spec.ts index 1b200a1bd..81f4186e8 100644 --- a/server/src/domain/trash/trash.service.spec.ts +++ b/server/src/domain/trash/trash.service.spec.ts @@ -23,7 +23,7 @@ describe(TrashService.name, () => { expect(sut).toBeDefined(); }); - beforeEach(async () => { + beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); communicationMock = newCommunicationRepositoryMock(); diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts index e6dbb8167..f0cc7938c 100644 --- a/server/src/domain/user/dto/create-user.dto.ts +++ b/server/src/domain/user/dto/create-user.dto.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; -import { Optional, toEmail, toSanitized } from '../../domain.util'; +import { IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; +import { Optional, ValidateBoolean, toEmail, toSanitized } from '../../domain.util'; export class CreateUserDto { @IsEmail({ require_tld: false }) @@ -21,8 +21,7 @@ export class CreateUserDto { @Transform(toSanitized) storageLabel?: string | null; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) memoriesEnabled?: boolean; @Optional({ nullable: true }) @@ -30,6 +29,9 @@ export class CreateUserDto { @IsPositive() @ApiProperty({ type: 'integer', format: 'int64' }) quotaSizeInBytes?: number | null; + + @ValidateBoolean({ optional: true }) + shouldChangePassword?: boolean; } export class CreateAdminDto { diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts index 1cab11627..e8cce2214 100644 --- a/server/src/domain/user/dto/update-user.dto.ts +++ b/server/src/domain/user/dto/update-user.dto.ts @@ -1,8 +1,8 @@ import { UserAvatarColor } from '@app/infra/entities'; import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; -import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; -import { Optional, toEmail, toSanitized } from '../../domain.util'; +import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; +import { Optional, ValidateBoolean, toEmail, toSanitized } from '../../domain.util'; export class UpdateUserDto { @Optional() @@ -30,16 +30,13 @@ export class UpdateUserDto { @ApiProperty({ format: 'uuid' }) id!: string; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) isAdmin?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) shouldChangePassword?: boolean; - @Optional() - @IsBoolean() + @ValidateBoolean({ optional: true }) memoriesEnabled?: boolean; @Optional() diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/domain/user/user.service.spec.ts index 13ae149b4..dba0106fb 100644 --- a/server/src/domain/user/user.service.spec.ts +++ b/server/src/domain/user/user.service.spec.ts @@ -8,12 +8,13 @@ import { import { authStub, newAlbumRepositoryMock, - newAssetRepositoryMock, newCryptoRepositoryMock, newJobRepositoryMock, newLibraryRepositoryMock, newStorageRepositoryMock, + newSystemConfigRepositoryMock, newUserRepositoryMock, + systemConfigStub, userStub, } from '@test'; import { when } from 'jest-when'; @@ -21,11 +22,11 @@ import { CacheControl, ImmichFileResponse } from '../domain.util'; import { JobName } from '../job'; import { IAlbumRepository, - IAssetRepository, ICryptoRepository, IJobRepository, ILibraryRepository, IStorageRepository, + ISystemConfigRepository, IUserRepository, } from '../repositories'; import { UpdateUserDto } from './dto/update-user.dto'; @@ -44,21 +45,21 @@ describe(UserService.name, () => { let cryptoRepositoryMock: jest.Mocked; let albumMock: jest.Mocked; - let assetMock: jest.Mocked; let jobMock: jest.Mocked; let libraryMock: jest.Mocked; let storageMock: jest.Mocked; + let configMock: jest.Mocked; - beforeEach(async () => { + beforeEach(() => { albumMock = newAlbumRepositoryMock(); - assetMock = newAssetRepositoryMock(); + configMock = newSystemConfigRepositoryMock(); cryptoRepositoryMock = newCryptoRepositoryMock(); jobMock = newJobRepositoryMock(); libraryMock = newLibraryRepositoryMock(); storageMock = newStorageRepositoryMock(); userMock = newUserRepositoryMock(); - sut = new UserService(albumMock, assetMock, cryptoRepositoryMock, jobMock, libraryMock, storageMock, userMock); + sut = new UserService(albumMock, cryptoRepositoryMock, jobMock, libraryMock, storageMock, configMock, userMock); when(userMock.get).calledWith(authStub.admin.user.id, {}).mockResolvedValue(userStub.admin); when(userMock.get).calledWith(authStub.admin.user.id, { withDeleted: true }).mockResolvedValue(userStub.admin); @@ -461,6 +462,22 @@ describe(UserService.name, () => { expect(jobMock.queueAll).toHaveBeenCalledWith([]); }); + it('should skip users not ready for deletion - deleteDelay30', async () => { + configMock.load.mockResolvedValue(systemConfigStub.deleteDelay30); + userMock.getDeletedUsers.mockResolvedValue([ + {}, + { deletedAt: undefined }, + { deletedAt: null }, + { deletedAt: makeDeletedAt(15) }, + ] as UserEntity[]); + + await sut.handleUserDeleteCheck(); + + expect(userMock.getDeletedUsers).toHaveBeenCalled(); + expect(jobMock.queue).not.toHaveBeenCalled(); + expect(jobMock.queueAll).toHaveBeenCalledWith([]); + }); + it('should queue user ready for deletion', async () => { const user = { id: 'deleted-user', deletedAt: makeDeletedAt(10) }; userMock.getDeletedUsers.mockResolvedValue([user] as UserEntity[]); @@ -470,6 +487,16 @@ describe(UserService.name, () => { expect(userMock.getDeletedUsers).toHaveBeenCalled(); expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]); }); + + it('should queue user ready for deletion - deleteDelay30', async () => { + const user = { id: 'deleted-user', deletedAt: makeDeletedAt(31) }; + userMock.getDeletedUsers.mockResolvedValue([user] as UserEntity[]); + + await sut.handleUserDeleteCheck(); + + expect(userMock.getDeletedUsers).toHaveBeenCalled(); + expect(jobMock.queueAll).toHaveBeenCalledWith([{ name: JobName.USER_DELETION, data: { id: user.id } }]); + }); }); describe('handleUserDelete', () => { @@ -497,7 +524,6 @@ describe(UserService.name, () => { expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/thumbs/deleted-user', options); expect(storageMock.unlinkDir).toHaveBeenCalledWith('upload/encoded-video/deleted-user', options); expect(albumMock.deleteAll).toHaveBeenCalledWith(user.id); - expect(assetMock.deleteAll).toHaveBeenCalledWith(user.id); expect(userMock.delete).toHaveBeenCalledWith(user, true); }); diff --git a/server/src/domain/user/user.service.ts b/server/src/domain/user/user.service.ts index a5b3fb7dc..9a862199b 100644 --- a/server/src/domain/user/user.service.ts +++ b/server/src/domain/user/user.service.ts @@ -8,34 +8,37 @@ import { CacheControl, ImmichFileResponse } from '../domain.util'; import { IEntityJob, JobName } from '../job'; import { IAlbumRepository, - IAssetRepository, ICryptoRepository, IJobRepository, ILibraryRepository, IStorageRepository, + ISystemConfigRepository, IUserRepository, UserFindOptions, } from '../repositories'; import { StorageCore, StorageFolder } from '../storage'; +import { SystemConfigCore } from '../system-config/system-config.core'; import { CreateUserDto, UpdateUserDto } from './dto'; import { CreateProfileImageResponseDto, UserResponseDto, mapCreateProfileImageResponse, mapUser } from './response-dto'; import { UserCore } from './user.core'; @Injectable() export class UserService { + private configCore: SystemConfigCore; private logger = new ImmichLogger(UserService.name); private userCore: UserCore; constructor( @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, - @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ICryptoRepository) cryptoRepository: ICryptoRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ILibraryRepository) libraryRepository: ILibraryRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, + @Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository, @Inject(IUserRepository) private userRepository: IUserRepository, ) { this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository); + this.configCore = SystemConfigCore.create(configRepository); } async getAll(auth: AuthDto, isAll: boolean): Promise { @@ -140,22 +143,26 @@ export class UserService { async handleUserDeleteCheck() { const users = await this.userRepository.getDeletedUsers(); + const config = await this.configCore.getConfig(); await this.jobRepository.queueAll( users.flatMap((user) => - this.isReadyForDeletion(user) ? [{ name: JobName.USER_DELETION, data: { id: user.id } }] : [], + this.isReadyForDeletion(user, config.user.deleteDelay) + ? [{ name: JobName.USER_DELETION, data: { id: user.id } }] + : [], ), ); return true; } async handleUserDelete({ id }: IEntityJob) { + const config = await this.configCore.getConfig(); const user = await this.userRepository.get(id, { withDeleted: true }); if (!user) { return false; } // just for extra protection here - if (!this.isReadyForDeletion(user)) { + if (!this.isReadyForDeletion(user, config.user.deleteDelay)) { this.logger.warn(`Skipped user that was not ready for deletion: id=${id}`); return false; } @@ -176,20 +183,18 @@ export class UserService { } this.logger.warn(`Removing user from database: ${user.id}`); - await this.albumRepository.deleteAll(user.id); - await this.assetRepository.deleteAll(user.id); await this.userRepository.delete(user, true); return true; } - private isReadyForDeletion(user: UserEntity): boolean { + private isReadyForDeletion(user: UserEntity, deleteDelay: number): boolean { if (!user.deletedAt) { return false; } - return DateTime.now().minus({ days: 7 }) > DateTime.fromJSDate(user.deletedAt); + return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt); } private async findOrFail(id: string, options: UserFindOptions) { diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/immich/api-v1/asset/asset.service.ts index 5dcc487be..923cb4ebe 100644 --- a/server/src/immich/api-v1/asset/asset.service.ts +++ b/server/src/immich/api-v1/asset/asset.service.ts @@ -26,7 +26,6 @@ import { InternalServerErrorException, NotFoundException, } from '@nestjs/common'; -import { parse } from 'node:path'; import { QueryFailedError } from 'typeorm'; import { IAssetRepositoryV1 } from './asset-repository'; import { AssetBulkUploadCheckDto } from './dto/asset-check.dto'; @@ -116,9 +115,17 @@ export class AssetService { await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); const assets = await this.assetRepository.getAllByFileCreationDate( { take: dto.take ?? 1000, skip: dto.skip }, - { ...dto, userIds: [userId], withDeleted: true, orderDirection: 'DESC', withExif: true, isVisible: true }, + { + ...dto, + userIds: [userId], + withDeleted: true, + orderDirection: 'DESC', + withExif: true, + isVisible: true, + withStacked: true, + }, ); - return assets.items.map((asset) => mapAsset(asset)); + return assets.items.map((asset) => mapAsset(asset, { withStack: true })); } async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise { @@ -348,7 +355,7 @@ export class AssetService { duration: dto.duration || null, isVisible: dto.isVisible ?? true, livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity), - originalFileName: parse(file.originalName).name, + originalFileName: file.originalName, sidecarPath: sidecarPath || null, isReadOnly: dto.isReadOnly ?? false, isOffline: dto.isOffline ?? false, diff --git a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts index d73856ab9..719018488 100644 --- a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts +++ b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts @@ -1,19 +1,13 @@ -import { Optional, toBoolean } from '@app/domain'; +import { Optional, ValidateBoolean, ValidateDate } from '@app/domain'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsBoolean, IsDate, IsInt, IsNotEmpty, IsUUID } from 'class-validator'; +import { Type } from 'class-transformer'; +import { IsInt, IsUUID } from 'class-validator'; export class AssetSearchDto { - @Optional() - @IsNotEmpty() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @Optional() - @IsNotEmpty() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isArchived?: boolean; @Optional() @@ -33,13 +27,9 @@ export class AssetSearchDto { @ApiProperty({ format: 'uuid' }) userId?: string; - @Optional() - @IsDate() - @Type(() => Date) + @ValidateDate({ optional: true }) updatedAfter?: Date; - @Optional() - @IsDate() - @Type(() => Date) + @ValidateDate({ optional: true }) updatedBefore?: Date; } diff --git a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts index 9850384d9..1b140d69f 100644 --- a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts +++ b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts @@ -1,7 +1,6 @@ -import { Optional, toBoolean, UploadFieldName, ValidateUUID } from '@app/domain'; +import { Optional, UploadFieldName, ValidateBoolean, ValidateDate, ValidateUUID } from '@app/domain'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsBoolean, IsDate, IsNotEmpty, IsString } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class CreateAssetDto { @ValidateUUID({ optional: true }) @@ -15,43 +14,29 @@ export class CreateAssetDto { @IsString() deviceId!: string; - @IsNotEmpty() - @IsDate() - @Type(() => Date) + @ValidateDate() fileCreatedAt!: Date; - @IsNotEmpty() - @IsDate() - @Type(() => Date) + @ValidateDate() fileModifiedAt!: Date; @Optional() @IsString() duration?: string; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isFavorite?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isArchived?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isVisible?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isOffline?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) + @ValidateBoolean({ optional: true }) isReadOnly?: boolean; // The properties below are added to correctly generate the API docs diff --git a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts b/server/src/immich/api-v1/asset/dto/serve-file.dto.ts index dd13c6dfb..72e228601 100644 --- a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts +++ b/server/src/immich/api-v1/asset/dto/serve-file.dto.ts @@ -1,18 +1,12 @@ -import { Optional, toBoolean } from '@app/domain'; +import { ValidateBoolean } from '@app/domain'; import { ApiProperty } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsBoolean } from 'class-validator'; export class ServeFileDto { - @Optional() - @IsBoolean() - @Transform(toBoolean) - @ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' }) + @ValidateBoolean({ optional: true }) + @ApiProperty({ title: 'Is serve thumbnail (resize) file' }) isThumb?: boolean; - @Optional() - @IsBoolean() - @Transform(toBoolean) - @ApiProperty({ type: Boolean, title: 'Is request made from web' }) + @ValidateBoolean({ optional: true }) + @ApiProperty({ title: 'Is request made from web' }) isWeb?: boolean; } diff --git a/server/src/immich/app.service.ts b/server/src/immich/app.service.ts index be82ae4dc..f3369b121 100644 --- a/server/src/immich/app.service.ts +++ b/server/src/immich/app.service.ts @@ -2,7 +2,6 @@ import { AuthService, DatabaseService, JobService, - LibraryService, ONE_HOUR, OpenGraphTags, ServerInfoService, @@ -45,7 +44,6 @@ export class AppService { private authService: AuthService, private configService: SystemConfigService, private jobService: JobService, - private libraryService: LibraryService, private serverService: ServerInfoService, private sharedLinkService: SharedLinkService, private storageService: StorageService, @@ -66,15 +64,10 @@ export class AppService { await this.databaseService.init(); await this.configService.init(); this.storageService.init(); - await this.libraryService.init(); await this.serverService.init(); this.logger.log(`Feature Flags: ${JSON.stringify(await this.serverService.getFeatures(), null, 2)}`); } - async teardown() { - await this.libraryService.unwatchAll(); - } - ssr(excludePaths: string[]) { let index = ''; try { diff --git a/server/src/immich/controllers/person.controller.ts b/server/src/immich/controllers/person.controller.ts index 6582d4461..2447f982b 100644 --- a/server/src/immich/controllers/person.controller.ts +++ b/server/src/immich/controllers/person.controller.ts @@ -6,6 +6,7 @@ import { MergePersonDto, PeopleResponseDto, PeopleUpdateDto, + PersonCreateDto, PersonResponseDto, PersonSearchDto, PersonService, @@ -32,22 +33,13 @@ export class PersonController { } @Post() - createPerson(@Auth() auth: AuthDto): Promise { - return this.service.createPerson(auth); - } - - @Put(':id/reassign') - reassignFaces( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @Body() dto: AssetFaceUpdateDto, - ): Promise { - return this.service.reassignFaces(auth, id, dto); + createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise { + return this.service.create(auth, dto); } @Put() updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { - return this.service.updatePeople(auth, dto); + return this.service.updateAll(auth, dto); } @Get(':id') @@ -85,6 +77,15 @@ export class PersonController { return this.service.getAssets(auth, id); } + @Put(':id/reassign') + reassignFaces( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: AssetFaceUpdateDto, + ): Promise { + return this.service.reassignFaces(auth, id, dto); + } + @Post(':id/merge') mergePerson( @Auth() auth: AuthDto, diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index b807da966..df1bec7c6 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -12,7 +12,7 @@ import { SmartSearchDto, } from '@app/domain'; import { SearchSuggestionRequestDto } from '@app/domain/search/dto/search-suggestion.dto'; -import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; import { Auth, Authenticated } from '../app.guard'; import { UseValidation } from '../app.utils'; @@ -24,22 +24,24 @@ import { UseValidation } from '../app.utils'; export class SearchController { constructor(private service: SearchService) {} + @Get() + @ApiOperation({ deprecated: true }) + search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise { + return this.service.search(auth, dto); + } + @Post('metadata') + @HttpCode(HttpStatus.OK) searchMetadata(@Auth() auth: AuthDto, @Body() dto: MetadataSearchDto): Promise { return this.service.searchMetadata(auth, dto); } @Post('smart') + @HttpCode(HttpStatus.OK) searchSmart(@Auth() auth: AuthDto, @Body() dto: SmartSearchDto): Promise { return this.service.searchSmart(auth, dto); } - @Get() - @ApiOperation({ deprecated: true }) - search(@Auth() auth: AuthDto, @Query() dto: SearchDto): Promise { - return this.service.search(auth, dto); - } - @Get('explore') getExploreData(@Auth() auth: AuthDto): Promise { return this.service.getExploreData(auth) as Promise; diff --git a/server/src/immich/interceptors/error.interceptor.ts b/server/src/immich/interceptors/error.interceptor.ts index 1dc52258e..5fabdbe55 100644 --- a/server/src/immich/interceptors/error.interceptor.ts +++ b/server/src/immich/interceptors/error.interceptor.ts @@ -15,7 +15,7 @@ import { routeToErrorMessage } from '../app.utils'; export class ErrorInterceptor implements NestInterceptor { private logger = new ImmichLogger(ErrorInterceptor.name); - async intercept(context: ExecutionContext, next: CallHandler): Promise> { + intercept(context: ExecutionContext, next: CallHandler): Observable { return next.handle().pipe( catchError((error) => throwError(() => { diff --git a/server/src/immich/interceptors/file-upload.interceptor.ts b/server/src/immich/interceptors/file-upload.interceptor.ts index 52cc447e8..a698dc8a6 100644 --- a/server/src/immich/interceptors/file-upload.interceptor.ts +++ b/server/src/immich/interceptors/file-upload.interceptor.ts @@ -40,9 +40,9 @@ interface Callback { (error: null, result: T): void; } -const callbackify = async (target: (...arguments_: any[]) => T, callback: Callback) => { +const callbackify = (target: (...arguments_: any[]) => T, callback: Callback) => { try { - return callback(null, await target()); + return callback(null, target()); } catch (error: Error | any) { return callback(error); } diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts index 373271158..96438a07d 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/infra/entities/asset.entity.ts @@ -35,6 +35,7 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum'; @Index('IDX_day_of_month', { synchronize: false }) @Index('IDX_month', { synchronize: false }) @Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId']) +@Index('idx_originalFileName_trigram', { synchronize: false }) // For all assets, each originalpath must be unique per user and library export class AssetEntity { @PrimaryGeneratedColumn('uuid') diff --git a/server/src/infra/entities/shared-link.entity.ts b/server/src/infra/entities/shared-link.entity.ts index f64ad8424..e7cd19e53 100644 --- a/server/src/infra/entities/shared-link.entity.ts +++ b/server/src/infra/entities/shared-link.entity.ts @@ -27,7 +27,7 @@ export class SharedLinkEntity { @Column() userId!: string; - @ManyToOne(() => UserEntity) + @ManyToOne(() => UserEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) user!: UserEntity; @Index('IDX_sharedlink_key') diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index e2d0c71f6..1ba219429 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -108,6 +108,8 @@ export enum SystemConfigKey { TRASH_DAYS = 'trash.days', THEME_CUSTOM_CSS = 'theme.customCss', + + USER_DELETE_DELAY = 'user.deleteDelay', } export enum TranscodePolicy { @@ -276,4 +278,7 @@ export interface SystemConfig { externalDomain: string; loginPageMessage: string; }; + user: { + deleteDelay: number; + }; } diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index 745f5a38f..636d78ab7 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -2,7 +2,6 @@ import { AssetSearchBuilderOptions, Paginated, PaginationOptions } from '@app/do import _ from 'lodash'; import { Between, - Brackets, FindManyOptions, IsNull, LessThanOrEqual, @@ -160,9 +159,15 @@ export function searchAssetBuilder( builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); } - const path = _.pick(options, ['encodedVideoPath', 'originalFileName', 'originalPath', 'resizePath', 'webpPath']); + const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'resizePath', 'webpPath']); builder.andWhere(_.omitBy(path, _.isUndefined)); + if (options.originalFileName) { + builder.andWhere(`f_unaccent(${builder.alias}.originalFileName) ILIKE f_unaccent(:originalFileName)`, { + originalFileName: `%${options.originalFileName}%`, + }); + } + const status = _.pick(options, ['isExternal', 'isFavorite', 'isOffline', 'isReadOnly', 'isVisible', 'type']); const { isArchived, @@ -223,12 +228,7 @@ export function searchAssetBuilder( } if (withStacked) { - builder - .leftJoinAndSelect(`${builder.alias}.stack`, 'stack') - .leftJoinAndSelect('stack.assets', 'stackedAssets') - .andWhere( - new Brackets((qb) => qb.where(`stack.primaryAssetId = ${builder.alias}.id`).orWhere('asset.stackId IS NULL')), - ); + builder.leftJoinAndSelect(`${builder.alias}.stack`, 'stack').leftJoinAndSelect('stack.assets', 'stackedAssets'); } const withDeleted = options.withDeleted ?? (trashedAfter !== undefined || trashedBefore !== undefined); diff --git a/server/src/infra/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts b/server/src/infra/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts new file mode 100644 index 000000000..1d4f13410 --- /dev/null +++ b/server/src/infra/migrations/1709608140355-AddAssetOriginalPathTrigramIndex.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddAssetOriginalPathTrigramIndex1709608140355 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE INDEX idx_originalFileName_trigram + ON assets + USING gin (f_unaccent("originalFileName") gin_trgm_ops)`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "idx_originalFileName_trigram"`); + } +} diff --git a/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts b/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts new file mode 100644 index 000000000..d1f73b4e3 --- /dev/null +++ b/server/src/infra/migrations/1709763765506-AddExtensionToOriginalFileName.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddExtensionToOriginalFileName1709763765506 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + WITH extension AS (WITH cte AS (SELECT a.id, STRING_TO_ARRAY(a."originalPath", '.')::TEXT[] AS arr + FROM assets a) + SELECT cte.id, cte.arr[ARRAY_UPPER(cte.arr, 1)] AS "ext" + FROM cte) + UPDATE assets + SET "originalFileName" = assets."originalFileName" || '.' || extension."ext" + FROM extension + WHERE assets.id = extension.id; + `); + } + + public async down(): Promise { + // noop + } +} diff --git a/server/src/infra/migrations/1709825430031-CascadeSharedLinksDelete.ts b/server/src/infra/migrations/1709825430031-CascadeSharedLinksDelete.ts new file mode 100644 index 000000000..968974192 --- /dev/null +++ b/server/src/infra/migrations/1709825430031-CascadeSharedLinksDelete.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class CascadeSharedLinksDelete1709825430031 implements MigrationInterface { + name = 'CascadeSharedLinksDelete1709825430031' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340"`); + await queryRunner.query(`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340"`); + await queryRunner.query(`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index a31ee2ad4..481305665 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -199,6 +199,29 @@ export class AssetRepository implements IAssetRepository { }); } + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.STRING]] }) + @ChunkedArray({ paramIndex: 1 }) + 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); + `, + [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'); builder = searchAssetBuilder(builder, options); @@ -507,6 +530,9 @@ export class AssetRepository implements IAssetRepository { select: { id: true, exifInfo: { + city: true, + state: true, + country: true, latitude: true, longitude: true, }, @@ -532,17 +558,16 @@ export class AssetRepository implements IAssetRepository { return assets.map((asset) => ({ id: asset.id, - - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ lat: asset.exifInfo!.latitude!, - - /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */ lon: asset.exifInfo!.longitude!, + city: asset.exifInfo!.city, + state: asset.exifInfo!.state, + country: asset.exifInfo!.country, })); } async getStatistics(ownerId: string, options: AssetStatsOptions): Promise { - let builder = await this.repository + let builder = this.repository .createQueryBuilder('asset') .select(`COUNT(asset.id)`, 'count') .addSelect(`asset.type`, 'type') diff --git a/server/src/infra/repositories/database.repository.ts b/server/src/infra/repositories/database.repository.ts index b0e4623af..b24602b89 100644 --- a/server/src/infra/repositories/database.repository.ts +++ b/server/src/infra/repositories/database.repository.ts @@ -210,6 +210,11 @@ export class DatabaseRepository implements IDatabaseRepository { return res as R; } + async tryLock(lock: DatabaseLock): Promise { + const queryRunner = this.dataSource.createQueryRunner(); + return await this.acquireTryLock(lock, queryRunner); + } + isBusy(lock: DatabaseLock): boolean { return this.asyncLock.isBusy(DatabaseLock[lock]); } @@ -222,6 +227,11 @@ export class DatabaseRepository implements IDatabaseRepository { return queryRunner.query('SELECT pg_advisory_lock($1)', [lock]); } + private async acquireTryLock(lock: DatabaseLock, queryRunner: QueryRunner): Promise { + const lockResult = await queryRunner.query('SELECT pg_try_advisory_lock($1)', [lock]); + return lockResult[0].pg_try_advisory_lock; + } + private async releaseLock(lock: DatabaseLock, queryRunner: QueryRunner): Promise { return queryRunner.query('SELECT pg_advisory_unlock($1)', [lock]); } diff --git a/server/src/infra/repositories/filesystem.provider.ts b/server/src/infra/repositories/filesystem.provider.ts index 3ffcd8111..fef184992 100644 --- a/server/src/infra/repositories/filesystem.provider.ts +++ b/server/src/infra/repositories/filesystem.provider.ts @@ -1,11 +1,12 @@ import { CrawlOptionsDto, DiskUsage, + IStorageRepository, ImmichReadStream, ImmichZipStream, - IStorageRepository, - mimeTypes, + StorageEventType, WatchEvents, + mimeTypes, } from '@app/domain'; import { ImmichLogger } from '@app/infra/logger'; import archiver from 'archiver'; @@ -141,10 +142,11 @@ export class FilesystemProvider implements IStorageRepository { watch(paths: string[], options: WatchOptions, events: Partial) { const watcher = chokidar.watch(paths, options); - watcher.on('ready', () => events.onReady?.()); - watcher.on('add', (path) => events.onAdd?.(path)); - watcher.on('change', (path) => events.onChange?.(path)); - watcher.on('unlink', (path) => events.onUnlink?.(path)); + watcher.on(StorageEventType.READY, () => events.onReady?.()); + watcher.on(StorageEventType.ADD, (path) => events.onAdd?.(path)); + watcher.on(StorageEventType.CHANGE, (path) => events.onChange?.(path)); + watcher.on(StorageEventType.UNLINK, (path) => events.onUnlink?.(path)); + watcher.on(StorageEventType.ERROR, (error) => events.onError?.(error)); return () => watcher.close(); } diff --git a/server/src/infra/repositories/library.repository.ts b/server/src/infra/repositories/library.repository.ts index 804fdc481..89db3d175 100644 --- a/server/src/infra/repositories/library.repository.ts +++ b/server/src/infra/repositories/library.repository.ts @@ -166,7 +166,7 @@ export class LibraryRepository implements ILibraryRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getAssetIds(libraryId: string, withDeleted = false): Promise { - let query = await this.repository + let query = this.repository .createQueryBuilder('library') .innerJoinAndSelect('library.assets', 'assets') .where('library.id = :id', { id: libraryId }) diff --git a/server/src/infra/repositories/media.repository.ts b/server/src/infra/repositories/media.repository.ts index 1f9395ff2..d5e4cd36f 100644 --- a/server/src/infra/repositories/media.repository.ts +++ b/server/src/infra/repositories/media.repository.ts @@ -1,4 +1,11 @@ -import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoInfo } from '@app/domain'; +import { + CropOptions, + IMediaRepository, + ResizeOptions, + TranscodeOptions, + VideoInfo, + handlePromiseError, +} from '@app/domain'; import { Colorspace } from '@app/infra/entities'; import { ImmichLogger } from '@app/infra/logger'; import ffmpeg, { FfprobeData } from 'fluent-ffmpeg'; @@ -99,8 +106,8 @@ export class MediaRepository implements IMediaRepository { .addOptions('-pass', '2') .addOptions('-passlogfile', output) .on('error', reject) - .on('end', () => fs.unlink(`${output}-0.log`)) - .on('end', () => fs.rm(`${output}-0.log.mbtree`, { force: true })) + .on('end', () => handlePromiseError(fs.unlink(`${output}-0.log`), this.logger)) + .on('end', () => handlePromiseError(fs.rm(`${output}-0.log.mbtree`, { force: true }), this.logger)) .on('end', resolve) .run(); }) diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 3a7ec2946..14c847ef6 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -171,16 +171,17 @@ export class PersonRepository implements IPersonRepository { @GenerateSql({ params: [DummyValue.UUID] }) async getStatistics(personId: string): Promise { + const items = await this.assetFaceRepository + .createQueryBuilder('face') + .leftJoin('face.asset', 'asset') + .where('face.personId = :personId', { personId }) + .andWhere('asset.isArchived = false') + .andWhere('asset.deletedAt IS NULL') + .andWhere('asset.livePhotoVideoId IS NULL') + .select('COUNT(DISTINCT(asset.id))', 'count') + .getRawOne(); return { - assets: await this.assetFaceRepository - .createQueryBuilder('face') - .leftJoin('face.asset', 'asset') - .where('face.personId = :personId', { personId }) - .andWhere('asset.isArchived = false') - .andWhere('asset.deletedAt IS NULL') - .andWhere('asset.livePhotoVideoId IS NULL') - .distinct(true) - .getCount(), + assets: items.count ?? 0, }; } @@ -222,9 +223,13 @@ export class PersonRepository implements IPersonRepository { .having('COUNT(face.assetId) != 0') .getRawOne(); + if (items == undefined) { + return { total: 0, hidden: 0 }; + } + const result: PeopleStatistics = { - total: items ? Number.parseInt(items.total) : 0, - hidden: items ? Number.parseInt(items.hidden) : 0, + total: items.total ?? 0, + hidden: items.hidden ?? 0, }; return result; diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql index e5cf6771f..54992e5f8 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/infra/sql/asset.repository.sql @@ -395,6 +395,39 @@ ORDER BY LIMIT 1 +-- AssetRepository.getPathsNotInLibrary +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 + ); + +-- AssetRepository.updateOfflineLibraryAssets +UPDATE "assets" +SET + "isOffline" = $1, + "updatedAt" = CURRENT_TIMESTAMP +WHERE + ( + "libraryId" = $2 + AND NOT ("originalPath" IN ($3)) + AND "isOffline" = $4 + ) + -- AssetRepository.getAllByFileCreationDate SELECT "asset"."id" AS "asset_id", diff --git a/server/src/infra/sql/person.repository.sql b/server/src/infra/sql/person.repository.sql index c2cc45ee8..b6a513ff9 100644 --- a/server/src/infra/sql/person.repository.sql +++ b/server/src/infra/sql/person.repository.sql @@ -224,8 +224,8 @@ LIMIT 20 -- PersonRepository.getStatistics -SELECT DISTINCT - COUNT(DISTINCT ("face"."id")) AS "cnt" +SELECT + COUNT(DISTINCT ("asset"."id")) AS "count" FROM "asset_faces" "face" LEFT JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" diff --git a/server/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql index c45d90a7a..48a7fc8e5 100644 --- a/server/src/infra/sql/search.repository.sql +++ b/server/src/infra/sql/search.repository.sql @@ -83,10 +83,6 @@ FROM "asset"."isFavorite" = $3 AND "asset"."isArchived" = $4 ) - AND ( - "stack"."primaryAssetId" = "asset"."id" - OR "asset"."stackId" IS NULL - ) ) AND ("asset"."deletedAt" IS NULL) ) "distinctAlias" @@ -184,10 +180,6 @@ WHERE "asset"."isFavorite" = $3 AND "asset"."isArchived" = $4 ) - AND ( - "stack"."primaryAssetId" = "asset"."id" - OR "asset"."stackId" IS NULL - ) AND "asset"."ownerId" IN ($5) ) AND ("asset"."deletedAt" IS NULL) diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index df1d9938b..623538e59 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -40,6 +40,7 @@ export class AppService { async init() { await this.databaseService.init(); await this.configService.init(); + await this.libraryService.init(); await this.jobService.init({ [JobName.ASSET_DELETION]: (data) => this.assetService.handleAssetDeletion(data), [JobName.ASSET_DELETION_CHECK]: () => this.assetService.handleAssetDeletionCheck(), @@ -86,6 +87,7 @@ export class AppService { } async teardown() { + await this.libraryService.teardown(); await this.metadataService.teardown(); } } diff --git a/server/src/test-utils/utils.ts b/server/src/test-utils/utils.ts index e5566f95d..cf9822295 100644 --- a/server/src/test-utils/utils.ts +++ b/server/src/test-utils/utils.ts @@ -1,4 +1,4 @@ -import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName } from '@app/domain'; +import { IJobRepository, IMediaRepository, JobItem, JobItemHandler, QueueName, StorageEventType } from '@app/domain'; import { AppModule } from '@app/immich'; import { InfraModule, InfraTestModule, dataSource } from '@app/infra'; import { MediaRepository } from '@app/infra/repositories'; @@ -48,6 +48,9 @@ export const db = { if (deleteUsers) { await em.query(`DELETE FROM "users" CASCADE;`); } + + // Release all locks + await em.query('SELECT pg_advisory_unlock_all()'); }); }, disconnect: async () => { @@ -75,22 +78,22 @@ class JobMock implements IJobRepository { async resume() {} async empty() {} async setConcurrency() {} - async getQueueStatus() { - return null as any; + getQueueStatus() { + return Promise.resolve(null) as any; } - async getJobCounts() { - return null as any; + getJobCounts() { + return Promise.resolve(null) as any; } async pause() {} - async clear() { - return []; + clear() { + return Promise.resolve([]); } async waitForQueueCompletion() {} } class MediaMockRepository extends MediaRepository { - async generateThumbhash() { - return Buffer.from('mock-thumbhash'); + generateThumbhash() { + return Promise.resolve(Buffer.from('mock-thumbhash')); } } @@ -124,34 +127,37 @@ export const testApp = { }, reset: async (options?: ResetOptions) => { await db.reset(options); - await app.get(AppService).init(); - - await app.get(MicroAppService).init(); }, get: (member: any) => app.get(member), teardown: async () => { if (app) { await app.get(MicroAppService).teardown(); - await app.get(AppService).teardown(); await app.close(); } await db.disconnect(); }, }; -export function waitForEvent(emitter: EventEmitter, event: string): Promise { - return new Promise((resolve, reject) => { - const success = (value: T) => { - emitter.off('error', fail); - resolve(value); - }; - const fail = (error: Error) => { - emitter.off(event, success); - reject(error); - }; - emitter.once(event, success); - emitter.once('error', fail); - }); +export function waitForEvent(emitter: EventEmitter, event: string, times = 1): Promise { + const promises: Promise[] = []; + + for (let i = 1; i <= times; i++) { + promises.push( + new Promise((resolve, reject) => { + const success = (value: any) => { + emitter.off(StorageEventType.ERROR, fail); + resolve(value); + }; + const fail = (error: Error) => { + emitter.off(event, success); + reject(error); + }; + emitter.once(event, success); + emitter.once(StorageEventType.ERROR, fail); + }), + ); + } + return Promise.all(promises); } const directoryExists = async (dirPath: string) => diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 36f646af6..ea1617d6a 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -16,7 +16,7 @@ export const assetStackStub = (stackId: string, assets: AssetEntity[]): AssetSta export const assetStub = { noResizePath: Object.freeze({ id: 'asset-id', - originalFileName: 'IMG_123', + originalFileName: 'IMG_123.jpg', deviceAssetId: 'device-asset-id', fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'), fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -77,7 +77,7 @@ export const assetStub = { livePhotoVideoId: null, tags: [], sharedLinks: [], - originalFileName: 'IMG_456', + originalFileName: 'IMG_456.jpg', faces: [], sidecarPath: null, isReadOnly: false, @@ -482,6 +482,9 @@ export const assetStub = { latitude: 100, longitude: 100, fileSizeInByte: 23_456, + city: 'test-city', + state: 'test-state', + country: 'test-country', } as ExifEntity, deletedAt: null, }), diff --git a/server/test/fixtures/index.ts b/server/test/fixtures/index.ts index 7a25c159a..2217c9b1f 100644 --- a/server/test/fixtures/index.ts +++ b/server/test/fixtures/index.ts @@ -10,7 +10,6 @@ export * from './library.stub'; export * from './media.stub'; export * from './partner.stub'; export * from './person.stub'; -export * from './search.stub'; export * from './shared-link.stub'; export * from './system-config.stub'; export * from './tag.stub'; diff --git a/server/test/fixtures/search.stub.ts b/server/test/fixtures/search.stub.ts deleted file mode 100644 index fc197d94f..000000000 --- a/server/test/fixtures/search.stub.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { SearchResult } from '@app/domain'; -import { AssetEntity, ExifEntity, SmartInfoEntity } from '@app/infra/entities'; -import { assetStub } from '.'; - -export const searchStub = { - emptyResults: Object.freeze>({ - total: 0, - count: 0, - page: 1, - items: [], - facets: [], - distances: [], - }), - - withImage: Object.freeze>({ - total: 1, - count: 1, - page: 1, - items: [assetStub.image], - facets: [], - distances: [], - }), - - exif: Object.freeze>({ - latitude: 90, - longitude: 90, - city: 'Immich', - state: 'Nebraska', - country: 'United States', - make: 'Canon', - model: 'EOS Rebel T7', - lensModel: 'Fancy lens', - }), - - smartInfo: Object.freeze>({ objects: ['car', 'tree'], tags: ['accident'] }), -}; diff --git a/server/test/fixtures/system-config.stub.ts b/server/test/fixtures/system-config.stub.ts index 0e99fb07a..9f9f02144 100644 --- a/server/test/fixtures/system-config.stub.ts +++ b/server/test/fixtures/system-config.stub.ts @@ -27,6 +27,7 @@ export const systemConfigStub: Record = { { key: SystemConfigKey.OAUTH_AUTO_REGISTER, value: true }, { key: SystemConfigKey.OAUTH_DEFAULT_STORAGE_QUOTA, value: 1 }, ], + deleteDelay30: [{ key: SystemConfigKey.USER_DELETE_DELAY, value: 30 }], libraryWatchEnabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: true }], libraryWatchDisabled: [{ key: SystemConfigKey.LIBRARY_WATCH_ENABLED, value: false }], }; diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 0be384b3a..63f1229a2 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -23,6 +23,8 @@ export const newAssetRepositoryMock = (): jest.Mocked => { updateAll: jest.fn(), getByLibraryId: jest.fn(), getByLibraryIdAndOriginalPath: jest.fn(), + updateOfflineLibraryAssets: jest.fn(), + getPathsNotInLibrary: jest.fn(), deleteAll: jest.fn(), save: jest.fn(), remove: jest.fn(), diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index f5a4d39a6..19e2df17a 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -13,6 +13,7 @@ export const newDatabaseRepositoryMock = (): jest.Mocked => shouldReindex: jest.fn(), runMigrations: jest.fn(), withLock: jest.fn().mockImplementation((_, function_: () => Promise) => function_()), + tryLock: jest.fn(), isBusy: jest.fn(), wait: jest.fn(), }; diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 1ee57b78d..e0b244fc2 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -1,9 +1,9 @@ -import { IStorageRepository, StorageCore, WatchEvents } from '@app/domain'; +import { IStorageRepository, StorageCore, StorageEventType, WatchEvents } from '@app/domain'; import { WatchOptions } from 'chokidar'; interface MockWatcherOptions { items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>; - close?: () => void; + close?: () => Promise; } export const makeMockWatcher = @@ -12,24 +12,29 @@ export const makeMockWatcher = events.onReady?.(); for (const item of items || []) { switch (item.event) { - case 'add': { + case StorageEventType.ADD: { events.onAdd?.(item.value); break; } - case 'change': { + case StorageEventType.CHANGE: { events.onChange?.(item.value); break; } - case 'unlink': { + case StorageEventType.UNLINK: { events.onUnlink?.(item.value); break; } - case 'error': { + case StorageEventType.ERROR: { events.onError?.(new Error(item.value)); } } } - return () => close?.(); + + if (close) { + return () => close(); + } + + return () => Promise.resolve(); }; export const newStorageRepositoryMock = (reset = true): jest.Mocked => { diff --git a/web/package-lock.json b/web/package-lock.json index 023705f7e..5819c0eb7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.97.0", + "version": "1.98.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.97.0", + "version": "1.98.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", @@ -63,7 +63,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.92.1", + "version": "1.98.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@oazapfts/runtime": "^1.0.0", @@ -6934,9 +6934,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.1.tgz", - "integrity": "sha512-ENAPbIxASf2R79IZwgkG5sBdeNA9kLRlXVvKKmTXh79zWTy0KKoT86XO2pHrTitUPINd+iXWy12MRmgzKGVckA==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.2.tgz", + "integrity": "sha512-ZzzE/wMuf48/1+Lf2Ffko0uDa6pyCfgHV6+uAhtg2U0AAXGrhCSW88vEJNAkAxW5qyrFY1y1zZ4J8TgHrjW++Q==", "dev": true, "peerDependencies": { "prettier": "^3.0.0", @@ -8036,9 +8036,9 @@ } }, "node_modules/svelte-check": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.5.tgz", - "integrity": "sha512-5aLgoQEdadvp8ypvKQ2avhnQ+V9YPQQaWrTFlXFw5g/v8xIQBvo+X/WqxTyD+V/ItDqXg3+abUA53rdDHgUjCA==", + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.6.tgz", + "integrity": "sha512-b9q9rOHOMYF3U8XllK7LmXTq1LeWQ98waGfEJzrFutViadkNl1tgdEtxIQ8yuPx+VQ4l7YrknYol+0lfZocaZw==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -8047,7 +8047,7 @@ "import-fresh": "^3.2.1", "picocolors": "^1.0.0", "sade": "^1.7.4", - "svelte-preprocess": "^5.1.0", + "svelte-preprocess": "^5.1.3", "typescript": "^5.0.3" }, "bin": { diff --git a/web/package.json b/web/package.json index b82b3bfc4..763f4ebe7 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.97.0", + "version": "1.98.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", diff --git a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte index 1046b7ef6..63065a0d9 100644 --- a/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte +++ b/web/src/lib/components/admin-page/delete-confirm-dialoge.svelte @@ -2,6 +2,7 @@ import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte'; import { handleError } from '$lib/utils/handle-error'; import { deleteUser, type UserResponseDto } from '@immich/sdk'; + import { serverConfig } from '$lib/stores/server-config.store'; import { createEventDispatcher } from 'svelte'; export let user: UserResponseDto; @@ -9,6 +10,7 @@ const dispatch = createEventDispatcher<{ success: void; fail: void; + cancel: void; }>(); const handleDeleteUser = async () => { @@ -26,11 +28,16 @@ }; - + dispatch('cancel')} +>

- {user.name}'s account and assets will be permanently deleted after 7 days. + {user.name}'s account and assets will be permanently deleted after {$serverConfig.userDeleteDelay} days.

Are you sure you want to continue?

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 edf5c4830..15b91c98b 100644 --- a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -148,8 +148,8 @@ {#if confirmJob} (confirmJob = null)} + {onConfirm} + onClose={() => (confirmJob = null)} /> {/if} diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte index c585d60e9..d9a8ed3bc 100644 --- a/web/src/lib/components/admin-page/restore-dialoge.svelte +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -8,6 +8,7 @@ const dispatch = createEventDispatcher<{ success: void; fail: void; + cancel: void; }>(); const handleRestoreUser = async () => { @@ -24,8 +25,8 @@ title="Restore User" confirmText="Continue" confirmColor="green" - on:confirm={handleRestoreUser} - on:cancel + onConfirm={handleRestoreUser} + onClose={() => dispatch('cancel')} >

{user.name}'s account will be restored.

diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index ac3512c6c..e8fabbe9c 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -102,6 +102,9 @@ {user.videos.toLocaleString($locale)} {asByteUnitString(user.usage, $locale, 0)} + {#if user.quotaSizeInBytes} + / {asByteUnitString(user.quotaSizeInBytes, $locale, 0)} + {/if} {#if user.quotaSizeInBytes} ({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%) diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 16b2afc7f..8f819f1eb 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -7,6 +7,7 @@ } from '$lib/components/shared-components/notification/notification'; import { handleError } from '$lib/utils/handle-error'; import { getConfig, getConfigDefaults, updateConfig, type SystemConfigDto } from '@immich/sdk'; + import { loadConfig } from '$lib/stores/server-config.store'; import { cloneDeep } from 'lodash-es'; import { createEventDispatcher, onMount } from 'svelte'; import type { SettingsEventType } from './admin-settings'; @@ -35,6 +36,8 @@ savedConfig = cloneDeep(newConfig); notificationController.show({ message: 'Settings saved', type: NotificationType.Info }); + await loadConfig(); + dispatch('save'); } catch (error) { handleError(error, 'Unable to save settings'); diff --git a/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte b/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte index b0c3e650d..e57d25398 100644 --- a/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte +++ b/web/src/lib/components/admin-page/settings/confirm-disable-login.svelte @@ -1,8 +1,11 @@ - +

Are you sure you want to disable all login methods? Login will be completely 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 ba24f3aab..b95a41acf 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 @@ -69,7 +69,7 @@ >

The name of a CLIP model listed here. Note that you - must re-run the 'Encode CLIP' job for all images upon changing a model. + must re-run the 'Smart Search' job for all images upon changing a model.

diff --git a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte index 217017447..387d7e470 100644 --- a/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte +++ b/web/src/lib/components/admin-page/settings/oauth/oauth-settings.svelte @@ -56,7 +56,7 @@ {#if isConfirmOpen} - handleConfirm(false)} on:confirm={() => handleConfirm(true)} /> + handleConfirm(false)} onConfirm={() => handleConfirm(true)} /> {/if}
diff --git a/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte b/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte index 8dc7323cf..2d3344dbe 100644 --- a/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte +++ b/web/src/lib/components/admin-page/settings/password-login/password-login-settings.svelte @@ -41,7 +41,7 @@ {#if isConfirmOpen} - handleConfirm(false)} on:confirm={() => handleConfirm(true)} /> + handleConfirm(false)} onConfirm={() => handleConfirm(true)} /> {/if}
diff --git a/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte b/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte new file mode 100644 index 000000000..81a93a409 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/user-settings/user-settings.svelte @@ -0,0 +1,45 @@ + + +
+
+
+
+ +
+ +
+ dispatch('reset', { ...detail, configKeys: ['user'] })} + on:save={() => dispatch('save', { user: config.user })} + showResetToDefault={!isEqual(savedConfig, defaultConfig)} + {disabled} + /> +
+
+
+
diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 508253c2c..8cca2f8c0 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -47,11 +47,11 @@ describe('AlbumCard component', () => { const detailsText = `${count} items` + (shared ? ' . Shared' : ''); expect(albumImgElement).toHaveAttribute('src'); - expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(albumImgElement).toHaveAttribute('alt', album.albumName); await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); - expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(albumImgElement).toHaveAttribute('alt', album.albumName); expect(sdkMock.getAssetThumbnail).not.toHaveBeenCalled(); expect(albumNameElement).toHaveTextContent(album.albumName); @@ -74,11 +74,11 @@ describe('AlbumCard component', () => { const albumImgElement = sut.getByTestId('album-image'); const albumNameElement = sut.getByTestId('album-name'); const albumDetailsElement = sut.getByTestId('album-details'); - expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(albumImgElement).toHaveAttribute('alt', album.albumName); await waitFor(() => expect(albumImgElement).toHaveAttribute('src', thumbnailUrl)); - expect(albumImgElement).toHaveAttribute('alt', album.id); + expect(albumImgElement).toHaveAttribute('alt', album.albumName); expect(sdkMock.getAssetThumbnail).toHaveBeenCalledTimes(1); expect(sdkMock.getAssetThumbnail).toHaveBeenCalledWith({ id: 'thumbnailIdOne', diff --git a/web/src/lib/components/album-page/album-card.svelte b/web/src/lib/components/album-page/album-card.svelte index e11ee18ba..8e54af18c 100644 --- a/web/src/lib/components/album-page/album-card.svelte +++ b/web/src/lib/components/album-page/album-card.svelte @@ -72,7 +72,7 @@ {album.id}(); - dispatch('close')}> + dispatch('close')}>
+ import { dateFormats } from '$lib/constants'; + import { locale } from '$lib/stores/preferences.store'; + import type { AlbumResponseDto } from '@immich/sdk'; + + export let album: AlbumResponseDto; + + $: startDate = formatDate(album.startDate); + $: endDate = formatDate(album.endDate); + + const formatDate = (date?: string) => { + return date ? new Date(date).toLocaleDateString($locale, dateFormats.album) : undefined; + }; + + const getDateRange = (start?: string, end?: string) => { + if (start && end && start !== end) { + return `${start} - ${end}`; + } + + if (start) { + return start; + } + + return ''; + }; + + + +

{getDateRange(startDate, endDate)}

+

ยท

+

{album.assetCount} items

+
diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 0e5abc1fa..9897125bc 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -3,11 +3,9 @@ import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; - import { locale } from '$lib/stores/preferences.store'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk'; import { onDestroy, onMount } from 'svelte'; - import { dateFormats } from '../../constants'; import { createAssetInteractionStore } from '../../stores/asset-interaction.store'; import { AssetStore } from '../../stores/assets.store'; import { downloadArchive } from '../../utils/asset-utils'; @@ -21,6 +19,7 @@ import { shouldIgnoreShortcut } from '$lib/utils/shortcut'; import { mdiFileImagePlusOutline, mdiFolderDownloadOutline } from '@mdi/js'; import { handlePromiseError } from '$lib/utils'; + import AlbumSummary from './album-summary.svelte'; export let sharedLink: SharedLinkResponseDto; export let user: UserResponseDto | undefined = undefined; @@ -40,31 +39,6 @@ } }); - const getDateRange = () => { - const { startDate, endDate } = album; - - let start = ''; - let end = ''; - - if (startDate) { - start = new Date(startDate).toLocaleDateString($locale, dateFormats.album); - } - - if (endDate) { - end = new Date(endDate).toLocaleDateString($locale, dateFormats.album); - } - - if (startDate && endDate && start !== end) { - return `${start} - ${end}`; - } - - if (start) { - return start; - } - - return ''; - }; - const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); onMount(() => { @@ -148,13 +122,8 @@ {album.albumName} - {#if album.assetCount > 0} - -

{getDateRange()}

-

ยท

-

{album.assetCount} items

-
+ {/if} diff --git a/web/src/lib/components/album-page/share-info-modal.svelte b/web/src/lib/components/album-page/share-info-modal.svelte index d26541764..db227b03a 100644 --- a/web/src/lib/components/album-page/share-info-modal.svelte +++ b/web/src/lib/components/album-page/share-info-modal.svelte @@ -129,8 +129,8 @@ title="Leave Album?" prompt="Are you sure you want to leave {album.albumName}?" confirmText="Leave" - on:confirm={handleRemoveUser} - on:cancel={() => (selectedRemoveUser = null)} + onConfirm={handleRemoveUser} + onClose={() => (selectedRemoveUser = null)} /> {/if} @@ -139,7 +139,7 @@ title="Remove User?" prompt="Are you sure you want to remove {selectedRemoveUser.name}" confirmText="Remove" - on:confirm={handleRemoveUser} - on:cancel={() => (selectedRemoveUser = null)} + onConfirm={handleRemoveUser} + onClose={() => (selectedRemoveUser = null)} /> {/if} diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index 4dd1b75e7..46305314b 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -195,7 +195,7 @@ comment-thumbnail
{/if} @@ -241,7 +241,7 @@ like-thumbnail
{/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index fe5d14d80..227fb1394 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -9,7 +9,6 @@ import { mdiAlertOutline, mdiArrowLeft, - mdiCloudDownloadOutline, mdiContentCopy, mdiDeleteOutline, mdiDotsVertical, @@ -20,6 +19,7 @@ mdiMagnifyPlusOutline, mdiMotionPauseOutline, mdiPlaySpeed, + mdiShareVariantOutline, } from '@mdi/js'; import { createEventDispatcher } from 'svelte'; import ContextMenu from '../shared-components/context-menu/context-menu.svelte'; @@ -32,12 +32,20 @@ export let isMotionPhotoPlaying = false; export let showDownloadButton: boolean; export let showDetailButton: boolean; + export let showShareButton: boolean; export let showSlideshow = false; export let hasStackChildren = false; $: isOwner = asset.ownerId === $user?.id; - type MenuItemEvent = 'addToAlbum' | 'addToSharedAlbum' | 'asProfileImage' | 'runJob' | 'playSlideShow' | 'unstack'; + type MenuItemEvent = + | 'addToAlbum' + | 'addToSharedAlbum' + | 'asProfileImage' + | 'download' + | 'playSlideShow' + | 'runJob' + | 'unstack'; const dispatch = createEventDispatcher<{ back: void; @@ -54,6 +62,7 @@ runJob: AssetJobName; playSlideShow: void; unstack: void; + showShareModal: void; }>(); let contextMenuPosition = { x: 0, y: 0 }; @@ -82,6 +91,14 @@ dispatch('back')} />
+ {#if showShareButton} + dispatch('showShareModal')} + title="Share" + /> + {/if} {#if asset.isOffline} {/if} - - {#if showDownloadButton} - dispatch('download')} - title="Download" - /> - {/if} {#if showDetailButton} onMenuClick('playSlideShow')} text="Slideshow" /> {/if} + {#if showDownloadButton} + onMenuClick('download')} text="Download" /> + {/if} onMenuClick('addToAlbum')} text="Add to Album" /> onMenuClick('addToSharedAlbum')} text="Add to Shared Album" /> diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index b8d25d8b6..5f32c000c 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -51,6 +51,7 @@ import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; import VideoViewer from './video-viewer.svelte'; + import CreateSharedLinkModal from '$lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; @@ -81,11 +82,13 @@ let appearsInAlbums: AlbumResponseDto[] = []; let isShowAlbumPicker = false; let isShowDeleteConfirmation = false; + let isShowShareModal = false; let addToSharedAlbum = true; let shouldPlayMotionPhoto = false; let isShowProfileImageCrop = false; let shouldShowDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; let shouldShowDetailButton = asset.hasMetadata; + let shouldShowShareModal = !asset.isTrashed; let canCopyImagesToClipboard: boolean; let slideshowStateUnsubscribe: () => void; let shuffleSlideshowUnsubscribe: () => void; @@ -292,6 +295,10 @@ isShowDeleteConfirmation = false; return; } + if (isShowShareModal) { + isShowShareModal = false; + return; + } closeViewer(); return; } @@ -563,6 +570,7 @@ showDetailButton={shouldShowDetailButton} showSlideshow={!!assetStore} hasStackChildren={$stackAssetsStore.length > 0} + showShareButton={shouldShowShareModal} on:back={closeViewer} on:showDetail={showDetailInfoHandler} on:download={() => downloadFile(asset)} @@ -577,6 +585,7 @@ on:runJob={({ detail: job }) => handleRunJob(job)} on:playSlideShow={() => ($slideshowState = SlideshowState.PlaySlideshow)} on:unstack={handleUnstack} + on:showShareModal={() => (isShowShareModal = true)} />
{/if} @@ -767,6 +776,10 @@ {#if isShowProfileImageCrop} (isShowProfileImageCrop = false)} /> {/if} + + {#if isShowShareModal} + (isShowShareModal = false)} /> + {/if}