diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a1cc0a114a..4ce7076011 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:fb211a0ea31a6177507498c084682aae8c9c31ca27668ea122246aa16a4723a0 +ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:7c2e711a4f7b02f32d2da16192d5e05aa7c95279be4ce889cff5df316f251c1d FROM ${BASEIMAGE} # Flutter SDK diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2baf6e9ae5..20da0c5201 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -177,13 +177,9 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - name: Any jobs failed? - if: ${{ contains(needs.*.result, 'failure') }} - run: exit 1 - - name: All jobs passed or skipped - if: ${{ !(contains(needs.*.result, 'failure')) }} - # zizmor: ignore[template-injection] - run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" + - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + with: + needs: ${{ toJSON(needs) }} success-check-ml: name: Docker Build & Push ML Success @@ -192,10 +188,6 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - name: Any jobs failed? - if: ${{ contains(needs.*.result, 'failure') }} - run: exit 1 - - name: All jobs passed or skipped - if: ${{ !(contains(needs.*.result, 'failure')) }} - # zizmor: ignore[template-injection] - run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" + - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + with: + needs: ${{ toJSON(needs) }} diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml index 754c0c38b3..62e84ac957 100644 --- a/.github/workflows/static_analysis.yml +++ b/.github/workflows/static_analysis.yml @@ -58,6 +58,14 @@ jobs: run: dart pub get working-directory: ./mobile + - name: Install DCM + run: | + sudo apt-get update + wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg + echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list + sudo apt-get update + sudo apt-get install dcm + - name: Generate translation file run: make translation working-directory: ./mobile @@ -100,6 +108,10 @@ jobs: run: dart run custom_lint working-directory: ./mobile + - name: Run DCM + run: dcm analyze lib + working-directory: ./mobile + zizmor: name: zizmor runs-on: ubuntu-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a0e8f51b06..186eb07761 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -479,13 +479,9 @@ jobs: runs-on: ubuntu-latest if: always() steps: - - name: Any jobs failed? - if: ${{ contains(needs.*.result, 'failure') }} - run: exit 1 - - name: All jobs passed or skipped - if: ${{ !(contains(needs.*.result, 'failure')) }} - # zizmor: ignore[template-injection] - run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" + - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + with: + needs: ${{ toJSON(needs) }} mobile-unit-tests: name: Unit Test Mobile diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml index b762ac636c..0758ef4dc9 100644 --- a/.github/workflows/weblate-lock.yml +++ b/.github/workflows/weblate-lock.yml @@ -52,10 +52,6 @@ jobs: permissions: {} if: always() steps: - - name: Any jobs failed? - if: ${{ contains(needs.*.result, 'failure') }} - run: exit 1 - - name: All jobs passed or skipped - if: ${{ !(contains(needs.*.result, 'failure')) }} - # zizmor: ignore[template-injection] - run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" + - uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3 + with: + needs: ${{ toJSON(needs) }} diff --git a/cli/package-lock.json b/cli/package-lock.json index 5373f3cdd1..7732e3fb71 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -27,7 +27,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -35,7 +35,7 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", @@ -61,7 +61,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "typescript": "^5.3.3" } }, @@ -79,21 +79,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -1353,22 +1338,15 @@ } }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", @@ -1878,9 +1856,9 @@ } }, "node_modules/builtin-modules": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", - "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", "dev": true, "license": "MIT", "engines": { @@ -2362,50 +2340,65 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz", - "integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==", + "version": "59.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz", + "integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", - "@eslint-community/eslint-utils": "^4.4.1", - "ci-info": "^4.1.0", + "@eslint-community/eslint-utils": "^4.5.1", + "@eslint/plugin-kit": "^0.2.7", + "ci-info": "^4.2.0", "clean-regexp": "^1.0.0", - "core-js-compat": "^3.40.0", + "core-js-compat": "^3.41.0", "esquery": "^1.6.0", - "globals": "^15.15.0", + "find-up-simple": "^1.0.1", + "globals": "^16.0.0", "indent-string": "^5.0.0", - "is-builtin-module": "^4.0.0", + "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", - "read-package-up": "^11.0.0", "regexp-tree": "^0.1.27", "regjsparser": "^0.12.0", "semver": "^7.7.1", "strip-indent": "^4.0.0" }, "engines": { - "node": ">=18.18" + "node": "^18.20.0 || ^20.10.0 || >=21.0.0" }, "funding": { "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=9.20.0" + "eslint": ">=9.22.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/eslint-scope": { @@ -2792,19 +2785,6 @@ "node": ">=8" } }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -2862,27 +2842,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/index-to-position": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-builtin-module": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", - "integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", "dev": true, "license": "MIT", "dependencies": { - "builtin-modules": "^4.0.0" + "builtin-modules": "^5.0.0" }, "engines": { "node": ">=18.20" @@ -3008,13 +2975,6 @@ "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -3272,21 +3232,6 @@ "dev": true, "license": "MIT" }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -3357,24 +3302,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3573,44 +3500,6 @@ ], "license": "MIT" }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -3809,42 +3698,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -4172,19 +4025,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", - "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -4229,19 +4069,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -4283,17 +4110,6 @@ "punycode": "^2.1.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", diff --git a/cli/package.json b/cli/package.json index fa5b8bc789..97c3448e31 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -29,7 +29,7 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index abcf3af728..1406062f55 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -82,7 +82,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:78ed1f9050eb9eaf766af6e580230b1c4965728650e332cd1ee918c0c4699775 + image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus @@ -94,7 +94,7 @@ services: command: [ './run.sh', '-disable-reporting' ] ports: - 3000:3000 - image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50 + image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055 volumes: - grafana-data:/var/lib/grafana diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 343295c60d..c49f9a761d 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,8 +15,8 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.15.21", - "@types/oidc-provider": "^8.5.1", + "@types/node": "^22.15.29", + "@types/oidc-provider": "^9.0.0", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", @@ -24,12 +24,12 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "exiftool-vendored": "^28.3.1", "globals": "^16.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", - "oidc-provider": "^8.5.1", + "oidc-provider": "^9.0.0", "pg": "^8.11.3", "pngjs": "^7.0.0", "prettier": "^3.2.5", @@ -66,7 +66,7 @@ "@types/lodash-es": "^4.17.12", "@types/micromatch": "^4.0.9", "@types/mock-fs": "^4.13.1", - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "@vitest/coverage-v8": "^3.0.0", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", @@ -74,7 +74,7 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", @@ -100,7 +100,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "typescript": "^5.3.3" } }, @@ -118,21 +118,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", @@ -1390,19 +1375,6 @@ "win32" ] }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -1410,19 +1382,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/@types/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", @@ -1520,13 +1479,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -1597,26 +1549,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/oidc-provider": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.8.1.tgz", - "integrity": "sha512-Yi/OJ7s0CFJ1AWAQrY2EO/zkV9uppLtiGAzrA07lBDveUOvxtYh7GflnHFXcgufVaPxVAjdykizjTYTMNVhdJw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-9.1.0.tgz", + "integrity": "sha512-UoC3ZQur+TtVL5hiUN8LoCbXocS2WI2eAPBtZtv1Y5F3vW0QTBawFAgDoctPqCQF73kah/Nzb5Gd3m5GtxFxiA==", "dev": true, "license": "MIT", "dependencies": { @@ -2301,9 +2246,9 @@ } }, "node_modules/builtin-modules": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", - "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", "dev": true, "license": "MIT", "engines": { @@ -2347,35 +2292,6 @@ "node": ">= 6.0.0" } }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2531,17 +2447,6 @@ "node": ">=0.8.0" } }, - "node_modules/co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", - "dev": true, - "license": "MIT", - "engines": { - "iojs": ">= 1.0.0", - "node": ">= 0.12.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2683,9 +2588,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2700,35 +2605,6 @@ } } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2753,16 +2629,6 @@ "dev": true, "license": "MIT" }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2866,9 +2732,9 @@ "license": "MIT" }, "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", "dev": true, "license": "MIT", "engines": { @@ -3153,50 +3019,65 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz", - "integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==", + "version": "59.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz", + "integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", - "@eslint-community/eslint-utils": "^4.4.1", - "ci-info": "^4.1.0", + "@eslint-community/eslint-utils": "^4.5.1", + "@eslint/plugin-kit": "^0.2.7", + "ci-info": "^4.2.0", "clean-regexp": "^1.0.0", - "core-js-compat": "^3.40.0", + "core-js-compat": "^3.41.0", "esquery": "^1.6.0", - "globals": "^15.15.0", + "find-up-simple": "^1.0.1", + "globals": "^16.0.0", "indent-string": "^5.0.0", - "is-builtin-module": "^4.0.0", + "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", - "read-package-up": "^11.0.0", "regexp-tree": "^0.1.27", "regjsparser": "^0.12.0", "semver": "^7.7.1", "strip-indent": "^4.0.0" }, "engines": { - "node": ">=18.18" + "node": "^18.20.0 || ^20.10.0 || >=21.0.0" }, "funding": { "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=9.20.0" + "eslint": ">=9.22.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/eslint-scope": { @@ -3551,16 +3432,6 @@ "node": ">= 6" } }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.17" - } - }, "node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -3760,19 +3631,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -3859,32 +3717,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", @@ -3961,19 +3793,6 @@ "he": "bin/he" } }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4032,13 +3851,6 @@ "node": ">= 0.6" } }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4056,33 +3868,6 @@ "node": ">= 0.8" } }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/http2-wrapper/node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -4160,19 +3945,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/index-to-position": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -4193,13 +3965,13 @@ "license": "ISC" }, "node_modules/is-builtin-module": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", - "integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", "dev": true, "license": "MIT", "dependencies": { - "builtin-modules": "^4.0.0" + "builtin-modules": "^5.0.0" }, "engines": { "node": ">=18.20" @@ -4228,25 +4000,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4270,25 +4023,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4376,13 +4110,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4454,9 +4181,9 @@ } }, "node_modules/koa": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.1.tgz", - "integrity": "sha512-umfX9d3iuSxTQP4pnzLOz0HKnPg0FaUUIKcye2lOiz3KPu1Y3M3xlz76dISdFPQs37P9eJz1wUpcTS6KDPn9fA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-3.0.0.tgz", + "integrity": "sha512-Usyqf1o+XN618R3Jzq4S4YWbKsRtPcGpgyHXD4APdGYQQyqQ59X+Oyc7fXHS2429stzLsBiDjj6zqqYe8kknfw==", "dev": true, "license": "MIT", "dependencies": { @@ -4464,28 +4191,24 @@ "cache-content-type": "^1.0.0", "content-disposition": "~0.5.2", "content-type": "^1.0.4", - "cookies": "~0.9.0", + "cookies": "~0.9.1", "debug": "^4.3.2", "delegates": "^1.0.0", - "depd": "^2.0.0", "destroy": "^1.0.4", - "encodeurl": "^1.0.2", + "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "fresh": "~0.5.2", "http-assert": "^1.3.0", - "http-errors": "^1.6.3", - "is-generator-function": "^1.0.7", + "http-errors": "^2.0.0", "koa-compose": "^4.1.0", - "koa-convert": "^2.0.0", "on-finished": "^2.3.0", - "only": "~0.0.2", "parseurl": "^1.3.2", - "statuses": "^1.5.0", - "type-is": "^1.6.16", + "statuses": "^2.0.1", + "type-is": "^2.0.1", "vary": "^1.1.2" }, "engines": { - "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + "node": ">= 18" } }, "node_modules/koa-compose": { @@ -4495,57 +4218,6 @@ "dev": true, "license": "MIT" }, - "node_modules/koa-convert": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", - "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", - "dev": true, - "license": "MIT", - "dependencies": { - "co": "^4.6.0", - "koa-compose": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/koa/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/koa/node_modules/http-errors/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/koa/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4590,19 +4262,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -4669,13 +4328,13 @@ } }, "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">= 0.8" } }, "node_modules/merge2": { @@ -4748,19 +4407,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -4928,34 +4574,6 @@ "node": ">=6" } }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/normalize-url": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", - "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -4980,16 +4598,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -5011,30 +4619,38 @@ "license": "MIT" }, "node_modules/oidc-provider": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-8.8.1.tgz", - "integrity": "sha512-qVChpayTwojUREJxLkFofUSK8kiSRIdzPrVSsoGibqRHl/YO60ege94OZS8vh7zaK+zxcG/Gu8UMaYB5ulohCQ==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/oidc-provider/-/oidc-provider-9.1.3.tgz", + "integrity": "sha512-DaiiCllAr+y33M2HzTRRpV7/jmcZBSfIXaDv0eHURIV85N0O+QUqtjVtPY+viCwHKBRGWwfShWWFKddPUxpAWw==", "dev": true, "license": "MIT", "dependencies": { "@koa/cors": "^5.0.0", "@koa/router": "^13.1.0", - "debug": "^4.4.0", + "debug": "^4.4.1", "eta": "^3.5.0", - "got": "^13.0.0", - "jose": "^5.9.6", + "jose": "^6.0.11", "jsesc": "^3.1.0", - "koa": "^2.15.4", - "nanoid": "^5.0.9", - "object-hash": "^3.0.0", - "oidc-token-hash": "^5.0.3", - "quick-lru": "^7.0.0", + "koa": "^3.0.0", + "nanoid": "^5.1.5", + "oidc-token-hash": "^5.1.0", + "quick-lru": "^7.0.1", "raw-body": "^3.0.0" }, "funding": { "url": "https://github.com/sponsors/panva" } }, + "node_modules/oidc-provider/node_modules/jose": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.0.11.tgz", + "integrity": "sha512-QxG7EaliDARm1O1S8BGakqncGT9s25bKL1WSf6/oa17Tkqwi8D2ZNglqCF+DsYF88/rV66Q/Q2mFAy697E1DUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/oidc-token-hash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.1.0.tgz", @@ -5068,12 +4684,6 @@ "wrappy": "1" } }, - "node_modules/only": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", - "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", - "dev": true - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5092,16 +4702,6 @@ "node": ">= 0.8.0" } }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5154,24 +4754,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -5724,44 +5306,6 @@ "node": ">= 0.8" } }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5813,13 +5357,6 @@ "node": ">=6" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "dev": true, - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5830,22 +5367,6 @@ "node": ">=4" } }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dev": true, - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -5981,24 +5502,6 @@ ], "license": "MIT" }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -6228,42 +5731,6 @@ "node": ">=0.10.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", @@ -6737,28 +6204,39 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", - "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", "dev": true, "license": "MIT", "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" }, "engines": { "node": ">= 0.6" @@ -6808,19 +6286,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -6894,17 +6359,6 @@ "node": ">=10.0.0" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/e2e/package.json b/e2e/package.json index 495b119ccf..9c69199f65 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,8 +25,8 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^22.15.21", - "@types/oidc-provider": "^8.5.1", + "@types/node": "^22.15.29", + "@types/oidc-provider": "^9.0.0", "@types/pg": "^8.15.1", "@types/pngjs": "^6.0.4", "@types/supertest": "^6.0.2", @@ -34,12 +34,12 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "exiftool-vendored": "^28.3.1", "globals": "^16.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", - "oidc-provider": "^8.5.1", + "oidc-provider": "^9.0.0", "pg": "^8.11.3", "pngjs": "^7.0.0", "prettier": "^3.2.5", diff --git a/i18n/en.json b/i18n/en.json index 2fbce6fbb7..1be84c5e7a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -22,6 +22,7 @@ "add_partner": "Add partner", "add_path": "Add path", "add_photos": "Add photos", + "add_tag": "Add tag", "add_to": "Add to…", "add_to_album": "Add to album", "add_to_album_bottom_sheet_added": "Added to {album}", @@ -169,7 +170,7 @@ "note_apply_storage_label_previous_assets": "Note: To apply the Storage Label to previously uploaded assets, run the", "note_cannot_be_changed_later": "NOTE: This cannot be changed later!", "notification_email_from_address": "From address", - "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \"", + "notification_email_from_address_description": "Sender email address, for example: \"Immich Photo Server \". Make sure to use an address you're allowed to send emails from.", "notification_email_host_description": "Host of the email server (e.g. smtp.immich.app)", "notification_email_ignore_certificate_errors": "Ignore certificate errors", "notification_email_ignore_certificate_errors_description": "Ignore TLS certificate validation errors (not recommended)", @@ -648,6 +649,7 @@ "confirm_password": "Confirm password", "confirm_tag_face": "Do you want to tag this face as {name}?", "confirm_tag_face_unnamed": "Do you want to tag this face?", + "connected_device": "Connected device", "connected_to": "Connected to", "contain": "Contain", "context": "Context", @@ -748,6 +750,7 @@ "disallow_edits": "Disallow edits", "discord": "Discord", "discover": "Discover", + "discovered_devices": "Discovered devices", "dismiss_all_errors": "Dismiss all errors", "dismiss_error": "Dismiss error", "display_options": "Display options", @@ -1132,6 +1135,7 @@ "list": "List", "loading": "Loading", "loading_search_results_failed": "Loading search results failed", + "local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server", "local_network": "Local network", "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", "location_permission": "Location permission", @@ -1272,6 +1276,7 @@ "no_archived_assets_message": "Archive photos and videos to hide them from your Photos view", "no_assets_message": "CLICK TO UPLOAD YOUR FIRST PHOTO", "no_assets_to_show": "No assets to show", + "no_cast_devices_found": "No cast devices found", "no_duplicates_found": "No duplicates were found.", "no_exif_info_available": "No exif info available", "no_explore_results_message": "Upload more photos to explore your collection.", @@ -1630,6 +1635,7 @@ "set_date_of_birth": "Set date of birth", "set_profile_picture": "Set profile picture", "set_slideshow_to_fullscreen": "Set Slideshow to fullscreen", + "set_stack_primary_asset": "Set as primary asset", "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": "Load original image", @@ -1767,6 +1773,7 @@ "start_date": "Start date", "state": "State", "status": "Status", + "stop_casting": "Stop casting", "stop_motion_photo": "Stop Motion Photo", "stop_photo_sharing": "Stop sharing your photos?", "stop_photo_sharing_description": "{partner} will no longer be able to access your photos.", diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index a462b5e696..1f4d7aa635 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:ab60e444e04215a62671149f24c59cc2893b49cb5dad26f9d139077a86be760e AS builder-cpu +FROM python:3.11-bookworm@sha256:d2621a9f74d31a8a60af19f97b09cc3ac54382c8680b6544018713a12ef6c048 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -59,7 +59,7 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ RUN apt-get update && apt-get install -y --no-install-recommends g++ -COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4a6c9444b126bd325fba904bff796bf91fb777bf6148d60109c4cb1de2ffc497 /uv /uvx /bin/ +COPY --from=ghcr.io/astral-sh/uv:latest@sha256:4faec156e35a5f345d57804d8858c6ba1cf6352ce5f4bffc11b7fdebdef46a38 /uv /uvx /bin/ RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ @@ -68,11 +68,11 @@ RUN if [ "$DEVICE" = "rocm" ]; then \ uv pip install /opt/onnxruntime_rocm-*.whl; \ fi -FROM python:3.11-slim-bookworm@sha256:97ef3198ec8c78690587167bb6a4905d00ffe053900687bdae93ad667e507cbb AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-cpu ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 -FROM python:3.11-slim-bookworm@sha256:97ef3198ec8c78690587167bb6a4905d00ffe053900687bdae93ad667e507cbb AS prod-openvino +FROM python:3.11-slim-bookworm@sha256:7a3ed1226224bcc1fe5443262363d42f48cf832a540c1836ba8ccbeaadf8637c AS prod-openvino RUN apt-get update && \ apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock index 22c6fad153..115ea259f3 100644 --- a/machine-learning/uv.lock +++ b/machine-learning/uv.lock @@ -346,11 +346,11 @@ wheels = [ [[package]] name = "configargparse" -version = "1.7" +version = "1.7.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/8a/73f1008adfad01cb923255b924b1528727b8270e67cb4ef41eabdc7d783e/ConfigArgParse-1.7.tar.gz", hash = "sha256:e7067471884de5478c58a511e529f0f9bd1c66bfef1dea90935438d6c23306d1", size = 43817, upload-time = "2023-07-23T16:20:04.95Z" } +sdist = { url = "https://files.pythonhosted.org/packages/85/4d/6c9ef746dfcc2a32e26f3860bb4a011c008c392b83eabdfb598d1a8bbe5d/configargparse-1.7.1.tar.gz", hash = "sha256:79c2ddae836a1e5914b71d58e4b9adbd9f7779d4e6351a637b7d2d9b6c46d3d9", size = 43958, upload-time = "2025-05-23T14:26:17.369Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6f/b3/b4ac838711fd74a2b4e6f746703cf9dd2cf5462d17dac07e349234e21b97/ConfigArgParse-1.7-py3-none-any.whl", hash = "sha256:d249da6591465c6c26df64a9f73d2536e743be2f244eb3ebe61114af2f94f86b", size = 25489, upload-time = "2023-07-23T16:20:03.27Z" }, + { url = "https://files.pythonhosted.org/packages/31/28/d28211d29bcc3620b1fece85a65ce5bb22f18670a03cd28ea4b75ede270c/configargparse-1.7.1-py3-none-any.whl", hash = "sha256:8b586a31f9d873abd1ca527ffbe58863c99f36d896e2829779803125e83be4b6", size = 25607, upload-time = "2025-05-23T14:26:15.923Z" }, ] [[package]] @@ -900,7 +900,7 @@ wheels = [ [[package]] name = "huggingface-hub" -version = "0.32.2" +version = "0.32.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, @@ -912,9 +912,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/76/44f7025d1b3f29336aeb7324a57dd7c19f7c69f6612b7637b39ac7c17302/huggingface_hub-0.32.2.tar.gz", hash = "sha256:64a288b1eadad6b60bbfd50f0e52fd6cfa2ef77ab13c3e8a834a038ae929de54", size = 422847, upload-time = "2025-05-27T09:23:00.306Z" } +sdist = { url = "https://files.pythonhosted.org/packages/60/c8/4f7d270285c46324fd66f62159eb16739aa5696f422dba57678a8c6b78e9/huggingface_hub-0.32.4.tar.gz", hash = "sha256:f61d45cd338736f59fb0e97550b74c24ee771bcc92c05ae0766b9116abe720be", size = 424494, upload-time = "2025-06-03T09:59:46.105Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/32/30/532fe57467a6cc7ff2e39f088db1cb6d6bf522f724a4a5c7beda1282d5a6/huggingface_hub-0.32.2-py3-none-any.whl", hash = "sha256:f8fcf14603237eadf96dbe577d30b330f8c27b4a0a31e8f6c94fdc25e021fdb8", size = 509968, upload-time = "2025-05-27T09:22:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/67/8b/222140f3cfb6f17b0dd8c4b9a0b36bd4ebefe9fb0098ba35d6960abcda0f/huggingface_hub-0.32.4-py3-none-any.whl", hash = "sha256:37abf8826b38d971f60d3625229221c36e53fe58060286db9baf619cfbf39767", size = 512101, upload-time = "2025-06-03T09:59:44.099Z" }, ] [[package]] @@ -1225,7 +1225,7 @@ wheels = [ [[package]] name = "locust" -version = "2.37.5" +version = "2.37.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, @@ -1245,25 +1245,26 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.11'" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/d1/60d5fddac2baa47314c091636868b50178a38fc71ce39d68afd847448028/locust-2.37.5.tar.gz", hash = "sha256:c90824c4cb6a01cdede220684c7c8381253fcca47fc689dbca4f6c46d740c99f", size = 2252000, upload-time = "2025-05-22T08:54:58.676Z" } +sdist = { url = "https://files.pythonhosted.org/packages/90/05/2bfdf19756c6a12f6f9513f75340ecf0595d83cab4d9fc91162225908e3d/locust-2.37.9.tar.gz", hash = "sha256:e43673b594ec5ecde4f9ba6e0d5c66c00d7c0ae93591951abe83e8d186c67175", size = 2252507, upload-time = "2025-06-05T09:26:58.186Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e0/a0/32a51fb48f96b0de6bb6ea7308f68b7ae1bae53e6b975672f8c4ef7f8c08/locust-2.37.5-py3-none-any.whl", hash = "sha256:9922a2718b42f1c57a05c822e47b66555b3c61292694ec5edaf7a166fac6d112", size = 2268626, upload-time = "2025-05-22T08:54:55.938Z" }, + { url = "https://files.pythonhosted.org/packages/33/1c/0ece4176231c578e819d970ec08d124492833e50aafd171c582bcc414446/locust-2.37.9-py3-none-any.whl", hash = "sha256:e17da439f3a252d1fb6d4c34daf00d7e8b87e99d833a32e8a79f4f8ebb07767d", size = 2269084, upload-time = "2025-06-05T09:26:56.257Z" }, ] [[package]] name = "locust-cloud" -version = "1.21.8" +version = "1.23.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "configargparse" }, { name = "gevent" }, { name = "platformdirs" }, + { name = "python-engineio" }, { name = "python-socketio", extra = ["client"] }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/09/d4/64a169b4831d26ab9dceacb192ea30c749501d87b4958e628cf1f7ef45c4/locust_cloud-1.21.8.tar.gz", hash = "sha256:e8bde0da013c8731a45cc834cdf9fec2fc21738a5f2807d93c2c5eeb3008a80e", size = 450414, upload-time = "2025-05-22T08:30:27.458Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/7c/d9cbbd051490aeedfbd6ddda8ad48f77dd848ee490f6ebd166d20db5911e/locust_cloud-1.23.1.tar.gz", hash = "sha256:a09161752b8c9a9205e97cef5223ee3ad967bc2d91c52d61952aaa3da6802a55", size = 450937, upload-time = "2025-06-05T06:07:53.773Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/76/aa8b2f73bdf7de5ee344e5d0c4749e8d62ff38257b41d9df37b0b7ac84e2/locust_cloud-1.21.8-py3-none-any.whl", hash = "sha256:4f06b5d8a26ba91840a768008f4870965b13cc71481de9797409556de2edc007", size = 407879, upload-time = "2025-05-22T08:30:25.512Z" }, + { url = "https://files.pythonhosted.org/packages/7e/01/5af43edee540e38ba0ee0a2e3beb72c50073e0f646bb543a8b34650315e3/locust_cloud-1.23.1-py3-none-any.whl", hash = "sha256:11677895c6ed6d0beef1b425a6f04f10ea2cfcaeaefbf00a97fb3c9134296e54", size = 408323, upload-time = "2025-06-05T06:07:51.947Z" }, ] [[package]] @@ -1414,40 +1415,41 @@ wheels = [ [[package]] name = "mypy" -version = "1.15.0" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, + { name = "pathspec" }, { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717, upload-time = "2025-02-05T03:50:34.655Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d4/38/13c2f1abae94d5ea0354e146b95a1be9b2137a0d506728e0da037c4276f6/mypy-1.16.0.tar.gz", hash = "sha256:84b94283f817e2aa6350a14b4a8fb2a35a53c286f97c9d30f53b63620e7af8ab", size = 3323139, upload-time = "2025-05-29T13:46:12.532Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433, upload-time = "2025-02-05T03:49:29.145Z" }, - { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472, upload-time = "2025-02-05T03:49:16.986Z" }, - { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424, upload-time = "2025-02-05T03:49:46.908Z" }, - { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450, upload-time = "2025-02-05T03:50:05.89Z" }, - { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765, upload-time = "2025-02-05T03:49:33.56Z" }, - { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701, upload-time = "2025-02-05T03:49:38.981Z" }, - { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338, upload-time = "2025-02-05T03:50:17.287Z" }, - { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540, upload-time = "2025-02-05T03:49:51.21Z" }, - { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051, upload-time = "2025-02-05T03:50:20.885Z" }, - { url = "https://files.pythonhosted.org/packages/e9/9a/1f7d18b30edd57441a6411fcbc0c6869448d1a4bacbaee60656ac0fc29c8/mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c", size = 12286751, upload-time = "2025-02-05T03:49:42.408Z" }, - { url = "https://files.pythonhosted.org/packages/72/af/19ff499b6f1dafcaf56f9881f7a965ac2f474f69f6f618b5175b044299f5/mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f", size = 12421783, upload-time = "2025-02-05T03:49:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/96/39/11b57431a1f686c1aed54bf794870efe0f6aeca11aca281a0bd87a5ad42c/mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f", size = 9265618, upload-time = "2025-02-05T03:49:54.581Z" }, - { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981, upload-time = "2025-02-05T03:50:28.25Z" }, - { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175, upload-time = "2025-02-05T03:50:13.411Z" }, - { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675, upload-time = "2025-02-05T03:50:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020, upload-time = "2025-02-05T03:48:48.705Z" }, - { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582, upload-time = "2025-02-05T03:49:03.628Z" }, - { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614, upload-time = "2025-02-05T03:50:00.313Z" }, - { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592, upload-time = "2025-02-05T03:48:55.789Z" }, - { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611, upload-time = "2025-02-05T03:48:44.581Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443, upload-time = "2025-02-05T03:49:25.514Z" }, - { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541, upload-time = "2025-02-05T03:49:57.623Z" }, - { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348, upload-time = "2025-02-05T03:48:52.361Z" }, - { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648, upload-time = "2025-02-05T03:49:11.395Z" }, - { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777, upload-time = "2025-02-05T03:50:08.348Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/a0485f0608a3d67029d3d73cec209278b025e3493a3acfda3ef3a88540fd/mypy-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7909541fef256527e5ee9c0a7e2aeed78b6cda72ba44298d1334fe7881b05c5c", size = 10967416, upload-time = "2025-05-29T13:34:17.783Z" }, + { url = "https://files.pythonhosted.org/packages/4b/53/5837c221f74c0d53a4bfc3003296f8179c3a2a7f336d7de7bbafbe96b688/mypy-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e71d6f0090c2256c713ed3d52711d01859c82608b5d68d4fa01a3fe30df95571", size = 10087654, upload-time = "2025-05-29T13:32:37.878Z" }, + { url = "https://files.pythonhosted.org/packages/29/59/5fd2400352c3093bed4c09017fe671d26bc5bb7e6ef2d4bf85f2a2488104/mypy-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:936ccfdd749af4766be824268bfe22d1db9eb2f34a3ea1d00ffbe5b5265f5491", size = 11875192, upload-time = "2025-05-29T13:34:54.281Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3e/4bfec74663a64c2012f3e278dbc29ffe82b121bc551758590d1b6449ec0c/mypy-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4086883a73166631307fdd330c4a9080ce24913d4f4c5ec596c601b3a4bdd777", size = 12612939, upload-time = "2025-05-29T13:33:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/88/1f/fecbe3dcba4bf2ca34c26ca016383a9676711907f8db4da8354925cbb08f/mypy-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:feec38097f71797da0231997e0de3a58108c51845399669ebc532c815f93866b", size = 12874719, upload-time = "2025-05-29T13:21:52.09Z" }, + { url = "https://files.pythonhosted.org/packages/f3/51/c2d280601cd816c43dfa512a759270d5a5ef638d7ac9bea9134c8305a12f/mypy-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:09a8da6a0ee9a9770b8ff61b39c0bb07971cda90e7297f4213741b48a0cc8d93", size = 9487053, upload-time = "2025-05-29T13:33:29.797Z" }, + { url = "https://files.pythonhosted.org/packages/24/c4/ff2f79db7075c274fe85b5fff8797d29c6b61b8854c39e3b7feb556aa377/mypy-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9f826aaa7ff8443bac6a494cf743f591488ea940dd360e7dd330e30dd772a5ab", size = 10884498, upload-time = "2025-05-29T13:18:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/02/07/12198e83006235f10f6a7808917376b5d6240a2fd5dce740fe5d2ebf3247/mypy-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82d056e6faa508501af333a6af192c700b33e15865bda49611e3d7d8358ebea2", size = 10011755, upload-time = "2025-05-29T13:34:00.851Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9b/5fd5801a72b5d6fb6ec0105ea1d0e01ab2d4971893076e558d4b6d6b5f80/mypy-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:089bedc02307c2548eb51f426e085546db1fa7dd87fbb7c9fa561575cf6eb1ff", size = 11800138, upload-time = "2025-05-29T13:32:55.082Z" }, + { url = "https://files.pythonhosted.org/packages/2e/81/a117441ea5dfc3746431e51d78a4aca569c677aa225bca2cc05a7c239b61/mypy-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6a2322896003ba66bbd1318c10d3afdfe24e78ef12ea10e2acd985e9d684a666", size = 12533156, upload-time = "2025-05-29T13:19:12.963Z" }, + { url = "https://files.pythonhosted.org/packages/3f/38/88ec57c6c86014d3f06251e00f397b5a7daa6888884d0abf187e4f5f587f/mypy-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:021a68568082c5b36e977d54e8f1de978baf401a33884ffcea09bd8e88a98f4c", size = 12742426, upload-time = "2025-05-29T13:20:22.72Z" }, + { url = "https://files.pythonhosted.org/packages/bd/53/7e9d528433d56e6f6f77ccf24af6ce570986c2d98a5839e4c2009ef47283/mypy-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:54066fed302d83bf5128632d05b4ec68412e1f03ef2c300434057d66866cea4b", size = 9478319, upload-time = "2025-05-29T13:21:17.582Z" }, + { url = "https://files.pythonhosted.org/packages/70/cf/158e5055e60ca2be23aec54a3010f89dcffd788732634b344fc9cb1e85a0/mypy-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c5436d11e89a3ad16ce8afe752f0f373ae9620841c50883dc96f8b8805620b13", size = 11062927, upload-time = "2025-05-29T13:35:52.328Z" }, + { url = "https://files.pythonhosted.org/packages/94/34/cfff7a56be1609f5d10ef386342ce3494158e4d506516890142007e6472c/mypy-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f2622af30bf01d8fc36466231bdd203d120d7a599a6d88fb22bdcb9dbff84090", size = 10083082, upload-time = "2025-05-29T13:35:33.378Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7f/7242062ec6288c33d8ad89574df87c3903d394870e5e6ba1699317a65075/mypy-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d045d33c284e10a038f5e29faca055b90eee87da3fc63b8889085744ebabb5a1", size = 11828306, upload-time = "2025-05-29T13:21:02.164Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5f/b392f7b4f659f5b619ce5994c5c43caab3d80df2296ae54fa888b3d17f5a/mypy-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4968f14f44c62e2ec4a038c8797a87315be8df7740dc3ee8d3bfe1c6bf5dba8", size = 12702764, upload-time = "2025-05-29T13:20:42.826Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c0/7646ef3a00fa39ac9bc0938626d9ff29d19d733011be929cfea59d82d136/mypy-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eb14a4a871bb8efb1e4a50360d4e3c8d6c601e7a31028a2c79f9bb659b63d730", size = 12896233, upload-time = "2025-05-29T13:18:37.446Z" }, + { url = "https://files.pythonhosted.org/packages/6d/38/52f4b808b3fef7f0ef840ee8ff6ce5b5d77381e65425758d515cdd4f5bb5/mypy-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:bd4e1ebe126152a7bbaa4daedd781c90c8f9643c79b9748caa270ad542f12bec", size = 9565547, upload-time = "2025-05-29T13:20:02.836Z" }, + { url = "https://files.pythonhosted.org/packages/97/9c/ca03bdbefbaa03b264b9318a98950a9c683e06472226b55472f96ebbc53d/mypy-1.16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a9e056237c89f1587a3be1a3a70a06a698d25e2479b9a2f57325ddaaffc3567b", size = 11059753, upload-time = "2025-05-29T13:18:18.167Z" }, + { url = "https://files.pythonhosted.org/packages/36/92/79a969b8302cfe316027c88f7dc6fee70129490a370b3f6eb11d777749d0/mypy-1.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b07e107affb9ee6ce1f342c07f51552d126c32cd62955f59a7db94a51ad12c0", size = 10073338, upload-time = "2025-05-29T13:19:48.079Z" }, + { url = "https://files.pythonhosted.org/packages/14/9b/a943f09319167da0552d5cd722104096a9c99270719b1afeea60d11610aa/mypy-1.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6fb60cbd85dc65d4d63d37cb5c86f4e3a301ec605f606ae3a9173e5cf34997b", size = 11827764, upload-time = "2025-05-29T13:46:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/ec/64/ff75e71c65a0cb6ee737287c7913ea155845a556c64144c65b811afdb9c7/mypy-1.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7e32297a437cc915599e0578fa6bc68ae6a8dc059c9e009c628e1c47f91495d", size = 12701356, upload-time = "2025-05-29T13:35:13.553Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ad/0e93c18987a1182c350f7a5fab70550852f9fabe30ecb63bfbe51b602074/mypy-1.16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:afe420c9380ccec31e744e8baff0d406c846683681025db3531b32db56962d52", size = 12900745, upload-time = "2025-05-29T13:17:24.409Z" }, + { url = "https://files.pythonhosted.org/packages/28/5d/036c278d7a013e97e33f08c047fe5583ab4f1fc47c9a49f985f1cdd2a2d7/mypy-1.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:55f9076c6ce55dd3f8cd0c6fff26a008ca8e5131b89d5ba6d86bd3f47e736eeb", size = 9572200, upload-time = "2025-05-29T13:33:44.92Z" }, + { url = "https://files.pythonhosted.org/packages/99/a3/6ed10530dec8e0fdc890d81361260c9ef1f5e5c217ad8c9b21ecb2b8366b/mypy-1.16.0-py3-none-any.whl", hash = "sha256:29e1499864a3888bca5c1542f2d7232c6e586295183320caa95758fc84034031", size = 2265773, upload-time = "2025-05-29T13:35:18.762Z" }, ] [[package]] @@ -1975,7 +1977,7 @@ wheels = [ [[package]] name = "pytest" -version = "8.3.5" +version = "8.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1983,23 +1985,24 @@ dependencies = [ { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, + { name = "pygments" }, { name = "tomli", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/aa/405082ce2749be5398045152251ac69c0f3578c7077efc53431303af97ce/pytest-8.4.0.tar.gz", hash = "sha256:14d920b48472ea0dbf68e45b96cd1ffda4705f33307dcc86c676c1b5104838a6", size = 1515232, upload-time = "2025-06-02T17:36:30.03Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" }, + { url = "https://files.pythonhosted.org/packages/2f/de/afa024cbe022b1b318a3d224125aa24939e99b4ff6f22e0ba639a2eaee47/pytest-8.4.0-py3-none-any.whl", hash = "sha256:f40f825768ad76c0977cbacdf1fd37c6f7a468e460ea6a0636078f8972d4517e", size = 363797, upload-time = "2025-06-02T17:36:27.859Z" }, ] [[package]] name = "pytest-asyncio" -version = "0.26.0" +version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, + { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, ] [[package]] @@ -2017,14 +2020,14 @@ wheels = [ [[package]] name = "pytest-mock" -version = "3.14.0" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c6/90/a955c3ab35ccd41ad4de556596fa86685bf4fc5ffcc62d22d856cfd4e29a/pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0", size = 32814, upload-time = "2024-03-21T22:14:04.964Z" } +sdist = { url = "https://files.pythonhosted.org/packages/71/28/67172c96ba684058a4d24ffe144d64783d2a270d0af0d9e792737bddc75c/pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e", size = 33241, upload-time = "2025-05-26T13:58:45.167Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/3b/b26f90f74e2986a82df6e7ac7e319b8ea7ccece1caec9f8ab6104dc70603/pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f", size = 9863, upload-time = "2024-03-21T22:14:02.694Z" }, + { url = "https://files.pythonhosted.org/packages/b2/05/77b60e520511c53d1c1ca75f1930c7dd8e971d0c4379b7f4b3f9644685ba/pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0", size = 9923, upload-time = "2025-05-26T13:58:43.487Z" }, ] [[package]] @@ -2050,14 +2053,14 @@ wheels = [ [[package]] name = "python-engineio" -version = "4.12.0" +version = "4.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "simple-websocket" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f7/e1/eee1129544b7f78fa2afa9fa0fce153cdcb21015b9b331d1b8adf90f45cb/python_engineio-4.12.0.tar.gz", hash = "sha256:f42a36a868d7063aa10ddccf6bd6117a169b6bd00d7ca53999772093b62014f9", size = 91503, upload-time = "2025-04-12T15:30:23.905Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/0b/67295279b66835f9fa7a491650efcd78b20321c127036eef62c11a31e028/python_engineio-4.12.2.tar.gz", hash = "sha256:e7e712ffe1be1f6a05ee5f951e72d434854a32fcfc7f6e4d9d3cae24ec70defa", size = 91677, upload-time = "2025-06-04T19:22:18.789Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/2b/f7/0aeea75424c47633c1d98557a2323be23bed31fa950f00161b34a5150d06/python_engineio-4.12.0-py3-none-any.whl", hash = "sha256:a0c47c129c39777e8ebc6d18011efd50db2144e4e8f08983acae8a3614626535", size = 59319, upload-time = "2025-04-12T15:30:22.325Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fa/df59acedf7bbb937f69174d00f921a7b93aa5a5f5c17d05296c814fff6fc/python_engineio-4.12.2-py3-none-any.whl", hash = "sha256:8218ab66950e179dfec4b4bbb30aecf3f5d86f5e58e6fc1aa7fde2c698b2804f", size = 59536, upload-time = "2025-06-04T19:22:16.916Z" }, ] [[package]] @@ -2215,16 +2218,16 @@ wheels = [ [[package]] name = "rich" -version = "13.9.4" +version = "14.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149, upload-time = "2024-11-01T16:43:57.873Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424, upload-time = "2024-11-01T16:43:55.817Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" }, ] [[package]] @@ -2300,27 +2303,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.11.11" +version = "0.11.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/da/9c6f995903b4d9474b39da91d2d626659af3ff1eeb43e9ae7c119349dba6/ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514", size = 4282054, upload-time = "2025-06-05T21:00:15.721Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" }, - { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" }, - { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" }, - { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" }, - { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" }, - { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" }, - { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" }, - { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" }, - { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" }, - { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" }, - { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" }, - { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" }, - { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" }, - { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" }, - { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" }, - { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" }, - { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/a11d381192966e0b4290842cc8d4fac7dc9214ddf627c11c1afff87da29b/ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46", size = 10292516, upload-time = "2025-06-05T20:59:32.944Z" }, + { url = "https://files.pythonhosted.org/packages/78/db/87c3b59b0d4e753e40b6a3b4a2642dfd1dcaefbff121ddc64d6c8b47ba00/ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48", size = 11106083, upload-time = "2025-06-05T20:59:37.03Z" }, + { url = "https://files.pythonhosted.org/packages/77/79/d8cec175856ff810a19825d09ce700265f905c643c69f45d2b737e4a470a/ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b", size = 10436024, upload-time = "2025-06-05T20:59:39.741Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5b/f6d94f2980fa1ee854b41568368a2e1252681b9238ab2895e133d303538f/ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a", size = 10646324, upload-time = "2025-06-05T20:59:42.185Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9c/b4c2acf24ea4426016d511dfdc787f4ce1ceb835f3c5fbdbcb32b1c63bda/ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc", size = 10174416, upload-time = "2025-06-05T20:59:44.319Z" }, + { url = "https://files.pythonhosted.org/packages/f3/10/e2e62f77c65ede8cd032c2ca39c41f48feabedb6e282bfd6073d81bb671d/ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629", size = 11724197, upload-time = "2025-06-05T20:59:46.935Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f0/466fe8469b85c561e081d798c45f8a1d21e0b4a5ef795a1d7f1a9a9ec182/ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933", size = 12511615, upload-time = "2025-06-05T20:59:49.534Z" }, + { url = "https://files.pythonhosted.org/packages/17/0e/cefe778b46dbd0cbcb03a839946c8f80a06f7968eb298aa4d1a4293f3448/ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165", size = 12117080, upload-time = "2025-06-05T20:59:51.654Z" }, + { url = "https://files.pythonhosted.org/packages/5d/2c/caaeda564cbe103bed145ea557cb86795b18651b0f6b3ff6a10e84e5a33f/ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71", size = 11326315, upload-time = "2025-06-05T20:59:54.469Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/782e7d681d660eda8c536962920c41309e6dd4ebcea9a2714ed5127d44bd/ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9", size = 11555640, upload-time = "2025-06-05T20:59:56.986Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d4/3d580c616316c7f07fb3c99dbecfe01fbaea7b6fd9a82b801e72e5de742a/ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc", size = 10507364, upload-time = "2025-06-05T20:59:59.154Z" }, + { url = "https://files.pythonhosted.org/packages/5a/dc/195e6f17d7b3ea6b12dc4f3e9de575db7983db187c378d44606e5d503319/ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7", size = 10141462, upload-time = "2025-06-05T21:00:01.481Z" }, + { url = "https://files.pythonhosted.org/packages/f4/8e/39a094af6967faa57ecdeacb91bedfb232474ff8c3d20f16a5514e6b3534/ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432", size = 11121028, upload-time = "2025-06-05T21:00:04.06Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c0/b0b508193b0e8a1654ec683ebab18d309861f8bd64e3a2f9648b80d392cb/ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492", size = 11602992, upload-time = "2025-06-05T21:00:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/7c/91/263e33ab93ab09ca06ce4f8f8547a858cc198072f873ebc9be7466790bae/ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250", size = 10474944, upload-time = "2025-06-05T21:00:08.459Z" }, + { url = "https://files.pythonhosted.org/packages/46/f4/7c27734ac2073aae8efb0119cae6931b6fb48017adf048fdf85c19337afc/ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3", size = 11548669, upload-time = "2025-06-05T21:00:11.147Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/b273dd11673fed8a6bd46032c0ea2a04b2ac9bfa9c628756a5856ba113b0/ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b", size = 10683928, upload-time = "2025-06-05T21:00:13.758Z" }, ] [[package]] @@ -2555,26 +2558,23 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.0.20250515" +version = "2.32.0.20250602" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/c1/cdc4f9b8cfd9130fbe6276db574f114541f4231fcc6fb29648289e6e3390/types_requests-2.32.0.20250515.tar.gz", hash = "sha256:09c8b63c11318cb2460813871aaa48b671002e59fda67ca909e9883777787581", size = 23012, upload-time = "2025-05-15T03:04:31.817Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/b0/5321e6eeba5d59e4347fcf9bf06a5052f085c3aa0f4876230566d6a4dc97/types_requests-2.32.0.20250602.tar.gz", hash = "sha256:ee603aeefec42051195ae62ca7667cd909a2f8128fdf8aad9e8a5219ecfab3bf", size = 23042, upload-time = "2025-06-02T03:15:02.958Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/0f/68a997c73a129287785f418c1ebb6004f81e46b53b3caba88c0e03fcd04a/types_requests-2.32.0.20250515-py3-none-any.whl", hash = "sha256:f8eba93b3a892beee32643ff836993f15a785816acca21ea0ffa006f05ef0fb2", size = 20635, upload-time = "2025-05-15T03:04:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/da/18/9b782980e575c6581d5c0c1c99f4c6f89a1d7173dad072ee96b2756c02e6/types_requests-2.32.0.20250602-py3-none-any.whl", hash = "sha256:f4f335f87779b47ce10b8b8597b409130299f6971ead27fead4fe7ba6ea3e726", size = 20638, upload-time = "2025-06-02T03:15:01.959Z" }, ] [[package]] name = "types-setuptools" -version = "76.0.0.20250313" +version = "80.9.0.20250529" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "setuptools" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b8/0f/2d1d000c2be3919bcdea15e5da48456bf1e55c18d02c5509ea59dade1408/types_setuptools-76.0.0.20250313.tar.gz", hash = "sha256:b2be66f550f95f3cad2a7d46177b273c7e9c80df7d257fa57addbbcfc8126a9e", size = 43627, upload-time = "2025-03-13T02:51:28.3Z" } +sdist = { url = "https://files.pythonhosted.org/packages/79/66/1b276526aad4696a9519919e637801f2c103419d2c248a6feb2729e034d1/types_setuptools-80.9.0.20250529.tar.gz", hash = "sha256:79e088ba0cba2186c8d6499cbd3e143abb142d28a44b042c28d3148b1e353c91", size = 41337, upload-time = "2025-05-29T03:07:34.487Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/89/ea9669a0a76b160ffb312d0b02b15bad053c1bc81d2a54e42e3a402ca754/types_setuptools-76.0.0.20250313-py3-none-any.whl", hash = "sha256:bf454b2a49b8cfd7ebcf5844d4dd5fe4c8666782df1e3663c5866fd51a47460e", size = 65845, upload-time = "2025-03-13T02:51:27.055Z" }, + { url = "https://files.pythonhosted.org/packages/1b/d8/83790d67ec771bf029a45ff1bd1aedbb738d8aa58c09dd0cc3033eea0e69/types_setuptools-80.9.0.20250529-py3-none-any.whl", hash = "sha256:00dfcedd73e333a430e10db096e4d46af93faf9314f832f13b6bbe3d6757e95f", size = 63263, upload-time = "2025-05-29T03:07:33.064Z" }, ] [[package]] @@ -2627,16 +2627,16 @@ wheels = [ [[package]] name = "uvicorn" -version = "0.34.2" +version = "0.34.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, { name = "h11" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815, upload-time = "2025-04-19T06:02:50.101Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483, upload-time = "2025-04-19T06:02:48.42Z" }, + { url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" }, ] [package.optional-dependencies] diff --git a/mobile/.gitignore b/mobile/.gitignore index 894a08802f..484c3f0afc 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -59,4 +59,4 @@ libisar.so .fvm/ # Translation file -lib/generated/codegen_loader.g.dart \ No newline at end of file +lib/generated/ \ No newline at end of file diff --git a/mobile/README.md b/mobile/README.md index fb82b4de0c..436b0a4c34 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -17,6 +17,27 @@ To add a new translation text, enter the key-value pair in the `i18n/en.json` in make translation ``` +## Static Analysis + +The following checks of static analysis must pass for a contribution to the mobile app to be valid: + +```bash +dart format lib +dart analyze +dart run custom_lint +dcm analyze lib +``` + +[DCM](https://dcm.dev/) is a vendor tool that needs to be downloaded manually to run locally. +Immich was provided an open source license. +To use it, it is important that you do not have an active free tier license (can be verified with `dcm license`). +If you have write-access to the Immich repository directly, running dcm in your clone should just work. +If you are working on a clone of a fork, you need to connect to the main Immich repository as remote first: + +```bash +git remote add immich git@github.com:immich-app/immich.git +``` + ## Immich-Flutter Directory Structure Below are the directory inside the `lib` directory: diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index 4c06edc8c9..b338b7b758 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -128,82 +128,169 @@ custom_lint: - test/**.dart dart_code_metrics: - extends: - - recommended rules: - # Common - - arguments-ordering: - last: - - child - - children - - avoid-accessing-other-classes-private-members - - avoid-assigning-to-static-field - - avoid-assignments-as-conditions - - avoid-async-call-in-sync-function - - avoid-collapsible-if - - avoid-collection-equality-checks - - avoid-complex-loop-conditions - - avoid-declaring-call-method - - avoid-extensions-on-records - - avoid-function-type-in-records - - avoid-future-ignore - - avoid-global-state - - avoid-inverted-boolean-checks - - avoid-late-final-reassignment - - avoid-local-functions: - exclude: - - test/**.dart - - avoid-negated-conditions - - avoid-nested-streams-and-futures - - avoid-referencing-subclasses - - avoid-unnecessary-continue - - avoid-unnecessary-nullable-return-type: false - - binary-expression-operand-order - - pattern-fields-ordering - - prefer-abstract-final-static-class - - prefer-commenting-future-delayed - - prefer-early-return - - prefer-first - - prefer-immediate-return - - prefer-last - - prefer-simpler-boolean-expressions - - prefer-switch-expression - - prefer-type-over-var - - use-existing-destructuring - - use-existing-variable - # Flutter - - avoid-border-all - - avoid-complex-arithmetic-expressions - - avoid-expanded-as-spacer - - avoid-if-with-many-branches - - avoid-inherited-widget-in-initstate - - avoid-late-context - - avoid-returning-widgets - - avoid-shrink-wrap-in-lists - - avoid-single-child-column-or-row - - avoid-stateless-widget-initialized-fields - - avoid-wrapping-in-padding - - prefer-align-over-container - - prefer-const-border-radius - - prefer-correct-callback-field-name: false - - prefer-correct-edge-insets-constructor - - prefer-define-hero-tag - - prefer-extracting-callbacks - - prefer-for-loop-in-children - - prefer-match-file-name: false - - prefer-sliver-prefix - - prefer-spacing - - prefer-text-rich - - prefer-transform-over-container - - prefer-using-list-view - - prefer-widget-private-members: - ignore-static: true - - use-closest-build-context - # riverpod - - avoid-calling-notifier-members-inside-build - - avoid-notifier-constructors - - avoid-ref-read-inside-build - - avoid-ref-watch-outside-build - - avoid-unnecessary-consumer-widgets - - dispose-provided-instances - - use-ref-read-synchronously + # All rules from "recommended" preset + # Show potential errors + # - avoid-cascade-after-if-null + # - avoid-collection-methods-with-unrelated-types + # - avoid-duplicate-exports + # - avoid-dynamic + # - avoid-missing-enum-constant-in-map + # - avoid-passing-async-when-sync-expected + # - avoid-throw-in-catch-block + - avoid-unused-parameters + # - avoid-unnecessary-type-assertions + # - avoid-unnecessary-type-casts + # - avoid-unrelated-type-assertions + # - avoid-unrelated-type-casts + # - no-empty-block + # - no-equal-then-else + # - prefer-correct-test-file-name + # - prefer-match-file-name + # - prefer-return-await + # - avoid-self-assignment + # - avoid-self-compare + # - avoid-shadowing + # - prefer-iterable-of + # - no-equal-switch-case + # - no-equal-conditions + # - avoid-equal-expressions + # - avoid-missed-calls + # - avoid-unnecessary-negations + # - avoid-unused-generics + # - function-always-returns-null + # - avoid-throw-objects-without-tostring + # - avoid-unsafe-collection-methods + # - prefer-wildcard-pattern + # - no-equal-switch-expression-cases + # - avoid-future-tostring + # - avoid-unassigned-late-fields + # - avoid-nested-futures + # - avoid-generics-shadowing + # - prefer-parentheses-with-if-null + # - no-equal-nested-conditions + # - avoid-shadowed-extension-methods + # - avoid-unnecessary-conditionals + # - avoid-double-slash-imports + # - avoid-map-keys-contains + # - prefer-correct-json-casts + # - avoid-duplicate-mixins + # - avoid-nullable-interpolation + # - avoid-unused-instances + # - prefer-correct-for-loop-increment + # - prefer-public-exception-classes + # - avoid-uncaught-future-errors + # - always-remove-listener + # - avoid-unnecessary-setstate + # - check-for-equals-in-render-object-setters + # - consistent-update-render-object + # - use-setstate-synchronously + # - avoid-incomplete-copy-with + # - proper-super-calls + # - dispose-fields + # - avoid-empty-setstate + # - avoid-state-constructors + # - avoid-recursive-widget-calls + # - avoid-missing-image-alt + # - avoid-passing-self-as-argument + # - avoid-unnecessary-if + # - avoid-unconditional-break + # - avoid-referencing-discarded-variables + # - avoid-unnecessary-local-late + # - avoid-wildcard-cases-with-enums + # - match-getter-setter-field-names + # - avoid-accessing-collections-by-constant-index + # - prefer-unique-test-names + # - avoid-duplicate-cascades + # - prefer-specific-cases-first + # - avoid-duplicate-switch-case-conditions + # - prefer-explicit-function-type + # - avoid-misused-test-matchers + # - avoid-duplicate-test-assertions + # - prefer-switch-with-enums + # - prefer-any-or-every + # - avoid-duplicate-map-keys + # - avoid-nullable-tostring + # - avoid-undisposed-instances + # - avoid-duplicate-initializers + # - avoid-unassigned-stream-subscriptions + # - avoid-empty-test-groups + # - avoid-not-encodable-in-to-json + # - avoid-contradictory-expressions + # - avoid-excessive-expressions + # - prefer-private-extension-type-field + # - avoid-renaming-representation-getters + # - avoid-empty-spread + # - avoid-unnecessary-gesture-detector + # - avoid-missing-completer-stack-trace + # - avoid-casting-to-extension-type + # - prefer-overriding-parent-equality + # - avoid-missing-controller + # - avoid-unknown-pragma + # - avoid-conditions-with-boolean-literals + # - avoid-multi-assignment + # - avoid-collection-equality-checks + # - avoid-only-rethrow + # - avoid-incorrect-image-opacity + # - avoid-misused-set-literals + # - dispose-class-fields + # - avoid-suspicious-super-overrides + # - avoid-assignments-as-conditions + # - avoid-unused-assignment + # - avoid-unnecessary-overrides + # - avoid-implicitly-nullable-extension-types + # Enable with the next release + # - avoid-late-final-reassignment + # - avoid-duplicate-constant-values + # - function-always-returns-same-value + # - avoid-flexible-outside-flex + # - avoid-unnecessary-patterns + # - use-closest-build-context + # - avoid-commented-out-code + # - avoid-recursive-tostring + # - avoid-enum-values-by-index + # - avoid-constant-assert-conditions + # - avoid-inconsistent-digit-separators + # - pass-existing-future-to-future-builder + # - pass-existing-stream-to-stream-builder + + # Code simplification + # - avoid-redundant-async + # - avoid-redundant-else + # - avoid-unnecessary-nullable-return-type + # - avoid-redundant-pragma-inline + # - avoid-nested-records + # - avoid-redundant-positional-field-name + # - avoid-explicit-pattern-field-name + # - prefer-simpler-patterns-null-check + # - avoid-unnecessary-return + # - avoid-duplicate-patterns + # - avoid-keywords-in-wildcard-pattern + # - avoid-unnecessary-futures + # - avoid-unnecessary-reassignment + # - avoid-unnecessary-call + # - avoid-unnecessary-stateful-widgets + # - prefer-dedicated-media-query-methods + # - avoid-unnecessary-overrides-in-state + # - move-variable-closer-to-its-usage + # - avoid-nullable-parameters-with-default-values + # - prefer-null-aware-spread + # - avoid-inferrable-type-arguments + # - avoid-unnecessary-super + # - avoid-unnecessary-collections + # - avoid-unnecessary-extends + # - avoid-unnecessary-enum-arguments + # - prefer-contains + # Enable with the next release + # - prefer-simpler-boolean-expressions + # - prefer-spacing + # - avoid-unnecessary-continue + # - avoid-unnecessary-compare-to + + # Style + # - prefer-trailing-comma + # - unnecessary-trailing-comma + # - prefer-declaring-const-constructor + # - prefer-single-widget-per-file + # - prefer-prefixed-global-constants + # - prefer-correct-callback-field-name diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index 7455ae99a2..0d07228252 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -29,6 +29,7 @@ if (keystorePropertiesFile.exists()) { android { compileSdkVersion 35 + ndkVersion = "28.1.13356709" compileOptions { sourceCompatibility JavaVersion.VERSION_17 diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt index 18a788903a..6ae4c99bd7 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/Messages.g.kt @@ -85,6 +85,8 @@ data class PlatformAsset ( val type: Long, val createdAt: Long? = null, val updatedAt: Long? = null, + val width: Long? = null, + val height: Long? = null, val durationInSeconds: Long ) { @@ -95,8 +97,10 @@ data class PlatformAsset ( val type = pigeonVar_list[2] as Long val createdAt = pigeonVar_list[3] as Long? val updatedAt = pigeonVar_list[4] as Long? - val durationInSeconds = pigeonVar_list[5] as Long - return PlatformAsset(id, name, type, createdAt, updatedAt, durationInSeconds) + val width = pigeonVar_list[5] as Long? + val height = pigeonVar_list[6] as Long? + val durationInSeconds = pigeonVar_list[7] as Long + return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds) } } fun toList(): List { @@ -106,6 +110,8 @@ data class PlatformAsset ( type, createdAt, updatedAt, + width, + height, durationInSeconds, ) } diff --git a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt index 70fc045d5b..7f8ad531be 100644 --- a/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/sync/MessagesImplBase.kt @@ -37,6 +37,8 @@ open class NativeSyncApiImplBase(context: Context) { MediaStore.MediaColumns.DATE_MODIFIED, MediaStore.Files.FileColumns.MEDIA_TYPE, MediaStore.MediaColumns.BUCKET_ID, + MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, MediaStore.MediaColumns.DURATION ) @@ -68,6 +70,8 @@ open class NativeSyncApiImplBase(context: Context) { val dateModifiedColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DATE_MODIFIED) val mediaTypeColumn = c.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MEDIA_TYPE) val bucketIdColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.BUCKET_ID) + val widthColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.WIDTH) + val heightColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.HEIGHT) val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION) while (c.moveToNext()) { @@ -86,12 +90,23 @@ open class NativeSyncApiImplBase(context: Context) { ?: c.getLong(dateAddedColumn) // Date modified is seconds since epoch val modifiedAt = c.getLong(dateModifiedColumn) + val width = c.getInt(widthColumn).toLong() + val height = c.getInt(heightColumn).toLong() // Duration is milliseconds val duration = if (mediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0 else c.getLong(durationColumn) / 1000 val bucketId = c.getString(bucketIdColumn) - val asset = PlatformAsset(id, name, mediaType.toLong(), createdAt, modifiedAt, duration) + val asset = PlatformAsset( + id, + name, + mediaType.toLong(), + createdAt, + modifiedAt, + width, + height, + duration + ) yield(AssetResult.ValidAsset(asset, bucketId)) } } diff --git a/mobile/bin/generate_keys.dart b/mobile/bin/generate_keys.dart new file mode 100644 index 0000000000..8353b1c6f4 --- /dev/null +++ b/mobile/bin/generate_keys.dart @@ -0,0 +1,63 @@ +// ignore_for_file: avoid_print + +import 'dart:convert'; +import 'dart:io'; + +const _kReservedWords = ['continue']; + +void main() async { + final sourceFile = File('../i18n/en.json'); + if (!await sourceFile.exists()) { + stderr.writeln('Source file does not exist'); + return; + } + + final outputDir = Directory('lib/generated'); + await outputDir.create(recursive: true); + + final outputFile = File('lib/generated/intl_keys.g.dart'); + await _generate(sourceFile, outputFile); + print('Generated ${outputFile.path}'); +} + +Future _generate(File source, File output) async { + final content = await source.readAsString(); + final translations = json.decode(content) as Map; + + final buffer = StringBuffer(''' +// DO NOT EDIT. This is code generated via generate_keys.dart + +abstract class IntlKeys { +'''); + + _writeKeys(buffer, translations); + buffer.writeln('}'); + + await output.writeAsString(buffer.toString()); +} + +void _writeKeys( + StringBuffer buffer, + Map map, [ + String prefix = '', +]) { + for (final entry in map.entries) { + final key = entry.key; + final value = entry.value; + + if (value is Map) { + _writeKeys(buffer, value, prefix.isEmpty ? key : '${prefix}_$key'); + } else { + final name = _cleanName(prefix.isEmpty ? key : '${prefix}_$key'); + final path = prefix.isEmpty ? key : '$prefix.$key'.replaceAll('_', '.'); + buffer.writeln(' static const $name = \'$path\';'); + } + } +} + +String _cleanName(String name) { + name = name.replaceAll(RegExp(r'[^a-zA-Z0-9_]'), '_'); + if (RegExp(r'^[0-9]').hasMatch(name)) name = 'k_$name'; + if (_kReservedWords.contains(name)) name = '${name}_'; + return name; +} diff --git a/mobile/dcm_global.yaml b/mobile/dcm_global.yaml new file mode 100644 index 0000000000..d2465e64b6 --- /dev/null +++ b/mobile/dcm_global.yaml @@ -0,0 +1 @@ +version: '>=1.29.0 <1.30.0' diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index ee8e41aea2..0d147b9b22 100644 --- a/mobile/drift_schemas/main/drift_schema_v1.json +++ b/mobile/drift_schemas/main/drift_schema_v1.json @@ -1 +1 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}}]} \ No newline at end of file +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.2.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"user_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_admin","getter_name":"isAdmin","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_admin\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_admin\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"email","getter_name":"email","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"profile_image_path","getter_name":"profileImagePath","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"quota_size_in_bytes","getter_name":"quotaSizeInBytes","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"quota_usage_in_bytes","getter_name":"quotaUsageInBytes","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":1,"references":[0],"type":"table","data":{"name":"user_metadata_entity","was_declared_in_moor":false,"columns":[{"name":"user_id","getter_name":"userId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"preferences","getter_name":"preferences","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"userPreferenceConverter","dart_type_name":"UserPreferences"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["user_id"]}},{"id":2,"references":[0],"type":"table","data":{"name":"partner_entity","was_declared_in_moor":false,"columns":[{"name":"shared_by_id","getter_name":"sharedById","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"shared_with_id","getter_name":"sharedWithId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"in_timeline","getter_name":"inTimeline","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"in_timeline\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"in_timeline\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["shared_by_id","shared_with_id"]}},{"id":3,"references":[],"type":"table","data":{"name":"local_album_entity","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"backup_selection","getter_name":"backupSelection","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(BackupSelection.values)","dart_type_name":"BackupSelection"}},{"name":"is_ios_shared_album","getter_name":"isIosSharedAlbum","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_ios_shared_album\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_ios_shared_album\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"marker","getter_name":"marker_","moor_type":"bool","nullable":true,"customConstraints":null,"defaultConstraints":"CHECK (\"marker\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"marker\" IN (0, 1))"},"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"local_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":5,"references":[4,3],"type":"table","data":{"name":"local_album_asset_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"album_id","getter_name":"albumId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES local_album_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES local_album_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id","album_id"]}},{"id":6,"references":[0],"type":"table","data":{"name":"remote_asset_entity","was_declared_in_moor":false,"columns":[{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetType.values)","dart_type_name":"AssetType"}},{"name":"created_at","getter_name":"createdAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"updated_at","getter_name":"updatedAt","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":"const CustomExpression('CURRENT_TIMESTAMP')","default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"duration_in_seconds","getter_name":"durationInSeconds","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"checksum","getter_name":"checksum","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"is_favorite","getter_name":"isFavorite","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"is_favorite\" IN (0, 1))","dialectAwareDefaultConstraints":{"sqlite":"CHECK (\"is_favorite\" IN (0, 1))"},"default_dart":"const CustomExpression('0')","default_client_dart":null,"dsl_features":[]},{"name":"owner_id","getter_name":"ownerId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES user_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES user_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"local_date_time","getter_name":"localDateTime","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"thumb_hash","getter_name":"thumbHash","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"deleted_at","getter_name":"deletedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"visibility","getter_name":"visibility","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(AssetVisibility.values)","dart_type_name":"AssetVisibility"}}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["id"]}},{"id":7,"references":[6],"type":"table","data":{"name":"remote_exif_entity","was_declared_in_moor":false,"columns":[{"name":"asset_id","getter_name":"assetId","moor_type":"string","nullable":false,"customConstraints":null,"defaultConstraints":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE","dialectAwareDefaultConstraints":{"sqlite":"REFERENCES remote_asset_entity (id) ON DELETE CASCADE"},"default_dart":null,"default_client_dart":null,"dsl_features":["unknown"]},{"name":"city","getter_name":"city","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"country","getter_name":"country","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"date_time_original","getter_name":"dateTimeOriginal","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"description","getter_name":"description","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"height","getter_name":"height","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"width","getter_name":"width","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"exposure_time","getter_name":"exposureTime","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"f_number","getter_name":"fNumber","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"file_size","getter_name":"fileSize","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"focal_length","getter_name":"focalLength","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"latitude","getter_name":"latitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"longitude","getter_name":"longitude","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"iso","getter_name":"iso","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"make","getter_name":"make","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"model","getter_name":"model","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"orientation","getter_name":"orientation","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"time_zone","getter_name":"timeZone","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"rating","getter_name":"rating","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"projection_type","getter_name":"projectionType","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":true,"constraints":[],"strict":true,"explicit_pk":["asset_id"]}},{"id":8,"references":[4],"type":"index","data":{"on":4,"name":"idx_local_asset_checksum","sql":null,"unique":false,"columns":["checksum"]}},{"id":9,"references":[6],"type":"index","data":{"on":6,"name":"UQ_remote_asset_owner_checksum","sql":null,"unique":true,"columns":["checksum","owner_id"]}}]} \ No newline at end of file diff --git a/mobile/immich_lint/pubspec.lock b/mobile/immich_lint/pubspec.lock index 263a43c22c..e05cbb3db3 100644 --- a/mobile/immich_lint/pubspec.lock +++ b/mobile/immich_lint/pubspec.lock @@ -189,10 +189,10 @@ packages: dependency: "direct dev" description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" logging: dependency: transitive description: @@ -362,4 +362,4 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" diff --git a/mobile/immich_lint/pubspec.yaml b/mobile/immich_lint/pubspec.yaml index 2890a4a595..e49e9c5010 100644 --- a/mobile/immich_lint/pubspec.yaml +++ b/mobile/immich_lint/pubspec.yaml @@ -11,4 +11,4 @@ dependencies: glob: ^2.1.2 dev_dependencies: - lints: ^5.0.0 + lints: ^6.0.0 diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 537cdba8d8..36ec03dd0e 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -1,6 +1,9 @@ PODS: - background_downloader (0.0.1): - Flutter + - bonsoir_darwin (0.0.1): + - Flutter + - FlutterMacOS - connectivity_plus (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -64,10 +67,10 @@ PODS: - local_auth_darwin (0.0.1): - Flutter - FlutterMacOS - - MapLibre (6.5.0) + - MapLibre (6.14.0) - maplibre_gl (0.0.1): - Flutter - - MapLibre (= 6.5.0) + - MapLibre (= 6.14.0) - native_video_player (1.0.0): - Flutter - network_info_plus (0.0.1): @@ -129,6 +132,7 @@ PODS: DEPENDENCIES: - background_downloader (from `.symlinks/plugins/background_downloader/ios`) + - bonsoir_darwin (from `.symlinks/plugins/bonsoir_darwin/darwin`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) @@ -173,6 +177,8 @@ SPEC REPOS: EXTERNAL SOURCES: background_downloader: :path: ".symlinks/plugins/background_downloader/ios" + bonsoir_darwin: + :path: ".symlinks/plugins/bonsoir_darwin/darwin" connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: @@ -236,6 +242,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: background_downloader: 50e91d979067b82081aba359d7d916b3ba5fadad + bonsoir_darwin: 29c7ccf356646118844721f36e1de4b61f6cbd0e connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c @@ -253,8 +260,8 @@ SPEC CHECKSUMS: integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26 local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 - MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e - maplibre_gl: eab61cca6e1cfa9187249bacd3f08b51e8cd8ae9 + MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd + maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f native_video_player: b65c58951ede2f93d103a25366bdebca95081265 network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 38a1573dbd..c59b4c4295 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -113,6 +113,11 @@ NSAllowsArbitraryLoads + NSBonjourServices + + _googlecast._tcp + _CC1AD845._googlecast._tcp + NSCameraUsageDescription We need to access the camera to let you take beautiful video using this app NSLocationAlwaysAndWhenInUseUsageDescription @@ -164,4 +169,4 @@ NSFaceIDUsageDescription We need to use FaceID to allow access to your locked folder - \ No newline at end of file + diff --git a/mobile/ios/Runner/Sync/Messages.g.swift b/mobile/ios/Runner/Sync/Messages.g.swift index eb765337c3..89eb092a16 100644 --- a/mobile/ios/Runner/Sync/Messages.g.swift +++ b/mobile/ios/Runner/Sync/Messages.g.swift @@ -135,6 +135,8 @@ struct PlatformAsset: Hashable { var type: Int64 var createdAt: Int64? = nil var updatedAt: Int64? = nil + var width: Int64? = nil + var height: Int64? = nil var durationInSeconds: Int64 @@ -145,7 +147,9 @@ struct PlatformAsset: Hashable { let type = pigeonVar_list[2] as! Int64 let createdAt: Int64? = nilOrValue(pigeonVar_list[3]) let updatedAt: Int64? = nilOrValue(pigeonVar_list[4]) - let durationInSeconds = pigeonVar_list[5] as! Int64 + let width: Int64? = nilOrValue(pigeonVar_list[5]) + let height: Int64? = nilOrValue(pigeonVar_list[6]) + let durationInSeconds = pigeonVar_list[7] as! Int64 return PlatformAsset( id: id, @@ -153,6 +157,8 @@ struct PlatformAsset: Hashable { type: type, createdAt: createdAt, updatedAt: updatedAt, + width: width, + height: height, durationInSeconds: durationInSeconds ) } @@ -163,6 +169,8 @@ struct PlatformAsset: Hashable { type, createdAt, updatedAt, + width, + height, durationInSeconds, ] } diff --git a/mobile/ios/Runner/Sync/MessagesImpl.swift b/mobile/ios/Runner/Sync/MessagesImpl.swift index 06c958b88a..861e8196c5 100644 --- a/mobile/ios/Runner/Sync/MessagesImpl.swift +++ b/mobile/ios/Runner/Sync/MessagesImpl.swift @@ -25,6 +25,8 @@ extension PHAsset { type: Int64(mediaType.rawValue), createdAt: creationDate.map { Int64($0.timeIntervalSince1970) }, updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) }, + width: Int64(pixelWidth), + height: Int64(pixelHeight), durationInSeconds: Int64(duration) ) } @@ -156,8 +158,6 @@ class NativeSyncApiImpl: NativeSyncApi { id: asset.localIdentifier, name: "", type: 0, - createdAt: nil, - updatedAt: nil, durationInSeconds: 0 ) if (updatedAssets.contains(AssetWrapper(with: predicate))) { diff --git a/mobile/lib/constants/filters.dart b/mobile/lib/constants/filters.dart index d9fa2920b7..61597f08d1 100644 --- a/mobile/lib/constants/filters.dart +++ b/mobile/lib/constants/filters.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -List filters = [ +const List filters = [ //Original - const ColorFilter.matrix([ + ColorFilter.matrix([ 1, 0, 0, @@ -25,7 +25,7 @@ List filters = [ 0, ]), //Vintage - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.8, 0.1, 0.1, @@ -48,7 +48,7 @@ List filters = [ 0, ]), //Mood - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.2, 0.1, 0.1, @@ -71,7 +71,7 @@ List filters = [ 0, ]), //Crisp - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.2, 0, 0, @@ -94,7 +94,7 @@ List filters = [ 0, ]), //Cool - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.9, 0, 0.2, @@ -117,7 +117,7 @@ List filters = [ 0, ]), //Blush - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.1, 0.1, 0.1, @@ -140,7 +140,7 @@ List filters = [ 0, ]), //Sunkissed - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.3, 0, 0.1, @@ -163,7 +163,7 @@ List filters = [ 0, ]), //Fresh - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.2, 0, 0, @@ -186,7 +186,7 @@ List filters = [ 0, ]), //Classic - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.1, 0, -0.1, @@ -209,7 +209,7 @@ List filters = [ 0, ]), //Lomo-ish - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.5, 0, 0.1, @@ -232,7 +232,7 @@ List filters = [ 0, ]), //Nashville - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.2, 0.15, -0.15, @@ -255,7 +255,7 @@ List filters = [ 0, ]), //Valencia - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.15, 0.1, 0.1, @@ -278,7 +278,7 @@ List filters = [ 0, ]), //Clarendon - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.2, 0, 0, @@ -301,7 +301,7 @@ List filters = [ 0, ]), //Moon - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.33, 0.33, 0.33, @@ -324,7 +324,7 @@ List filters = [ 0, ]), //Willow - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.5, 0.5, 0.5, @@ -347,7 +347,7 @@ List filters = [ 0, ]), //Kodak - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.3, 0.1, -0.1, @@ -370,7 +370,7 @@ List filters = [ 0, ]), //Frost - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.8, 0.2, 0.1, @@ -393,7 +393,7 @@ List filters = [ 0, ]), //Night Vision - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.1, 0.95, 0.2, @@ -416,7 +416,7 @@ List filters = [ 0, ]), //Sunset - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.5, 0.2, 0, @@ -439,7 +439,7 @@ List filters = [ 0, ]), //Noir - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.3, -0.3, 0.1, @@ -462,7 +462,7 @@ List filters = [ 0, ]), //Dreamy - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.1, 0.1, 0.1, @@ -485,7 +485,7 @@ List filters = [ 0, ]), //Sepia - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.393, 0.769, 0.189, @@ -508,7 +508,7 @@ List filters = [ 0, ]), //Radium - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.438, -0.062, -0.062, @@ -531,7 +531,7 @@ List filters = [ 0, ]), //Aqua - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.2126, 0.7152, 0.0722, @@ -554,7 +554,7 @@ List filters = [ 0, ]), //Purple Haze - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.3, 0, 1.2, @@ -577,7 +577,7 @@ List filters = [ 0, ]), //Lemonade - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.2, 0.1, 0, @@ -600,7 +600,7 @@ List filters = [ 0, ]), //Caramel - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.6, 0.2, 0, @@ -623,7 +623,7 @@ List filters = [ 0, ]), //Peachy - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.3, 0.5, 0, @@ -646,7 +646,7 @@ List filters = [ 0, ]), //Neon - const ColorFilter.matrix([ + ColorFilter.matrix([ 1, 0, 1, @@ -669,7 +669,7 @@ List filters = [ 0, ]), //Cold Morning - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.9, 0.1, 0.2, @@ -692,7 +692,7 @@ List filters = [ 0, ]), //Lush - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.9, 0.2, 0, @@ -715,7 +715,7 @@ List filters = [ 0, ]), //Urban Neon - const ColorFilter.matrix([ + ColorFilter.matrix([ 1.1, 0, 0.3, @@ -738,7 +738,7 @@ List filters = [ 0, ]), //Monochrome - const ColorFilter.matrix([ + ColorFilter.matrix([ 0.6, 0.2, 0.2, diff --git a/mobile/lib/domain/interfaces/local_album.interface.dart b/mobile/lib/domain/interfaces/local_album.interface.dart index 9c81460dfc..e12bb59fb8 100644 --- a/mobile/lib/domain/interfaces/local_album.interface.dart +++ b/mobile/lib/domain/interfaces/local_album.interface.dart @@ -3,11 +3,11 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/local_album.model.dart'; abstract interface class ILocalAlbumRepository implements IDatabaseRepository { - Future> getAll({SortLocalAlbumsBy? sortBy}); + Future> getAll({Set sortBy = const {}}); - Future> getAssetsForAlbum(String albumId); + Future> getAssets(String albumId); - Future> getAssetIdsForAlbum(String albumId); + Future> getAssetIds(String albumId); Future upsert( LocalAlbum album, { @@ -26,12 +26,9 @@ abstract interface class ILocalAlbumRepository implements IDatabaseRepository { required Map> assetAlbums, }); - Future syncAlbumDeletes( - String albumId, - Iterable assetIdsToKeep, - ); + Future syncDeletes(String albumId, Iterable assetIdsToKeep); Future> getAssetsToHash(String albumId); } -enum SortLocalAlbumsBy { id } +enum SortLocalAlbumsBy { id, backupSelection, isIosSharedAlbum } diff --git a/mobile/lib/domain/interfaces/store.interface.dart b/mobile/lib/domain/interfaces/store.interface.dart index 7a45f9dbe0..b0a6762566 100644 --- a/mobile/lib/domain/interfaces/store.interface.dart +++ b/mobile/lib/domain/interfaces/store.interface.dart @@ -6,9 +6,11 @@ abstract interface class IStoreRepository implements IDatabaseRepository { Future tryGet(StoreKey key); + Future>> getAll(); + Stream watch(StoreKey key); - Stream watchAll(); + Stream> watchAll(); Future update(StoreKey key, T value); diff --git a/mobile/lib/domain/interfaces/sync_stream.interface.dart b/mobile/lib/domain/interfaces/sync_stream.interface.dart index 5f61d6b52f..502cf0ad46 100644 --- a/mobile/lib/domain/interfaces/sync_stream.interface.dart +++ b/mobile/lib/domain/interfaces/sync_stream.interface.dart @@ -15,4 +15,13 @@ abstract interface class ISyncStreamRepository implements IDatabaseRepository { Future updatePartnerAssetsV1(Iterable data); Future deletePartnerAssetsV1(Iterable data); Future updatePartnerAssetsExifV1(Iterable data); + + Future updateAlbumsV1(Iterable data); + Future deleteAlbumsV1(Iterable data); + + // Future updateAlbumAssetsV1(Iterable data); + // Future deleteAlbumAssetsV1(Iterable data); + + Future updateAlbumUsersV1(Iterable data); + Future deleteAlbumUsersV1(Iterable data); } diff --git a/mobile/lib/domain/models/album/album.model.dart b/mobile/lib/domain/models/album/album.model.dart new file mode 100644 index 0000000000..47683121ba --- /dev/null +++ b/mobile/lib/domain/models/album/album.model.dart @@ -0,0 +1,68 @@ +enum AssetOrder { + // do not change this order! + asc, + desc, +} + +// Model for an album stored in the server +class Album { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final AssetOrder order; + + const Album({ + required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order, + }); + + @override + String toString() { + return '''Album { + id: $id, + name: $name, + description: $description, + createdAt: $createdAt, + updatedAt: $updatedAt, + isActivityEnabled: $isActivityEnabled, + order: $order, + thumbnailAssetId: ${thumbnailAssetId ?? ""} + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! Album) return false; + if (identical(this, other)) return true; + return id == other.id && + name == other.name && + description == other.description && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + thumbnailAssetId == other.thumbnailAssetId && + isActivityEnabled == other.isActivityEnabled && + order == other.order; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + description.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + thumbnailAssetId.hashCode ^ + isActivityEnabled.hashCode ^ + order.hashCode; + } +} diff --git a/mobile/lib/domain/models/album_user.model.dart b/mobile/lib/domain/models/album_user.model.dart new file mode 100644 index 0000000000..10c03a7c4b --- /dev/null +++ b/mobile/lib/domain/models/album_user.model.dart @@ -0,0 +1,5 @@ +enum AlbumUserRole { + // do not change this order! + editor, + viewer, +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/local_album.model.dart index ee27d91a71..b0b08937ab 100644 --- a/mobile/lib/domain/models/local_album.model.dart +++ b/mobile/lib/domain/models/local_album.model.dart @@ -1,12 +1,10 @@ enum BackupSelection { - none._(1), - selected._(0), - excluded._(2); - // Used to sort albums based on the backupSelection // selected -> none -> excluded - final int sortOrder; - const BackupSelection._(this.sortOrder); + // Do not change the order of these values + selected, + none, + excluded, } class LocalAlbum { diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart index 427e4ed0d1..046e26ecbf 100644 --- a/mobile/lib/domain/models/store.model.dart +++ b/mobile/lib/domain/models/store.model.dart @@ -76,23 +76,23 @@ enum StoreKey { Type get type => T; } -class StoreUpdateEvent { +class StoreDto { final StoreKey key; final T? value; - const StoreUpdateEvent(this.key, this.value); + const StoreDto(this.key, this.value); @override String toString() { return ''' -StoreUpdateEvent: { +StoreDto: { key: $key, value: ${value ?? ''}, }'''; } @override - bool operator ==(covariant StoreUpdateEvent other) { + bool operator ==(covariant StoreDto other) { if (identical(this, other)) return true; return other.key == key && other.value == value; diff --git a/mobile/lib/domain/models/sync_event.model.dart b/mobile/lib/domain/models/sync_event.model.dart index 2ad8a75fe1..d78769ceac 100644 --- a/mobile/lib/domain/models/sync_event.model.dart +++ b/mobile/lib/domain/models/sync_event.model.dart @@ -2,8 +2,7 @@ import 'package:openapi/api.dart'; class SyncEvent { final SyncEntityType type; - // ignore: avoid-dynamic - final dynamic data; + final Object data; final String ack; const SyncEvent({required this.type, required this.data, required this.ack}); diff --git a/mobile/lib/domain/services/hash.service.dart b/mobile/lib/domain/services/hash.service.dart index 9820453db1..d8ad3dc2dc 100644 --- a/mobile/lib/domain/services/hash.service.dart +++ b/mobile/lib/domain/services/hash.service.dart @@ -33,18 +33,12 @@ class HashService { Future hashAssets() async { final Stopwatch stopwatch = Stopwatch()..start(); // Sorted by backupSelection followed by isCloud - final localAlbums = await _localAlbumRepository.getAll(); - localAlbums.sort((a, b) { - final backupComparison = - a.backupSelection.sortOrder.compareTo(b.backupSelection.sortOrder); - - if (backupComparison != 0) { - return backupComparison; - } - - // Local albums come before iCloud albums - return (a.isIosSharedAlbum ? 1 : 0).compareTo(b.isIosSharedAlbum ? 1 : 0); - }); + final localAlbums = await _localAlbumRepository.getAll( + sortBy: { + SortLocalAlbumsBy.backupSelection, + SortLocalAlbumsBy.isIosSharedAlbum, + }, + ); for (final album in localAlbums) { final assetsToHash = @@ -96,13 +90,18 @@ class HashService { final hashed = []; final hashes = await _nativeSyncApi.hashPaths(toHash.map((e) => e.path).toList()); + assert( + hashes.length == toHash.length, + "Hashes length does not match toHash length: ${hashes.length} != ${toHash.length}", + ); - for (final (index, hash) in hashes.indexed) { - final asset = toHash[index].asset; + for (int i = 0; i < hashes.length; i++) { + final hash = hashes[i]; + final asset = toHash[i].asset; if (hash?.length == 20) { hashed.add(asset.copyWith(checksum: base64.encode(hash!))); } else { - _log.warning("Failed to hash file ${asset.id}"); + _log.warning("Failed to hash file for ${asset.id}"); } } diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index e39999f222..ff77ebd83e 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -66,7 +66,7 @@ class LocalSyncService { if (_platform.isAndroid) { for (final album in dbAlbums) { final deviceIds = await _nativeSyncApi.getAssetIdsForAlbum(album.id); - await _localAlbumRepository.syncAlbumDeletes(album.id, deviceIds); + await _localAlbumRepository.syncDeletes(album.id, deviceIds); } } @@ -113,7 +113,7 @@ class LocalSyncService { } final dbAlbums = - await _localAlbumRepository.getAll(sortBy: SortLocalAlbumsBy.id); + await _localAlbumRepository.getAll(sortBy: {SortLocalAlbumsBy.id}); await diffSortedLists( dbAlbums, @@ -252,7 +252,7 @@ class LocalSyncService { .then((a) => a.toLocalAssets()) : []; final assetsInDb = dbAlbum.assetCount > 0 - ? await _localAlbumRepository.getAssetsForAlbum(dbAlbum.id) + ? await _localAlbumRepository.getAssets(dbAlbum.id) : []; if (deviceAlbum.assetCount == 0) { @@ -373,6 +373,8 @@ extension on Iterable { updatedAt: e.updatedAt == null ? DateTime.now() : DateTime.fromMillisecondsSinceEpoch(e.updatedAt! * 1000), + width: e.width, + height: e.height, durationInSeconds: e.durationInSeconds, ), ).toList(); diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart index 2136912a67..a25147d185 100644 --- a/mobile/lib/domain/services/log.service.dart +++ b/mobile/lib/domain/services/log.service.dart @@ -8,6 +8,11 @@ import 'package:immich_mobile/domain/models/log.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:logging/logging.dart'; +/// Service responsible for handling application logging. +/// +/// It listens to Dart's [Logger.root], buffers logs in memory (optionally), +/// writes them to a persistent [ILogRepository], and manages log levels +/// via [IStoreRepository] class LogService { final ILogRepository _logRepository; final IStoreRepository _storeRepository; @@ -18,19 +23,11 @@ class LogService { /// This is useful when logging in quick succession, as it increases performance /// and reduces NAND wear. However, it may cause the logs to be lost in case of a crash / in isolates. final bool _shouldBuffer; + Timer? _flushTimer; late final StreamSubscription _logSubscription; - LogService._( - this._logRepository, - this._storeRepository, - this._shouldBuffer, - ) { - // Listen to log messages and write them to the database - _logSubscription = Logger.root.onRecord.listen(_writeLogToDatabase); - } - static LogService? _instance; static LogService get I { if (_instance == null) { @@ -44,10 +41,7 @@ class LogService { required IStoreRepository storeRepository, bool shouldBuffer = true, }) async { - if (_instance != null) { - return _instance!; - } - _instance = await create( + _instance ??= await create( logRepository: logRepository, storeRepository: storeRepository, shouldBuffer: shouldBuffer, @@ -61,55 +55,28 @@ class LogService { bool shouldBuffer = true, }) async { final instance = LogService._(logRepository, storeRepository, shouldBuffer); - // Truncate logs to 250 await logRepository.truncate(limit: kLogTruncateLimit); - // Get log level from store - final level = await instance._storeRepository.tryGet(StoreKey.logLevel); - if (level != null) { - Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO; - } + final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ?? + LogLevel.info.index; + Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO; return instance; } - Future setlogLevel(LogLevel level) async { - await _storeRepository.insert(StoreKey.logLevel, level.index); - Logger.root.level = level.toLevel(); + LogService._( + this._logRepository, + this._storeRepository, + this._shouldBuffer, + ) { + _logSubscription = Logger.root.onRecord.listen(_handleLogRecord); } - Future> getMessages() async { - final logsFromDb = await _logRepository.getAll(); - if (_msgBuffer.isNotEmpty) { - return [..._msgBuffer.reversed, ...logsFromDb]; - } - return logsFromDb; - } - - Future clearLogs() async { - _flushTimer?.cancel(); - _flushTimer = null; - _msgBuffer.clear(); - await _logRepository.deleteAll(); - } - - /// Flush pending log messages to persistent storage - void flush() { - if (_flushTimer == null) { - return; - } - _flushTimer!.cancel(); - // TODO: Rename enable this after moving to sqlite - #16504 - // await _flushBufferToDatabase(); - } - - Future dispose() { - _flushTimer?.cancel(); - _logSubscription.cancel(); - return _flushBufferToDatabase(); - } - - void _writeLogToDatabase(LogRecord r) { + void _handleLogRecord(LogRecord r) { if (kDebugMode) { - debugPrint('[${r.level.name}] [${r.time}] ${r.message}'); + debugPrint( + '[${r.level.name}] [${r.time}] [${r.loggerName}] ${r.message}' + '${r.error == null ? '' : '\nError: ${r.error}'}' + '${r.stackTrace == null ? '' : '\nStack: ${r.stackTrace}'}', + ); } final record = LogMessage( @@ -125,14 +92,44 @@ class LogService { _msgBuffer.add(record); _flushTimer ??= Timer( const Duration(seconds: 5), - () => unawaited(_flushBufferToDatabase()), + () => unawaited(flushBuffer()), ); } else { unawaited(_logRepository.insert(record)); } } - Future _flushBufferToDatabase() async { + Future setLogLevel(LogLevel level) async { + await _storeRepository.insert(StoreKey.logLevel, level.index); + Logger.root.level = level.toLevel(); + } + + Future> getMessages() async { + final logsFromDb = await _logRepository.getAll(); + return [..._msgBuffer.reversed, ...logsFromDb]; + } + + Future clearLogs() async { + _flushTimer?.cancel(); + _flushTimer = null; + _msgBuffer.clear(); + await _logRepository.deleteAll(); + } + + void flush() { + _flushTimer?.cancel(); + // TODO: Rename enable this after moving to sqlite - #16504 + // await _flushBufferToDatabase(); + } + + Future dispose() { + _flushTimer?.cancel(); + _logSubscription.cancel(); + return flushBuffer(); + } + + // TOOD: Move this to private once Isar is removed + Future flushBuffer() async { _flushTimer = null; final buffer = [..._msgBuffer]; _msgBuffer.clear(); diff --git a/mobile/lib/domain/services/store.service.dart b/mobile/lib/domain/services/store.service.dart index 73426cbf4e..7b71acd254 100644 --- a/mobile/lib/domain/services/store.service.dart +++ b/mobile/lib/domain/services/store.service.dart @@ -3,15 +3,17 @@ import 'dart:async'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; +/// Provides access to a persistent key-value store with an in-memory cache. +/// Listens for repository changes to keep the cache updated. class StoreService { final IStoreRepository _storeRepository; - final Map _cache = {}; - late final StreamSubscription _storeUpdateSubscription; + /// In-memory cache. Keys are [StoreKey.id] + final Map _cache = {}; + late final StreamSubscription _storeUpdateSubscription; - StoreService._({ - required IStoreRepository storeRepository, - }) : _storeRepository = storeRepository; + StoreService._({required IStoreRepository storeRepository}) + : _storeRepository = storeRepository; // TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider static StoreService? _instance; @@ -23,7 +25,6 @@ class StoreService { } // TODO: Replace the implementation with the one from create after removing the typedef - /// Initializes the store with the given [storeRepository] static Future init({ required IStoreRepository storeRepository, }) async { @@ -31,7 +32,6 @@ class StoreService { return _instance!; } - /// Initializes the store with the given [storeRepository] static Future create({ required IStoreRepository storeRepository, }) async { @@ -41,16 +41,14 @@ class StoreService { return instance; } - /// Fills the cache with the values from the DB Future _populateCache() async { - for (StoreKey key in StoreKey.values) { - final storeValue = await _storeRepository.tryGet(key); - _cache[key.id] = storeValue; + final storeValues = await _storeRepository.getAll(); + for (StoreDto storeValue in storeValues) { + _cache[storeValue.key.id] = storeValue.value; } } - /// Listens for changes in the DB and updates the cache - StreamSubscription _listenForChange() => + StreamSubscription _listenForChange() => _storeRepository.watchAll().listen((event) { _cache[event.key.id] = event.value; }); @@ -61,11 +59,11 @@ class StoreService { _cache.clear(); } - /// Returns the stored value for the given key (possibly null) - T? tryGet(StoreKey key) => _cache[key.id]; + /// Returns the cached value for [key], or `null` + T? tryGet(StoreKey key) => _cache[key.id] as T?; - /// Returns the stored value for the given key or if null the [defaultValue] - /// Throws a [StoreKeyNotFoundException] if both are null + /// Returns the stored value for [key] or [defaultValue]. + /// Throws [StoreKeyNotFoundException] if value and [defaultValue] are null. T get(StoreKey key, [T? defaultValue]) { final value = tryGet(key) ?? defaultValue; if (value == null) { @@ -74,23 +72,23 @@ class StoreService { return value; } - /// Asynchronously stores the value in the Store + /// Stores the [value] for the [key]. Skips write if value hasn't changed. Future put, T>(U key, T value) async { if (_cache[key.id] == value) return; await _storeRepository.insert(key, value); _cache[key.id] = value; } - /// Watches a specific key for changes + /// Returns a stream that emits the value for [key] on change. Stream watch(StoreKey key) => _storeRepository.watch(key); - /// Removes the value asynchronously from the Store + /// Removes the value for [key] Future delete(StoreKey key) async { await _storeRepository.delete(key); _cache.remove(key.id); } - /// Clears all values from this store (cache and DB) + /// Clears all values from thw store (cache and DB) Future clear() async { await _storeRepository.deleteAll(); _cache.clear(); diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index 00f97825b2..f997bef139 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid-passing-async-when-sync-expected - import 'dart:async'; import 'package:immich_mobile/domain/interfaces/sync_api.interface.dart'; @@ -59,8 +57,7 @@ class SyncStreamService { Future _handleSyncData( SyncEntityType type, - // ignore: avoid-dynamic - Iterable data, + Iterable data, ) async { _logger.fine("Processing sync data for $type of length ${data.length}"); switch (type) { @@ -84,6 +81,18 @@ class SyncStreamService { return _syncStreamRepository.deletePartnerAssetsV1(data.cast()); case SyncEntityType.partnerAssetExifV1: return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast()); + case SyncEntityType.albumV1: + return _syncStreamRepository.updateAlbumsV1(data.cast()); + case SyncEntityType.albumDeleteV1: + return _syncStreamRepository.deleteAlbumsV1(data.cast()); + // case SyncEntityType.albumAssetV1: + // return _syncStreamRepository.updateAlbumAssetsV1(data.cast()); + // case SyncEntityType.albumAssetDeleteV1: + // return _syncStreamRepository.deleteAlbumAssetsV1(data.cast()); + case SyncEntityType.albumUserV1: + return _syncStreamRepository.updateAlbumUsersV1(data.cast()); + case SyncEntityType.albumUserDeleteV1: + return _syncStreamRepository.deleteAlbumUsersV1(data.cast()); default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/infrastructure/entities/album_user.entity.dart b/mobile/lib/infrastructure/entities/album_user.entity.dart new file mode 100644 index 0000000000..c9bbfbd22f --- /dev/null +++ b/mobile/lib/infrastructure/entities/album_user.entity.dart @@ -0,0 +1,20 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album_user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class AlbumUserEntity extends Table with DriftDefaultsMixin { + const AlbumUserEntity(); + + TextColumn get albumId => + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get role => intEnum()(); + + @override + Set get primaryKey => {albumId, userId}; +} diff --git a/mobile/lib/infrastructure/entities/album_user.entity.drift.dart b/mobile/lib/infrastructure/entities/album_user.entity.drift.dart new file mode 100644 index 0000000000..008e0bb52b --- /dev/null +++ b/mobile/lib/infrastructure/entities/album_user.entity.drift.dart @@ -0,0 +1,602 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/album_user.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/album_user.entity.dart' + as i3; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i4; +import 'package:drift/internal/modular.dart' as i5; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i6; + +typedef $$AlbumUserEntityTableCreateCompanionBuilder + = i1.AlbumUserEntityCompanion Function({ + required String albumId, + required String userId, + required i2.AlbumUserRole role, +}); +typedef $$AlbumUserEntityTableUpdateCompanionBuilder + = i1.AlbumUserEntityCompanion Function({ + i0.Value albumId, + i0.Value userId, + i0.Value role, +}); + +final class $$AlbumUserEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, i1.$AlbumUserEntityTable, i1.AlbumUserEntityData> { + $$AlbumUserEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i4.$RemoteAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet('album_user_entity') + .albumId, + i5.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .id)); + + i4.$$RemoteAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i4 + .$$RemoteAlbumEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('remote_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i6.$UserEntityTable _userIdTable(i0.GeneratedDatabase db) => + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i5.ReadDatabaseContainer(db) + .resultSet('album_user_entity') + .userId, + i5.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i6.$$UserEntityTableProcessedTableManager get userId { + final $_column = $_itemColumn('user_id')!; + + final manager = i6 + .$$UserEntityTableTableManager( + $_db, + i5.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_userIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$AlbumUserEntityTableFilterComposer + extends i0.Composer { + $$AlbumUserEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnWithTypeConverterFilters + get role => $composableBuilder( + column: $table.role, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i4.$$RemoteAlbumEntityTableFilterComposer get albumId { + final i4.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$RemoteAlbumEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$UserEntityTableFilterComposer get userId { + final i6.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$UserEntityTableFilterComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$AlbumUserEntityTableOrderingComposer + extends i0.Composer { + $$AlbumUserEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get role => $composableBuilder( + column: $table.role, builder: (column) => i0.ColumnOrderings(column)); + + i4.$$RemoteAlbumEntityTableOrderingComposer get albumId { + final i4.$$RemoteAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$RemoteAlbumEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$UserEntityTableOrderingComposer get userId { + final i6.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$AlbumUserEntityTableAnnotationComposer + extends i0.Composer { + $$AlbumUserEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumnWithTypeConverter get role => + $composableBuilder(column: $table.role, builder: (column) => column); + + i4.$$RemoteAlbumEntityTableAnnotationComposer get albumId { + final i4.$$RemoteAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i4.$$RemoteAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i6.$$UserEntityTableAnnotationComposer get userId { + final i6.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.userId, + referencedTable: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i6.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i5.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$AlbumUserEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$AlbumUserEntityTable, + i1.AlbumUserEntityData, + i1.$$AlbumUserEntityTableFilterComposer, + i1.$$AlbumUserEntityTableOrderingComposer, + i1.$$AlbumUserEntityTableAnnotationComposer, + $$AlbumUserEntityTableCreateCompanionBuilder, + $$AlbumUserEntityTableUpdateCompanionBuilder, + (i1.AlbumUserEntityData, i1.$$AlbumUserEntityTableReferences), + i1.AlbumUserEntityData, + i0.PrefetchHooks Function({bool albumId, bool userId})> { + $$AlbumUserEntityTableTableManager( + i0.GeneratedDatabase db, i1.$AlbumUserEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$AlbumUserEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + i1.$$AlbumUserEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => i1 + .$$AlbumUserEntityTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + i0.Value albumId = const i0.Value.absent(), + i0.Value userId = const i0.Value.absent(), + i0.Value role = const i0.Value.absent(), + }) => + i1.AlbumUserEntityCompanion( + albumId: albumId, + userId: userId, + role: role, + ), + createCompanionCallback: ({ + required String albumId, + required String userId, + required i2.AlbumUserRole role, + }) => + i1.AlbumUserEntityCompanion.insert( + albumId: albumId, + userId: userId, + role: role, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$AlbumUserEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({albumId = false, userId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: + i1.$$AlbumUserEntityTableReferences._albumIdTable(db), + referencedColumn: i1.$$AlbumUserEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + if (userId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.userId, + referencedTable: + i1.$$AlbumUserEntityTableReferences._userIdTable(db), + referencedColumn: + i1.$$AlbumUserEntityTableReferences._userIdTable(db).id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$AlbumUserEntityTableProcessedTableManager = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$AlbumUserEntityTable, + i1.AlbumUserEntityData, + i1.$$AlbumUserEntityTableFilterComposer, + i1.$$AlbumUserEntityTableOrderingComposer, + i1.$$AlbumUserEntityTableAnnotationComposer, + $$AlbumUserEntityTableCreateCompanionBuilder, + $$AlbumUserEntityTableUpdateCompanionBuilder, + (i1.AlbumUserEntityData, i1.$$AlbumUserEntityTableReferences), + i1.AlbumUserEntityData, + i0.PrefetchHooks Function({bool albumId, bool userId})>; + +class $AlbumUserEntityTable extends i3.AlbumUserEntity + with i0.TableInfo<$AlbumUserEntityTable, i1.AlbumUserEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $AlbumUserEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _userIdMeta = + const i0.VerificationMeta('userId'); + @override + late final i0.GeneratedColumn userId = i0.GeneratedColumn( + 'user_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + @override + late final i0.GeneratedColumnWithTypeConverter role = + i0.GeneratedColumn('role', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$AlbumUserEntityTable.$converterrole); + @override + List get $columns => [albumId, userId, role]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'album_user_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {albumId, userId}; + @override + i1.AlbumUserEntityData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.AlbumUserEntityData( + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + userId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}user_id'])!, + role: i1.$AlbumUserEntityTable.$converterrole.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}role'])!), + ); + } + + @override + $AlbumUserEntityTable createAlias(String alias) { + return $AlbumUserEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converterrole = + const i0.EnumIndexConverter(i2.AlbumUserRole.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class AlbumUserEntityData extends i0.DataClass + implements i0.Insertable { + final String albumId; + final String userId; + final i2.AlbumUserRole role; + const AlbumUserEntityData( + {required this.albumId, required this.userId, required this.role}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['album_id'] = i0.Variable(albumId); + map['user_id'] = i0.Variable(userId); + { + map['role'] = + i0.Variable(i1.$AlbumUserEntityTable.$converterrole.toSql(role)); + } + return map; + } + + factory AlbumUserEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return AlbumUserEntityData( + albumId: serializer.fromJson(json['albumId']), + userId: serializer.fromJson(json['userId']), + role: i1.$AlbumUserEntityTable.$converterrole + .fromJson(serializer.fromJson(json['role'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'albumId': serializer.toJson(albumId), + 'userId': serializer.toJson(userId), + 'role': serializer + .toJson(i1.$AlbumUserEntityTable.$converterrole.toJson(role)), + }; + } + + i1.AlbumUserEntityData copyWith( + {String? albumId, String? userId, i2.AlbumUserRole? role}) => + i1.AlbumUserEntityData( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + AlbumUserEntityData copyWithCompanion(i1.AlbumUserEntityCompanion data) { + return AlbumUserEntityData( + albumId: data.albumId.present ? data.albumId.value : this.albumId, + userId: data.userId.present ? data.userId.value : this.userId, + role: data.role.present ? data.role.value : this.role, + ); + } + + @override + String toString() { + return (StringBuffer('AlbumUserEntityData(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(albumId, userId, role); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.AlbumUserEntityData && + other.albumId == this.albumId && + other.userId == this.userId && + other.role == this.role); +} + +class AlbumUserEntityCompanion + extends i0.UpdateCompanion { + final i0.Value albumId; + final i0.Value userId; + final i0.Value role; + const AlbumUserEntityCompanion({ + this.albumId = const i0.Value.absent(), + this.userId = const i0.Value.absent(), + this.role = const i0.Value.absent(), + }); + AlbumUserEntityCompanion.insert({ + required String albumId, + required String userId, + required i2.AlbumUserRole role, + }) : albumId = i0.Value(albumId), + userId = i0.Value(userId), + role = i0.Value(role); + static i0.Insertable custom({ + i0.Expression? albumId, + i0.Expression? userId, + i0.Expression? role, + }) { + return i0.RawValuesInsertable({ + if (albumId != null) 'album_id': albumId, + if (userId != null) 'user_id': userId, + if (role != null) 'role': role, + }); + } + + i1.AlbumUserEntityCompanion copyWith( + {i0.Value? albumId, + i0.Value? userId, + i0.Value? role}) { + return i1.AlbumUserEntityCompanion( + albumId: albumId ?? this.albumId, + userId: userId ?? this.userId, + role: role ?? this.role, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + if (userId.present) { + map['user_id'] = i0.Variable(userId.value); + } + if (role.present) { + map['role'] = i0.Variable( + i1.$AlbumUserEntityTable.$converterrole.toSql(role.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('AlbumUserEntityCompanion(') + ..write('albumId: $albumId, ') + ..write('userId: $userId, ') + ..write('role: $role') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart index 68bc1b3c5d..a3c79b2e2e 100644 --- a/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/local_asset.entity.drift.dart @@ -14,6 +14,8 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder required i2.AssetType type, i0.Value createdAt, i0.Value updatedAt, + i0.Value width, + i0.Value height, i0.Value durationInSeconds, required String id, i0.Value checksum, @@ -25,6 +27,8 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder i0.Value type, i0.Value createdAt, i0.Value updatedAt, + i0.Value width, + i0.Value height, i0.Value durationInSeconds, i0.Value id, i0.Value checksum, @@ -54,6 +58,12 @@ class $$LocalAssetEntityTableFilterComposer i0.ColumnFilters get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => i0.ColumnFilters(column)); @@ -91,6 +101,12 @@ class $$LocalAssetEntityTableOrderingComposer column: $table.updatedAt, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => i0.ColumnOrderings(column)); @@ -127,6 +143,12 @@ class $$LocalAssetEntityTableAnnotationComposer i0.GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + i0.GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + i0.GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + i0.GeneratedColumn get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => column); @@ -173,6 +195,8 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< i0.Value type = const i0.Value.absent(), i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), i0.Value id = const i0.Value.absent(), i0.Value checksum = const i0.Value.absent(), @@ -183,6 +207,8 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< type: type, createdAt: createdAt, updatedAt: updatedAt, + width: width, + height: height, durationInSeconds: durationInSeconds, id: id, checksum: checksum, @@ -193,6 +219,8 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< required i2.AssetType type, i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), required String id, i0.Value checksum = const i0.Value.absent(), @@ -203,6 +231,8 @@ class $$LocalAssetEntityTableTableManager extends i0.RootTableManager< type: type, createdAt: createdAt, updatedAt: updatedAt, + width: width, + height: height, durationInSeconds: durationInSeconds, id: id, checksum: checksum, @@ -268,6 +298,18 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity type: i0.DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _widthMeta = + const i0.VerificationMeta('width'); + @override + late final i0.GeneratedColumn width = i0.GeneratedColumn( + 'width', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _heightMeta = + const i0.VerificationMeta('height'); + @override + late final i0.GeneratedColumn height = i0.GeneratedColumn( + 'height', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); static const i0.VerificationMeta _durationInSecondsMeta = const i0.VerificationMeta('durationInSeconds'); @override @@ -301,6 +343,8 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity type, createdAt, updatedAt, + width, + height, durationInSeconds, id, checksum, @@ -331,6 +375,14 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } + if (data.containsKey('width')) { + context.handle( + _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } if (data.containsKey('duration_in_seconds')) { context.handle( _durationInSecondsMeta, @@ -371,6 +423,10 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, updatedAt: attachedDatabase.typeMapping.read( i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}height']), durationInSeconds: attachedDatabase.typeMapping.read( i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), id: attachedDatabase.typeMapping @@ -401,6 +457,8 @@ class LocalAssetEntityData extends i0.DataClass final i2.AssetType type; final DateTime createdAt; final DateTime updatedAt; + final int? width; + final int? height; final int? durationInSeconds; final String id; final String? checksum; @@ -410,6 +468,8 @@ class LocalAssetEntityData extends i0.DataClass required this.type, required this.createdAt, required this.updatedAt, + this.width, + this.height, this.durationInSeconds, required this.id, this.checksum, @@ -424,6 +484,12 @@ class LocalAssetEntityData extends i0.DataClass } map['created_at'] = i0.Variable(createdAt); map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = i0.Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = i0.Variable(height); + } if (!nullToAbsent || durationInSeconds != null) { map['duration_in_seconds'] = i0.Variable(durationInSeconds); } @@ -444,6 +510,8 @@ class LocalAssetEntityData extends i0.DataClass .fromJson(serializer.fromJson(json['type'])), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), durationInSeconds: serializer.fromJson(json['durationInSeconds']), id: serializer.fromJson(json['id']), checksum: serializer.fromJson(json['checksum']), @@ -459,6 +527,8 @@ class LocalAssetEntityData extends i0.DataClass .toJson(i1.$LocalAssetEntityTable.$convertertype.toJson(type)), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), 'durationInSeconds': serializer.toJson(durationInSeconds), 'id': serializer.toJson(id), 'checksum': serializer.toJson(checksum), @@ -471,6 +541,8 @@ class LocalAssetEntityData extends i0.DataClass i2.AssetType? type, DateTime? createdAt, DateTime? updatedAt, + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), String? id, i0.Value checksum = const i0.Value.absent(), @@ -480,6 +552,8 @@ class LocalAssetEntityData extends i0.DataClass type: type ?? this.type, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, @@ -493,6 +567,8 @@ class LocalAssetEntityData extends i0.DataClass type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, @@ -510,6 +586,8 @@ class LocalAssetEntityData extends i0.DataClass ..write('type: $type, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') @@ -519,8 +597,8 @@ class LocalAssetEntityData extends i0.DataClass } @override - int get hashCode => Object.hash(name, type, createdAt, updatedAt, - durationInSeconds, id, checksum, isFavorite); + int get hashCode => Object.hash(name, type, createdAt, updatedAt, width, + height, durationInSeconds, id, checksum, isFavorite); @override bool operator ==(Object other) => identical(this, other) || @@ -529,6 +607,8 @@ class LocalAssetEntityData extends i0.DataClass other.type == this.type && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && other.durationInSeconds == this.durationInSeconds && other.id == this.id && other.checksum == this.checksum && @@ -541,6 +621,8 @@ class LocalAssetEntityCompanion final i0.Value type; final i0.Value createdAt; final i0.Value updatedAt; + final i0.Value width; + final i0.Value height; final i0.Value durationInSeconds; final i0.Value id; final i0.Value checksum; @@ -550,6 +632,8 @@ class LocalAssetEntityCompanion this.type = const i0.Value.absent(), this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.height = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), this.id = const i0.Value.absent(), this.checksum = const i0.Value.absent(), @@ -560,6 +644,8 @@ class LocalAssetEntityCompanion required i2.AssetType type, this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.height = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), required String id, this.checksum = const i0.Value.absent(), @@ -572,6 +658,8 @@ class LocalAssetEntityCompanion i0.Expression? type, i0.Expression? createdAt, i0.Expression? updatedAt, + i0.Expression? width, + i0.Expression? height, i0.Expression? durationInSeconds, i0.Expression? id, i0.Expression? checksum, @@ -582,6 +670,8 @@ class LocalAssetEntityCompanion if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, if (id != null) 'id': id, if (checksum != null) 'checksum': checksum, @@ -594,6 +684,8 @@ class LocalAssetEntityCompanion i0.Value? type, i0.Value? createdAt, i0.Value? updatedAt, + i0.Value? width, + i0.Value? height, i0.Value? durationInSeconds, i0.Value? id, i0.Value? checksum, @@ -603,6 +695,8 @@ class LocalAssetEntityCompanion type: type ?? this.type, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, durationInSeconds: durationInSeconds ?? this.durationInSeconds, id: id ?? this.id, checksum: checksum ?? this.checksum, @@ -626,6 +720,12 @@ class LocalAssetEntityCompanion if (updatedAt.present) { map['updated_at'] = i0.Variable(updatedAt.value); } + if (width.present) { + map['width'] = i0.Variable(width.value); + } + if (height.present) { + map['height'] = i0.Variable(height.value); + } if (durationInSeconds.present) { map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); } @@ -648,6 +748,8 @@ class LocalAssetEntityCompanion ..write('type: $type, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart new file mode 100644 index 0000000000..7f9f1efa67 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumEntity(); + + TextColumn get id => text()(); + + TextColumn get name => text()(); + + TextColumn get description => text()(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get thumbnailAssetId => text() + .references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull) + .nullable()(); + + BoolColumn get isActivityEnabled => + boolean().withDefault(const Constant(true))(); + + IntColumn get order => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart new file mode 100644 index 0000000000..cc7b642c9b --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart @@ -0,0 +1,946 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i1; +import 'package:immich_mobile/domain/models/album/album.model.dart' as i2; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart' + as i3; +import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart' + as i5; +import 'package:drift/internal/modular.dart' as i6; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i7; + +typedef $$RemoteAlbumEntityTableCreateCompanionBuilder + = i1.RemoteAlbumEntityCompanion Function({ + required String id, + required String name, + required String description, + i0.Value createdAt, + i0.Value updatedAt, + required String ownerId, + i0.Value thumbnailAssetId, + i0.Value isActivityEnabled, + required i2.AssetOrder order, +}); +typedef $$RemoteAlbumEntityTableUpdateCompanionBuilder + = i1.RemoteAlbumEntityCompanion Function({ + i0.Value id, + i0.Value name, + i0.Value description, + i0.Value createdAt, + i0.Value updatedAt, + i0.Value ownerId, + i0.Value thumbnailAssetId, + i0.Value isActivityEnabled, + i0.Value order, +}); + +final class $$RemoteAlbumEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAlbumEntityTable, + i1.RemoteAlbumEntityData> { + $$RemoteAlbumEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .ownerId, + i6.ReadDatabaseContainer(db) + .resultSet('user_entity') + .id)); + + i5.$$UserEntityTableProcessedTableManager get ownerId { + final $_column = $_itemColumn('owner_id')!; + + final manager = i5 + .$$UserEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('user_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_ownerIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i7.$RemoteAssetEntityTable _thumbnailAssetIdTable( + i0.GeneratedDatabase db) => + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i6.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .thumbnailAssetId, + i6.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i7.$$RemoteAssetEntityTableProcessedTableManager? get thumbnailAssetId { + final $_column = $_itemColumn('thumbnail_asset_id'); + if ($_column == null) return null; + final manager = i7 + .$$RemoteAssetEntityTableTableManager( + $_db, + i6.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_thumbnailAssetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAlbumEntityTableFilterComposer + extends i0.Composer { + $$RemoteAlbumEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get createdAt => $composableBuilder( + column: $table.createdAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get updatedAt => $composableBuilder( + column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get isActivityEnabled => $composableBuilder( + column: $table.isActivityEnabled, + builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnWithTypeConverterFilters + get order => $composableBuilder( + column: $table.order, + builder: (column) => i0.ColumnWithTypeConverterFilters(column)); + + i5.$$UserEntityTableFilterComposer get ownerId { + final i5.$$UserEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i7.$$RemoteAssetEntityTableFilterComposer get thumbnailAssetId { + final i7.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailAssetId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i7.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAlbumEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get name => $composableBuilder( + column: $table.name, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get description => $composableBuilder( + column: $table.description, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get createdAt => $composableBuilder( + column: $table.createdAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get updatedAt => $composableBuilder( + column: $table.updatedAt, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get isActivityEnabled => $composableBuilder( + column: $table.isActivityEnabled, + builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get order => $composableBuilder( + column: $table.order, builder: (column) => i0.ColumnOrderings(column)); + + i5.$$UserEntityTableOrderingComposer get ownerId { + final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i7.$$RemoteAssetEntityTableOrderingComposer get thumbnailAssetId { + final i7.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailAssetId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i7.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAlbumEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i0.GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + i0.GeneratedColumn get name => + $composableBuilder(column: $table.name, builder: (column) => column); + + i0.GeneratedColumn get description => $composableBuilder( + column: $table.description, builder: (column) => column); + + i0.GeneratedColumn get createdAt => + $composableBuilder(column: $table.createdAt, builder: (column) => column); + + i0.GeneratedColumn get updatedAt => + $composableBuilder(column: $table.updatedAt, builder: (column) => column); + + i0.GeneratedColumn get isActivityEnabled => $composableBuilder( + column: $table.isActivityEnabled, builder: (column) => column); + + i0.GeneratedColumnWithTypeConverter get order => + $composableBuilder(column: $table.order, builder: (column) => column); + + i5.$$UserEntityTableAnnotationComposer get ownerId { + final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.ownerId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$UserEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet('user_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i7.$$RemoteAssetEntityTableAnnotationComposer get thumbnailAssetId { + final i7.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.thumbnailAssetId, + referencedTable: i6.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i7.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i6.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumEntityTable, + i1.RemoteAlbumEntityData, + i1.$$RemoteAlbumEntityTableFilterComposer, + i1.$$RemoteAlbumEntityTableOrderingComposer, + i1.$$RemoteAlbumEntityTableAnnotationComposer, + $$RemoteAlbumEntityTableCreateCompanionBuilder, + $$RemoteAlbumEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences), + i1.RemoteAlbumEntityData, + i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})> { + $$RemoteAlbumEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAlbumEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAlbumEntityTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => i1 + .$$RemoteAlbumEntityTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAlbumEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value id = const i0.Value.absent(), + i0.Value name = const i0.Value.absent(), + i0.Value description = const i0.Value.absent(), + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + i0.Value ownerId = const i0.Value.absent(), + i0.Value thumbnailAssetId = const i0.Value.absent(), + i0.Value isActivityEnabled = const i0.Value.absent(), + i0.Value order = const i0.Value.absent(), + }) => + i1.RemoteAlbumEntityCompanion( + id: id, + name: name, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ), + createCompanionCallback: ({ + required String id, + required String name, + required String description, + i0.Value createdAt = const i0.Value.absent(), + i0.Value updatedAt = const i0.Value.absent(), + required String ownerId, + i0.Value thumbnailAssetId = const i0.Value.absent(), + i0.Value isActivityEnabled = const i0.Value.absent(), + required i2.AssetOrder order, + }) => + i1.RemoteAlbumEntityCompanion.insert( + id: id, + name: name, + description: description, + createdAt: createdAt, + updatedAt: updatedAt, + ownerId: ownerId, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAlbumEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({ownerId = false, thumbnailAssetId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (ownerId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.ownerId, + referencedTable: + i1.$$RemoteAlbumEntityTableReferences._ownerIdTable(db), + referencedColumn: i1.$$RemoteAlbumEntityTableReferences + ._ownerIdTable(db) + .id, + ) as T; + } + if (thumbnailAssetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.thumbnailAssetId, + referencedTable: i1.$$RemoteAlbumEntityTableReferences + ._thumbnailAssetIdTable(db), + referencedColumn: i1.$$RemoteAlbumEntityTableReferences + ._thumbnailAssetIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAlbumEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumEntityTable, + i1.RemoteAlbumEntityData, + i1.$$RemoteAlbumEntityTableFilterComposer, + i1.$$RemoteAlbumEntityTableOrderingComposer, + i1.$$RemoteAlbumEntityTableAnnotationComposer, + $$RemoteAlbumEntityTableCreateCompanionBuilder, + $$RemoteAlbumEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumEntityData, i1.$$RemoteAlbumEntityTableReferences), + i1.RemoteAlbumEntityData, + i0.PrefetchHooks Function({bool ownerId, bool thumbnailAssetId})>; + +class $RemoteAlbumEntityTable extends i3.RemoteAlbumEntity + with i0.TableInfo<$RemoteAlbumEntityTable, i1.RemoteAlbumEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAlbumEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id'); + @override + late final i0.GeneratedColumn id = i0.GeneratedColumn( + 'id', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _nameMeta = + const i0.VerificationMeta('name'); + @override + late final i0.GeneratedColumn name = i0.GeneratedColumn( + 'name', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _descriptionMeta = + const i0.VerificationMeta('description'); + @override + late final i0.GeneratedColumn description = + i0.GeneratedColumn('description', aliasedName, false, + type: i0.DriftSqlType.string, requiredDuringInsert: true); + static const i0.VerificationMeta _createdAtMeta = + const i0.VerificationMeta('createdAt'); + @override + late final i0.GeneratedColumn createdAt = + i0.GeneratedColumn('created_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _updatedAtMeta = + const i0.VerificationMeta('updatedAt'); + @override + late final i0.GeneratedColumn updatedAt = + i0.GeneratedColumn('updated_at', aliasedName, false, + type: i0.DriftSqlType.dateTime, + requiredDuringInsert: false, + defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _ownerIdMeta = + const i0.VerificationMeta('ownerId'); + @override + late final i0.GeneratedColumn ownerId = i0.GeneratedColumn( + 'owner_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES user_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _thumbnailAssetIdMeta = + const i0.VerificationMeta('thumbnailAssetId'); + @override + late final i0.GeneratedColumn thumbnailAssetId = + i0.GeneratedColumn('thumbnail_asset_id', aliasedName, true, + type: i0.DriftSqlType.string, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE SET NULL')); + static const i0.VerificationMeta _isActivityEnabledMeta = + const i0.VerificationMeta('isActivityEnabled'); + @override + late final i0.GeneratedColumn isActivityEnabled = + i0.GeneratedColumn('is_activity_enabled', aliasedName, false, + type: i0.DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'CHECK ("is_activity_enabled" IN (0, 1))'), + defaultValue: const i4.Constant(true)); + @override + late final i0.GeneratedColumnWithTypeConverter order = + i0.GeneratedColumn('order', aliasedName, false, + type: i0.DriftSqlType.int, requiredDuringInsert: true) + .withConverter( + i1.$RemoteAlbumEntityTable.$converterorder); + @override + List get $columns => [ + id, + name, + description, + createdAt, + updatedAt, + ownerId, + thumbnailAssetId, + isActivityEnabled, + order + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('name')) { + context.handle( + _nameMeta, name.isAcceptableOrUnknown(data['name']!, _nameMeta)); + } else if (isInserting) { + context.missing(_nameMeta); + } + if (data.containsKey('description')) { + context.handle( + _descriptionMeta, + description.isAcceptableOrUnknown( + data['description']!, _descriptionMeta)); + } else if (isInserting) { + context.missing(_descriptionMeta); + } + if (data.containsKey('created_at')) { + context.handle(_createdAtMeta, + createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta)); + } + if (data.containsKey('updated_at')) { + context.handle(_updatedAtMeta, + updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); + } + if (data.containsKey('owner_id')) { + context.handle(_ownerIdMeta, + ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta)); + } else if (isInserting) { + context.missing(_ownerIdMeta); + } + if (data.containsKey('thumbnail_asset_id')) { + context.handle( + _thumbnailAssetIdMeta, + thumbnailAssetId.isAcceptableOrUnknown( + data['thumbnail_asset_id']!, _thumbnailAssetIdMeta)); + } + if (data.containsKey('is_activity_enabled')) { + context.handle( + _isActivityEnabledMeta, + isActivityEnabled.isAcceptableOrUnknown( + data['is_activity_enabled']!, _isActivityEnabledMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id}; + @override + i1.RemoteAlbumEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAlbumEntityData( + id: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!, + name: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}name'])!, + description: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}description'])!, + createdAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, + updatedAt: attachedDatabase.typeMapping.read( + i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + ownerId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!, + thumbnailAssetId: attachedDatabase.typeMapping.read( + i0.DriftSqlType.string, data['${effectivePrefix}thumbnail_asset_id']), + isActivityEnabled: attachedDatabase.typeMapping.read( + i0.DriftSqlType.bool, data['${effectivePrefix}is_activity_enabled'])!, + order: i1.$RemoteAlbumEntityTable.$converterorder.fromSql(attachedDatabase + .typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}order'])!), + ); + } + + @override + $RemoteAlbumEntityTable createAlias(String alias) { + return $RemoteAlbumEntityTable(attachedDatabase, alias); + } + + static i0.JsonTypeConverter2 $converterorder = + const i0.EnumIndexConverter(i2.AssetOrder.values); + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumEntityData extends i0.DataClass + implements i0.Insertable { + final String id; + final String name; + final String description; + final DateTime createdAt; + final DateTime updatedAt; + final String ownerId; + final String? thumbnailAssetId; + final bool isActivityEnabled; + final i2.AssetOrder order; + const RemoteAlbumEntityData( + {required this.id, + required this.name, + required this.description, + required this.createdAt, + required this.updatedAt, + required this.ownerId, + this.thumbnailAssetId, + required this.isActivityEnabled, + required this.order}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['id'] = i0.Variable(id); + map['name'] = i0.Variable(name); + map['description'] = i0.Variable(description); + map['created_at'] = i0.Variable(createdAt); + map['updated_at'] = i0.Variable(updatedAt); + map['owner_id'] = i0.Variable(ownerId); + if (!nullToAbsent || thumbnailAssetId != null) { + map['thumbnail_asset_id'] = i0.Variable(thumbnailAssetId); + } + map['is_activity_enabled'] = i0.Variable(isActivityEnabled); + { + map['order'] = i0.Variable( + i1.$RemoteAlbumEntityTable.$converterorder.toSql(order)); + } + return map; + } + + factory RemoteAlbumEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAlbumEntityData( + id: serializer.fromJson(json['id']), + name: serializer.fromJson(json['name']), + description: serializer.fromJson(json['description']), + createdAt: serializer.fromJson(json['createdAt']), + updatedAt: serializer.fromJson(json['updatedAt']), + ownerId: serializer.fromJson(json['ownerId']), + thumbnailAssetId: serializer.fromJson(json['thumbnailAssetId']), + isActivityEnabled: serializer.fromJson(json['isActivityEnabled']), + order: i1.$RemoteAlbumEntityTable.$converterorder + .fromJson(serializer.fromJson(json['order'])), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'id': serializer.toJson(id), + 'name': serializer.toJson(name), + 'description': serializer.toJson(description), + 'createdAt': serializer.toJson(createdAt), + 'updatedAt': serializer.toJson(updatedAt), + 'ownerId': serializer.toJson(ownerId), + 'thumbnailAssetId': serializer.toJson(thumbnailAssetId), + 'isActivityEnabled': serializer.toJson(isActivityEnabled), + 'order': serializer.toJson( + i1.$RemoteAlbumEntityTable.$converterorder.toJson(order)), + }; + } + + i1.RemoteAlbumEntityData copyWith( + {String? id, + String? name, + String? description, + DateTime? createdAt, + DateTime? updatedAt, + String? ownerId, + i0.Value thumbnailAssetId = const i0.Value.absent(), + bool? isActivityEnabled, + i2.AssetOrder? order}) => + i1.RemoteAlbumEntityData( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId.present + ? thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + RemoteAlbumEntityData copyWithCompanion(i1.RemoteAlbumEntityCompanion data) { + return RemoteAlbumEntityData( + id: data.id.present ? data.id.value : this.id, + name: data.name.present ? data.name.value : this.name, + description: + data.description.present ? data.description.value : this.description, + createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, + updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId, + thumbnailAssetId: data.thumbnailAssetId.present + ? data.thumbnailAssetId.value + : this.thumbnailAssetId, + isActivityEnabled: data.isActivityEnabled.present + ? data.isActivityEnabled.value + : this.isActivityEnabled, + order: data.order.present ? data.order.value : this.order, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityData(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(id, name, description, createdAt, updatedAt, + ownerId, thumbnailAssetId, isActivityEnabled, order); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAlbumEntityData && + other.id == this.id && + other.name == this.name && + other.description == this.description && + other.createdAt == this.createdAt && + other.updatedAt == this.updatedAt && + other.ownerId == this.ownerId && + other.thumbnailAssetId == this.thumbnailAssetId && + other.isActivityEnabled == this.isActivityEnabled && + other.order == this.order); +} + +class RemoteAlbumEntityCompanion + extends i0.UpdateCompanion { + final i0.Value id; + final i0.Value name; + final i0.Value description; + final i0.Value createdAt; + final i0.Value updatedAt; + final i0.Value ownerId; + final i0.Value thumbnailAssetId; + final i0.Value isActivityEnabled; + final i0.Value order; + const RemoteAlbumEntityCompanion({ + this.id = const i0.Value.absent(), + this.name = const i0.Value.absent(), + this.description = const i0.Value.absent(), + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + this.ownerId = const i0.Value.absent(), + this.thumbnailAssetId = const i0.Value.absent(), + this.isActivityEnabled = const i0.Value.absent(), + this.order = const i0.Value.absent(), + }); + RemoteAlbumEntityCompanion.insert({ + required String id, + required String name, + required String description, + this.createdAt = const i0.Value.absent(), + this.updatedAt = const i0.Value.absent(), + required String ownerId, + this.thumbnailAssetId = const i0.Value.absent(), + this.isActivityEnabled = const i0.Value.absent(), + required i2.AssetOrder order, + }) : id = i0.Value(id), + name = i0.Value(name), + description = i0.Value(description), + ownerId = i0.Value(ownerId), + order = i0.Value(order); + static i0.Insertable custom({ + i0.Expression? id, + i0.Expression? name, + i0.Expression? description, + i0.Expression? createdAt, + i0.Expression? updatedAt, + i0.Expression? ownerId, + i0.Expression? thumbnailAssetId, + i0.Expression? isActivityEnabled, + i0.Expression? order, + }) { + return i0.RawValuesInsertable({ + if (id != null) 'id': id, + if (name != null) 'name': name, + if (description != null) 'description': description, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + if (ownerId != null) 'owner_id': ownerId, + if (thumbnailAssetId != null) 'thumbnail_asset_id': thumbnailAssetId, + if (isActivityEnabled != null) 'is_activity_enabled': isActivityEnabled, + if (order != null) 'order': order, + }); + } + + i1.RemoteAlbumEntityCompanion copyWith( + {i0.Value? id, + i0.Value? name, + i0.Value? description, + i0.Value? createdAt, + i0.Value? updatedAt, + i0.Value? ownerId, + i0.Value? thumbnailAssetId, + i0.Value? isActivityEnabled, + i0.Value? order}) { + return i1.RemoteAlbumEntityCompanion( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + createdAt: createdAt ?? this.createdAt, + updatedAt: updatedAt ?? this.updatedAt, + ownerId: ownerId ?? this.ownerId, + thumbnailAssetId: thumbnailAssetId ?? this.thumbnailAssetId, + isActivityEnabled: isActivityEnabled ?? this.isActivityEnabled, + order: order ?? this.order, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (id.present) { + map['id'] = i0.Variable(id.value); + } + if (name.present) { + map['name'] = i0.Variable(name.value); + } + if (description.present) { + map['description'] = i0.Variable(description.value); + } + if (createdAt.present) { + map['created_at'] = i0.Variable(createdAt.value); + } + if (updatedAt.present) { + map['updated_at'] = i0.Variable(updatedAt.value); + } + if (ownerId.present) { + map['owner_id'] = i0.Variable(ownerId.value); + } + if (thumbnailAssetId.present) { + map['thumbnail_asset_id'] = i0.Variable(thumbnailAssetId.value); + } + if (isActivityEnabled.present) { + map['is_activity_enabled'] = i0.Variable(isActivityEnabled.value); + } + if (order.present) { + map['order'] = i0.Variable( + i1.$RemoteAlbumEntityTable.$converterorder.toSql(order.value)); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumEntityCompanion(') + ..write('id: $id, ') + ..write('name: $name, ') + ..write('description: $description, ') + ..write('createdAt: $createdAt, ') + ..write('updatedAt: $updatedAt, ') + ..write('ownerId: $ownerId, ') + ..write('thumbnailAssetId: $thumbnailAssetId, ') + ..write('isActivityEnabled: $isActivityEnabled, ') + ..write('order: $order') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart new file mode 100644 index 0000000000..1dcc336ed8 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.dart @@ -0,0 +1,17 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumAssetEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumAssetEntity(); + + TextColumn get assetId => + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart new file mode 100644 index 0000000000..ab50607c96 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart @@ -0,0 +1,565 @@ +// dart format width=80 +// ignore_for_file: type=lint +import 'package:drift/drift.dart' as i0; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' + as i1; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart' + as i2; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart' + as i3; +import 'package:drift/internal/modular.dart' as i4; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i5; + +typedef $$RemoteAlbumAssetEntityTableCreateCompanionBuilder + = i1.RemoteAlbumAssetEntityCompanion Function({ + required String assetId, + required String albumId, +}); +typedef $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder + = i1.RemoteAlbumAssetEntityCompanion Function({ + i0.Value assetId, + i0.Value albumId, +}); + +final class $$RemoteAlbumAssetEntityTableReferences extends i0.BaseReferences< + i0.GeneratedDatabase, + i1.$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData> { + $$RemoteAlbumAssetEntityTableReferences( + super.$_db, super.$_table, super.$_typedResult); + + static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'remote_album_asset_entity') + .assetId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_asset_entity') + .id)); + + i3.$$RemoteAssetEntityTableProcessedTableManager get assetId { + final $_column = $_itemColumn('asset_id')!; + + final manager = i3 + .$$RemoteAssetEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_asset_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_assetIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } + + static i5.$RemoteAlbumEntityTable _albumIdTable(i0.GeneratedDatabase db) => + i4.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .createAlias(i0.$_aliasNameGenerator( + i4.ReadDatabaseContainer(db) + .resultSet( + 'remote_album_asset_entity') + .albumId, + i4.ReadDatabaseContainer(db) + .resultSet('remote_album_entity') + .id)); + + i5.$$RemoteAlbumEntityTableProcessedTableManager get albumId { + final $_column = $_itemColumn('album_id')!; + + final manager = i5 + .$$RemoteAlbumEntityTableTableManager( + $_db, + i4.ReadDatabaseContainer($_db) + .resultSet('remote_album_entity')) + .filter((f) => f.id.sqlEquals($_column)); + final item = $_typedResult.readTableOrNull(_albumIdTable($_db)); + if (item == null) return manager; + return i0.ProcessedTableManager( + manager.$state.copyWith(prefetchedData: [item])); + } +} + +class $$RemoteAlbumAssetEntityTableFilterComposer + extends i0.Composer { + $$RemoteAlbumAssetEntityTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableFilterComposer get assetId { + final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$RemoteAlbumEntityTableFilterComposer get albumId { + final i5.$$RemoteAlbumEntityTableFilterComposer composer = $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$RemoteAlbumEntityTableFilterComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumAssetEntityTableOrderingComposer + extends i0.Composer { + $$RemoteAlbumAssetEntityTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableOrderingComposer get assetId { + final i3.$$RemoteAssetEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$RemoteAlbumEntityTableOrderingComposer get albumId { + final i5.$$RemoteAlbumEntityTableOrderingComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$RemoteAlbumEntityTableOrderingComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumAssetEntityTableAnnotationComposer + extends i0.Composer { + $$RemoteAlbumAssetEntityTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + i3.$$RemoteAssetEntityTableAnnotationComposer get assetId { + final i3.$$RemoteAssetEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.assetId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_asset_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i3.$$RemoteAssetEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_asset_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } + + i5.$$RemoteAlbumEntityTableAnnotationComposer get albumId { + final i5.$$RemoteAlbumEntityTableAnnotationComposer composer = + $composerBuilder( + composer: this, + getCurrentColumn: (t) => t.albumId, + referencedTable: i4.ReadDatabaseContainer($db) + .resultSet('remote_album_entity'), + getReferencedColumn: (t) => t.id, + builder: (joinBuilder, + {$addJoinBuilderToRootComposer, + $removeJoinBuilderFromRootComposer}) => + i5.$$RemoteAlbumEntityTableAnnotationComposer( + $db: $db, + $table: i4.ReadDatabaseContainer($db) + .resultSet( + 'remote_album_entity'), + $addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer, + joinBuilder: joinBuilder, + $removeJoinBuilderFromRootComposer: + $removeJoinBuilderFromRootComposer, + )); + return composer; + } +} + +class $$RemoteAlbumAssetEntityTableTableManager extends i0.RootTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData, + i1.$$RemoteAlbumAssetEntityTableFilterComposer, + i1.$$RemoteAlbumAssetEntityTableOrderingComposer, + i1.$$RemoteAlbumAssetEntityTableAnnotationComposer, + $$RemoteAlbumAssetEntityTableCreateCompanionBuilder, + $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder, + (i1.RemoteAlbumAssetEntityData, i1.$$RemoteAlbumAssetEntityTableReferences), + i1.RemoteAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})> { + $$RemoteAlbumAssetEntityTableTableManager( + i0.GeneratedDatabase db, i1.$RemoteAlbumAssetEntityTable table) + : super(i0.TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + i1.$$RemoteAlbumAssetEntityTableFilterComposer( + $db: db, $table: table), + createOrderingComposer: () => + i1.$$RemoteAlbumAssetEntityTableOrderingComposer( + $db: db, $table: table), + createComputedFieldComposer: () => + i1.$$RemoteAlbumAssetEntityTableAnnotationComposer( + $db: db, $table: table), + updateCompanionCallback: ({ + i0.Value assetId = const i0.Value.absent(), + i0.Value albumId = const i0.Value.absent(), + }) => + i1.RemoteAlbumAssetEntityCompanion( + assetId: assetId, + albumId: albumId, + ), + createCompanionCallback: ({ + required String assetId, + required String albumId, + }) => + i1.RemoteAlbumAssetEntityCompanion.insert( + assetId: assetId, + albumId: albumId, + ), + withReferenceMapper: (p0) => p0 + .map((e) => ( + e.readTable(table), + i1.$$RemoteAlbumAssetEntityTableReferences(db, table, e) + )) + .toList(), + prefetchHooksCallback: ({assetId = false, albumId = false}) { + return i0.PrefetchHooks( + db: db, + explicitlyWatchedTables: [], + addJoins: < + T extends i0.TableManagerState< + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic, + dynamic>>(state) { + if (assetId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.assetId, + referencedTable: i1.$$RemoteAlbumAssetEntityTableReferences + ._assetIdTable(db), + referencedColumn: i1.$$RemoteAlbumAssetEntityTableReferences + ._assetIdTable(db) + .id, + ) as T; + } + if (albumId) { + state = state.withJoin( + currentTable: table, + currentColumn: table.albumId, + referencedTable: i1.$$RemoteAlbumAssetEntityTableReferences + ._albumIdTable(db), + referencedColumn: i1.$$RemoteAlbumAssetEntityTableReferences + ._albumIdTable(db) + .id, + ) as T; + } + + return state; + }, + getPrefetchedDataCallback: (items) async { + return []; + }, + ); + }, + )); +} + +typedef $$RemoteAlbumAssetEntityTableProcessedTableManager + = i0.ProcessedTableManager< + i0.GeneratedDatabase, + i1.$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData, + i1.$$RemoteAlbumAssetEntityTableFilterComposer, + i1.$$RemoteAlbumAssetEntityTableOrderingComposer, + i1.$$RemoteAlbumAssetEntityTableAnnotationComposer, + $$RemoteAlbumAssetEntityTableCreateCompanionBuilder, + $$RemoteAlbumAssetEntityTableUpdateCompanionBuilder, + ( + i1.RemoteAlbumAssetEntityData, + i1.$$RemoteAlbumAssetEntityTableReferences + ), + i1.RemoteAlbumAssetEntityData, + i0.PrefetchHooks Function({bool assetId, bool albumId})>; + +class $RemoteAlbumAssetEntityTable extends i2.RemoteAlbumAssetEntity + with + i0.TableInfo<$RemoteAlbumAssetEntityTable, + i1.RemoteAlbumAssetEntityData> { + @override + final i0.GeneratedDatabase attachedDatabase; + final String? _alias; + $RemoteAlbumAssetEntityTable(this.attachedDatabase, [this._alias]); + static const i0.VerificationMeta _assetIdMeta = + const i0.VerificationMeta('assetId'); + @override + late final i0.GeneratedColumn assetId = i0.GeneratedColumn( + 'asset_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_asset_entity (id) ON DELETE CASCADE')); + static const i0.VerificationMeta _albumIdMeta = + const i0.VerificationMeta('albumId'); + @override + late final i0.GeneratedColumn albumId = i0.GeneratedColumn( + 'album_id', aliasedName, false, + type: i0.DriftSqlType.string, + requiredDuringInsert: true, + defaultConstraints: i0.GeneratedColumn.constraintIsAlways( + 'REFERENCES remote_album_entity (id) ON DELETE CASCADE')); + @override + List get $columns => [assetId, albumId]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'remote_album_asset_entity'; + @override + i0.VerificationContext validateIntegrity( + i0.Insertable instance, + {bool isInserting = false}) { + final context = i0.VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('asset_id')) { + context.handle(_assetIdMeta, + assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta)); + } else if (isInserting) { + context.missing(_assetIdMeta); + } + if (data.containsKey('album_id')) { + context.handle(_albumIdMeta, + albumId.isAcceptableOrUnknown(data['album_id']!, _albumIdMeta)); + } else if (isInserting) { + context.missing(_albumIdMeta); + } + return context; + } + + @override + Set get $primaryKey => {assetId, albumId}; + @override + i1.RemoteAlbumAssetEntityData map(Map data, + {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return i1.RemoteAlbumAssetEntityData( + assetId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!, + albumId: attachedDatabase.typeMapping + .read(i0.DriftSqlType.string, data['${effectivePrefix}album_id'])!, + ); + } + + @override + $RemoteAlbumAssetEntityTable createAlias(String alias) { + return $RemoteAlbumAssetEntityTable(attachedDatabase, alias); + } + + @override + bool get withoutRowId => true; + @override + bool get isStrict => true; +} + +class RemoteAlbumAssetEntityData extends i0.DataClass + implements i0.Insertable { + final String assetId; + final String albumId; + const RemoteAlbumAssetEntityData( + {required this.assetId, required this.albumId}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['asset_id'] = i0.Variable(assetId); + map['album_id'] = i0.Variable(albumId); + return map; + } + + factory RemoteAlbumAssetEntityData.fromJson(Map json, + {i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return RemoteAlbumAssetEntityData( + assetId: serializer.fromJson(json['assetId']), + albumId: serializer.fromJson(json['albumId']), + ); + } + @override + Map toJson({i0.ValueSerializer? serializer}) { + serializer ??= i0.driftRuntimeOptions.defaultSerializer; + return { + 'assetId': serializer.toJson(assetId), + 'albumId': serializer.toJson(albumId), + }; + } + + i1.RemoteAlbumAssetEntityData copyWith({String? assetId, String? albumId}) => + i1.RemoteAlbumAssetEntityData( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + RemoteAlbumAssetEntityData copyWithCompanion( + i1.RemoteAlbumAssetEntityCompanion data) { + return RemoteAlbumAssetEntityData( + assetId: data.assetId.present ? data.assetId.value : this.assetId, + albumId: data.albumId.present ? data.albumId.value : this.albumId, + ); + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityData(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(assetId, albumId); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is i1.RemoteAlbumAssetEntityData && + other.assetId == this.assetId && + other.albumId == this.albumId); +} + +class RemoteAlbumAssetEntityCompanion + extends i0.UpdateCompanion { + final i0.Value assetId; + final i0.Value albumId; + const RemoteAlbumAssetEntityCompanion({ + this.assetId = const i0.Value.absent(), + this.albumId = const i0.Value.absent(), + }); + RemoteAlbumAssetEntityCompanion.insert({ + required String assetId, + required String albumId, + }) : assetId = i0.Value(assetId), + albumId = i0.Value(albumId); + static i0.Insertable custom({ + i0.Expression? assetId, + i0.Expression? albumId, + }) { + return i0.RawValuesInsertable({ + if (assetId != null) 'asset_id': assetId, + if (albumId != null) 'album_id': albumId, + }); + } + + i1.RemoteAlbumAssetEntityCompanion copyWith( + {i0.Value? assetId, i0.Value? albumId}) { + return i1.RemoteAlbumAssetEntityCompanion( + assetId: assetId ?? this.assetId, + albumId: albumId ?? this.albumId, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (assetId.present) { + map['asset_id'] = i0.Variable(assetId.value); + } + if (albumId.present) { + map['album_id'] = i0.Variable(albumId.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('RemoteAlbumAssetEntityCompanion(') + ..write('assetId: $assetId, ') + ..write('albumId: $albumId') + ..write(')')) + .toString(); + } +} diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart index e3fe521700..c244b5a09a 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.drift.dart @@ -17,6 +17,8 @@ typedef $$RemoteAssetEntityTableCreateCompanionBuilder required i2.AssetType type, i0.Value createdAt, i0.Value updatedAt, + i0.Value width, + i0.Value height, i0.Value durationInSeconds, required String id, required String checksum, @@ -33,6 +35,8 @@ typedef $$RemoteAssetEntityTableUpdateCompanionBuilder i0.Value type, i0.Value createdAt, i0.Value updatedAt, + i0.Value width, + i0.Value height, i0.Value durationInSeconds, i0.Value id, i0.Value checksum, @@ -101,6 +105,12 @@ class $$RemoteAssetEntityTableFilterComposer i0.ColumnFilters get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnFilters(column)); + + i0.ColumnFilters get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnFilters(column)); + i0.ColumnFilters get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => i0.ColumnFilters(column)); @@ -175,6 +185,12 @@ class $$RemoteAssetEntityTableOrderingComposer column: $table.updatedAt, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get width => $composableBuilder( + column: $table.width, builder: (column) => i0.ColumnOrderings(column)); + + i0.ColumnOrderings get height => $composableBuilder( + column: $table.height, builder: (column) => i0.ColumnOrderings(column)); + i0.ColumnOrderings get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => i0.ColumnOrderings(column)); @@ -249,6 +265,12 @@ class $$RemoteAssetEntityTableAnnotationComposer i0.GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + i0.GeneratedColumn get width => + $composableBuilder(column: $table.width, builder: (column) => column); + + i0.GeneratedColumn get height => + $composableBuilder(column: $table.height, builder: (column) => column); + i0.GeneratedColumn get durationInSeconds => $composableBuilder( column: $table.durationInSeconds, builder: (column) => column); @@ -326,6 +348,8 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< i0.Value type = const i0.Value.absent(), i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), i0.Value id = const i0.Value.absent(), i0.Value checksum = const i0.Value.absent(), @@ -341,6 +365,8 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< type: type, createdAt: createdAt, updatedAt: updatedAt, + width: width, + height: height, durationInSeconds: durationInSeconds, id: id, checksum: checksum, @@ -356,6 +382,8 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< required i2.AssetType type, i0.Value createdAt = const i0.Value.absent(), i0.Value updatedAt = const i0.Value.absent(), + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), required String id, required String checksum, @@ -371,6 +399,8 @@ class $$RemoteAssetEntityTableTableManager extends i0.RootTableManager< type: type, createdAt: createdAt, updatedAt: updatedAt, + width: width, + height: height, durationInSeconds: durationInSeconds, id: id, checksum: checksum, @@ -477,6 +507,18 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity type: i0.DriftSqlType.dateTime, requiredDuringInsert: false, defaultValue: i4.currentDateAndTime); + static const i0.VerificationMeta _widthMeta = + const i0.VerificationMeta('width'); + @override + late final i0.GeneratedColumn width = i0.GeneratedColumn( + 'width', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); + static const i0.VerificationMeta _heightMeta = + const i0.VerificationMeta('height'); + @override + late final i0.GeneratedColumn height = i0.GeneratedColumn( + 'height', aliasedName, true, + type: i0.DriftSqlType.int, requiredDuringInsert: false); static const i0.VerificationMeta _durationInSecondsMeta = const i0.VerificationMeta('durationInSeconds'); @override @@ -543,6 +585,8 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity type, createdAt, updatedAt, + width, + height, durationInSeconds, id, checksum, @@ -578,6 +622,14 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity context.handle(_updatedAtMeta, updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta)); } + if (data.containsKey('width')) { + context.handle( + _widthMeta, width.isAcceptableOrUnknown(data['width']!, _widthMeta)); + } + if (data.containsKey('height')) { + context.handle(_heightMeta, + height.isAcceptableOrUnknown(data['height']!, _heightMeta)); + } if (data.containsKey('duration_in_seconds')) { context.handle( _durationInSecondsMeta, @@ -640,6 +692,10 @@ class $RemoteAssetEntityTable extends i3.RemoteAssetEntity i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, updatedAt: attachedDatabase.typeMapping.read( i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + width: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}width']), + height: attachedDatabase.typeMapping + .read(i0.DriftSqlType.int, data['${effectivePrefix}height']), durationInSeconds: attachedDatabase.typeMapping.read( i0.DriftSqlType.int, data['${effectivePrefix}duration_in_seconds']), id: attachedDatabase.typeMapping @@ -684,6 +740,8 @@ class RemoteAssetEntityData extends i0.DataClass final i2.AssetType type; final DateTime createdAt; final DateTime updatedAt; + final int? width; + final int? height; final int? durationInSeconds; final String id; final String checksum; @@ -698,6 +756,8 @@ class RemoteAssetEntityData extends i0.DataClass required this.type, required this.createdAt, required this.updatedAt, + this.width, + this.height, this.durationInSeconds, required this.id, required this.checksum, @@ -717,6 +777,12 @@ class RemoteAssetEntityData extends i0.DataClass } map['created_at'] = i0.Variable(createdAt); map['updated_at'] = i0.Variable(updatedAt); + if (!nullToAbsent || width != null) { + map['width'] = i0.Variable(width); + } + if (!nullToAbsent || height != null) { + map['height'] = i0.Variable(height); + } if (!nullToAbsent || durationInSeconds != null) { map['duration_in_seconds'] = i0.Variable(durationInSeconds); } @@ -749,6 +815,8 @@ class RemoteAssetEntityData extends i0.DataClass .fromJson(serializer.fromJson(json['type'])), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), + width: serializer.fromJson(json['width']), + height: serializer.fromJson(json['height']), durationInSeconds: serializer.fromJson(json['durationInSeconds']), id: serializer.fromJson(json['id']), checksum: serializer.fromJson(json['checksum']), @@ -770,6 +838,8 @@ class RemoteAssetEntityData extends i0.DataClass .toJson(i1.$RemoteAssetEntityTable.$convertertype.toJson(type)), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), + 'width': serializer.toJson(width), + 'height': serializer.toJson(height), 'durationInSeconds': serializer.toJson(durationInSeconds), 'id': serializer.toJson(id), 'checksum': serializer.toJson(checksum), @@ -788,6 +858,8 @@ class RemoteAssetEntityData extends i0.DataClass i2.AssetType? type, DateTime? createdAt, DateTime? updatedAt, + i0.Value width = const i0.Value.absent(), + i0.Value height = const i0.Value.absent(), i0.Value durationInSeconds = const i0.Value.absent(), String? id, String? checksum, @@ -802,6 +874,8 @@ class RemoteAssetEntityData extends i0.DataClass type: type ?? this.type, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + width: width.present ? width.value : this.width, + height: height.present ? height.value : this.height, durationInSeconds: durationInSeconds.present ? durationInSeconds.value : this.durationInSeconds, @@ -821,6 +895,8 @@ class RemoteAssetEntityData extends i0.DataClass type: data.type.present ? data.type.value : this.type, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + width: data.width.present ? data.width.value : this.width, + height: data.height.present ? data.height.value : this.height, durationInSeconds: data.durationInSeconds.present ? data.durationInSeconds.value : this.durationInSeconds, @@ -846,6 +922,8 @@ class RemoteAssetEntityData extends i0.DataClass ..write('type: $type, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') @@ -865,6 +943,8 @@ class RemoteAssetEntityData extends i0.DataClass type, createdAt, updatedAt, + width, + height, durationInSeconds, id, checksum, @@ -882,6 +962,8 @@ class RemoteAssetEntityData extends i0.DataClass other.type == this.type && other.createdAt == this.createdAt && other.updatedAt == this.updatedAt && + other.width == this.width && + other.height == this.height && other.durationInSeconds == this.durationInSeconds && other.id == this.id && other.checksum == this.checksum && @@ -899,6 +981,8 @@ class RemoteAssetEntityCompanion final i0.Value type; final i0.Value createdAt; final i0.Value updatedAt; + final i0.Value width; + final i0.Value height; final i0.Value durationInSeconds; final i0.Value id; final i0.Value checksum; @@ -913,6 +997,8 @@ class RemoteAssetEntityCompanion this.type = const i0.Value.absent(), this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.height = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), this.id = const i0.Value.absent(), this.checksum = const i0.Value.absent(), @@ -928,6 +1014,8 @@ class RemoteAssetEntityCompanion required i2.AssetType type, this.createdAt = const i0.Value.absent(), this.updatedAt = const i0.Value.absent(), + this.width = const i0.Value.absent(), + this.height = const i0.Value.absent(), this.durationInSeconds = const i0.Value.absent(), required String id, required String checksum, @@ -948,6 +1036,8 @@ class RemoteAssetEntityCompanion i0.Expression? type, i0.Expression? createdAt, i0.Expression? updatedAt, + i0.Expression? width, + i0.Expression? height, i0.Expression? durationInSeconds, i0.Expression? id, i0.Expression? checksum, @@ -963,6 +1053,8 @@ class RemoteAssetEntityCompanion if (type != null) 'type': type, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, + if (width != null) 'width': width, + if (height != null) 'height': height, if (durationInSeconds != null) 'duration_in_seconds': durationInSeconds, if (id != null) 'id': id, if (checksum != null) 'checksum': checksum, @@ -980,6 +1072,8 @@ class RemoteAssetEntityCompanion i0.Value? type, i0.Value? createdAt, i0.Value? updatedAt, + i0.Value? width, + i0.Value? height, i0.Value? durationInSeconds, i0.Value? id, i0.Value? checksum, @@ -994,6 +1088,8 @@ class RemoteAssetEntityCompanion type: type ?? this.type, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + width: width ?? this.width, + height: height ?? this.height, durationInSeconds: durationInSeconds ?? this.durationInSeconds, id: id ?? this.id, checksum: checksum ?? this.checksum, @@ -1022,6 +1118,12 @@ class RemoteAssetEntityCompanion if (updatedAt.present) { map['updated_at'] = i0.Variable(updatedAt.value); } + if (width.present) { + map['width'] = i0.Variable(width.value); + } + if (height.present) { + map['height'] = i0.Variable(height.value); + } if (durationInSeconds.present) { map['duration_in_seconds'] = i0.Variable(durationInSeconds.value); } @@ -1061,6 +1163,8 @@ class RemoteAssetEntityCompanion ..write('type: $type, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('width: $width, ') + ..write('height: $height, ') ..write('durationInSeconds: $durationInSeconds, ') ..write('id: $id, ') ..write('checksum: $checksum, ') diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 4ad60276a2..f414cd9f67 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -3,12 +3,15 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart'; import 'package:immich_mobile/domain/interfaces/db.interface.dart'; +import 'package:immich_mobile/infrastructure/entities/album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; -import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:isar/isar.dart'; @@ -38,8 +41,11 @@ class IsarDatabaseRepository implements IDatabaseRepository { LocalAlbumEntity, LocalAssetEntity, LocalAlbumAssetEntity, - RemoteAssetEntity, RemoteExifEntity, + RemoteAssetEntity, + RemoteAlbumEntity, + RemoteAlbumAssetEntity, + AlbumUserEntity, ], ) class Drift extends $Drift implements IDatabaseRepository { diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index d1bda93653..54c12ecde5 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart @@ -17,6 +17,12 @@ import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift. as i7; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart' as i8; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart' + as i9; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart' + as i10; +import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart' + as i11; abstract class $Drift extends i0.GeneratedDatabase { $Drift(i0.QueryExecutor e) : super(e); @@ -36,6 +42,12 @@ abstract class $Drift extends i0.GeneratedDatabase { i7.$RemoteAssetEntityTable(this); late final i8.$RemoteExifEntityTable remoteExifEntity = i8.$RemoteExifEntityTable(this); + late final i9.$RemoteAlbumEntityTable remoteAlbumEntity = + i9.$RemoteAlbumEntityTable(this); + late final i10.$RemoteAlbumAssetEntityTable remoteAlbumAssetEntity = + i10.$RemoteAlbumAssetEntityTable(this); + late final i11.$AlbumUserEntityTable albumUserEntity = + i11.$AlbumUserEntityTable(this); @override Iterable> get allTables => allSchemaEntities.whereType>(); @@ -49,6 +61,9 @@ abstract class $Drift extends i0.GeneratedDatabase { localAlbumAssetEntity, remoteAssetEntity, remoteExifEntity, + remoteAlbumEntity, + remoteAlbumAssetEntity, + albumUserEntity, i5.idxLocalAssetChecksum, i7.uQRemoteAssetOwnerChecksum ]; @@ -108,6 +123,50 @@ abstract class $Drift extends i0.GeneratedDatabase { i0.TableUpdate('remote_exif_entity', kind: i0.UpdateKind.delete), ], ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_entity', kind: i0.UpdateKind.update), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_asset_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('remote_album_asset_entity', + kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('remote_album_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('album_user_entity', kind: i0.UpdateKind.delete), + ], + ), + i0.WritePropagation( + on: i0.TableUpdateQuery.onTableName('user_entity', + limitUpdateKind: i0.UpdateKind.delete), + result: [ + i0.TableUpdate('album_user_entity', kind: i0.UpdateKind.delete), + ], + ), ], ); @override @@ -134,4 +193,11 @@ class $DriftManager { i7.$$RemoteAssetEntityTableTableManager(_db, _db.remoteAssetEntity); i8.$$RemoteExifEntityTableTableManager get remoteExifEntity => i8.$$RemoteExifEntityTableTableManager(_db, _db.remoteExifEntity); + i9.$$RemoteAlbumEntityTableTableManager get remoteAlbumEntity => + i9.$$RemoteAlbumEntityTableTableManager(_db, _db.remoteAlbumEntity); + i10.$$RemoteAlbumAssetEntityTableTableManager get remoteAlbumAssetEntity => + i10.$$RemoteAlbumAssetEntityTableTableManager( + _db, _db.remoteAlbumAssetEntity); + i11.$$AlbumUserEntityTableTableManager get albumUserEntity => + i11.$$AlbumUserEntityTableTableManager(_db, _db.albumUserEntity); } diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index 5b46fd5d0d..c6a990fc34 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -17,7 +17,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository super(_db); @override - Future> getAll({SortLocalAlbumsBy? sortBy}) { + Future> getAll({Set sortBy = const {}}) { final assetCount = _db.localAlbumAssetEntity.assetId.count(); final query = _db.localAlbumEntity.select().join([ @@ -30,9 +30,23 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository query ..addColumns([assetCount]) ..groupBy([_db.localAlbumEntity.id]); - if (sortBy == SortLocalAlbumsBy.id) { - query.orderBy([OrderingTerm.asc(_db.localAlbumEntity.id)]); + + if (sortBy.isNotEmpty) { + final orderings = []; + for (final sort in sortBy) { + orderings.add( + switch (sort) { + SortLocalAlbumsBy.id => OrderingTerm.asc(_db.localAlbumEntity.id), + SortLocalAlbumsBy.backupSelection => + OrderingTerm.asc(_db.localAlbumEntity.backupSelection), + SortLocalAlbumsBy.isIosSharedAlbum => + OrderingTerm.asc(_db.localAlbumEntity.isIosSharedAlbum), + }, + ); + } + query.orderBy(orderings); } + return query .map( (row) => row @@ -49,7 +63,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository // That is not the case on Android since asset <-> album has one:one mapping final assetsToDelete = _platform.isIOS ? await _getUniqueAssetsInAlbum(albumId) - : await getAssetIdsForAlbum(albumId); + : await getAssetIds(albumId); await _deleteAssets(assetsToDelete); // All the other assets that are still associated will be unlinked automatically on-cascade @@ -59,7 +73,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository }); @override - Future syncAlbumDeletes( + Future syncDeletes( String albumId, Iterable assetIdsToKeep, ) async { @@ -172,7 +186,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository } @override - Future> getAssetsForAlbum(String albumId) { + Future> getAssets(String albumId) { final query = _db.localAlbumAssetEntity.select().join( [ innerJoin( @@ -189,7 +203,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository } @override - Future> getAssetIdsForAlbum(String albumId) { + Future> getAssetIds(String albumId) { final query = _db.localAlbumAssetEntity.selectOnly() ..addColumns([_db.localAlbumAssetEntity.assetId]) ..where(_db.localAlbumAssetEntity.albumId.equals(albumId)); @@ -272,7 +286,9 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository type: asset.type, createdAt: Value(asset.createdAt), updatedAt: Value(asset.updatedAt), - durationInSeconds: Value.absentIfNull(asset.durationInSeconds), + width: Value(asset.width), + height: Value(asset.height), + durationInSeconds: Value(asset.durationInSeconds), id: asset.id, checksum: const Value(null), ); diff --git a/mobile/lib/infrastructure/repositories/store.repository.dart b/mobile/lib/infrastructure/repositories/store.repository.dart index e8769c5084..fec36193bc 100644 --- a/mobile/lib/infrastructure/repositories/store.repository.dart +++ b/mobile/lib/infrastructure/repositories/store.repository.dart @@ -22,7 +22,7 @@ class IsarStoreRepository extends IsarDatabaseRepository } @override - Stream watchAll() { + Stream> watchAll() { return _db.storeValues .filter() .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) @@ -71,10 +71,11 @@ class IsarStoreRepository extends IsarDatabaseRepository .asyncMap((e) async => e == null ? null : await _toValue(key, e)); } - Future _toUpdateEvent(StoreValue entity) async { - final key = StoreKey.values.firstWhere((e) => e.id == entity.id); + Future> _toUpdateEvent(StoreValue entity) async { + final key = StoreKey.values.firstWhere((e) => e.id == entity.id) + as StoreKey; final value = await _toValue(key, entity); - return StoreUpdateEvent(key, value); + return StoreDto(key, value); } Future _toValue(StoreKey key, StoreValue entity) async => @@ -107,4 +108,13 @@ class IsarStoreRepository extends IsarDatabaseRepository }; return StoreValue(key.id, intValue: intValue, strValue: strValue); } + + @override + Future>> getAll() async { + final entities = await _db.storeValues + .filter() + .anyOf(validStoreKeys, (query, id) => query.idEqualTo(id)) + .findAll(); + return Future.wait(entities.map((e) => _toUpdateEvent(e)).toList()); + } } diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index 2349f35df7..70f940c10b 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -50,6 +50,9 @@ class SyncApiRepository implements ISyncApiRepository { SyncRequestType.partnerAssetsV1, SyncRequestType.assetExifsV1, SyncRequestType.partnerAssetExifsV1, + SyncRequestType.albumsV1, + // SyncRequestType.albumAssetsV1, + SyncRequestType.albumUsersV1, ], ).toJson(), ); @@ -65,8 +68,7 @@ class SyncApiRepository implements ISyncApiRepository { } try { - final response = - await client.send(request).timeout(const Duration(seconds: 20)); + final response = await client.send(request); if (response.statusCode != 200) { final errorBody = await response.stream.bytesToString(); @@ -130,8 +132,7 @@ class SyncApiRepository implements ISyncApiRepository { } } -// ignore: avoid-dynamic -const _kResponseMap = { +const _kResponseMap = { SyncEntityType.userV1: SyncUserV1.fromJson, SyncEntityType.userDeleteV1: SyncUserDeleteV1.fromJson, SyncEntityType.partnerV1: SyncPartnerV1.fromJson, @@ -142,4 +143,10 @@ const _kResponseMap = { SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumV1: SyncAlbumV1.fromJson, + SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson, + // SyncEntityType.albumAssetV1: SyncAlbumAssetV1.fromJson, + // SyncEntityType.albumAssetDeleteV1: SyncAlbumAssetDeleteV1.fromJson, + SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson, + SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson, }; diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 7aa8fc6efe..472bc9a6b5 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -3,12 +3,19 @@ import 'package:immich_mobile/domain/interfaces/sync_stream.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/domain/models/album_user.model.dart'; +import 'package:immich_mobile/infrastructure/entities/album_user.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +// import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; -import 'package:openapi/api.dart' hide AssetVisibility; +import 'package:openapi/api.dart' as api + show AssetVisibility, AssetOrder, AlbumUserRole; +import 'package:openapi/api.dart' + hide AssetVisibility, AssetOrder, AlbumUserRole; class DriftSyncStreamRepository extends DriftDatabaseRepository implements ISyncStreamRepository { @@ -161,6 +168,135 @@ class DriftSyncStreamRepository extends DriftDatabaseRepository } } + @override + Future updateAlbumsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumEntityCompanion( + name: Value(album.name), + description: Value(album.description), + ownerId: Value(album.ownerId), + thumbnailAssetId: Value(album.thumbnailAssetId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order.toAssetOrder()), + ); + + batch.insert( + _db.remoteAlbumEntity, + companion.copyWith(id: Value(album.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing updateAlbumsV1', e, s); + rethrow; + } + } + + @override + Future deleteAlbumsV1(Iterable data) async { + try { + _db.batch((batch) { + for (final album in data) { + batch.delete( + _db.remoteAlbumEntity, + RemoteAlbumEntityCompanion(id: Value(album.albumId)), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing deleteAlbumsV1', e, s); + rethrow; + } + } + + // @override + // Future updateAlbumAssetsV1(Iterable data) async { + // try { + // await _db.remoteAlbumAssetEntity.insertAll( + // data.map( + // (albumAsset) => RemoteAlbumAssetEntityCompanion.insert( + // albumId: albumAsset.albumId, + // assetId: albumAsset.assetId, + // ), + // ), + // mode: InsertMode.insertOrIgnore, + // ); + // } catch (e, s) { + // _logger.severe('Error while processing updateAlbumAssetsV1', e, s); + // rethrow; + // } + // } + + // @override + // Future deleteAlbumAssetsV1(Iterable data) async { + // try { + // await _db.batch((batch) { + // for (final albumAsset in data) { + // batch.delete( + // _db.remoteAlbumAssetEntity, + // RemoteAlbumAssetEntityCompanion( + // albumId: Value(albumAsset.albumId), + // assetId: Value(albumAsset.assetId), + // ), + // ); + // } + // }); + // } catch (e, s) { + // _logger.severe('Error while processing deleteAlbumAssetsV1', e, s); + // rethrow; + // } + // } + + @override + Future updateAlbumUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final albumUser in data) { + final companion = AlbumUserEntityCompanion( + role: Value(albumUser.role.toAlbumUserRole()), + ); + + batch.insert( + _db.albumUserEntity, + companion.copyWith( + albumId: Value(albumUser.albumId), + userId: Value(albumUser.userId), + ), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing updateAlbumUsersV1', e, s); + rethrow; + } + } + + @override + Future deleteAlbumUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final albumUser in data) { + batch.delete( + _db.albumUserEntity, + AlbumUserEntityCompanion( + albumId: Value(albumUser.albumId), + userId: Value(albumUser.userId), + ), + ); + } + }); + } catch (e, s) { + _logger.severe('Error while processing deleteAlbumUsersV1', e, s); + rethrow; + } + } + Future _updateAssetsV1(Iterable data) => _db.batch((batch) { for (final asset in data) { @@ -251,3 +387,19 @@ extension on api.AssetVisibility { _ => throw Exception('Unknown AssetVisibility value: $this'), }; } + +extension on api.AssetOrder { + AssetOrder toAssetOrder() => switch (this) { + api.AssetOrder.asc => AssetOrder.asc, + api.AssetOrder.desc => AssetOrder.desc, + _ => throw Exception('Unknown AssetOrder value: $this'), + }; +} + +extension on api.AlbumUserRole { + AlbumUserRole toAlbumUserRole() => switch (this) { + api.AlbumUserRole.editor => AlbumUserRole.editor, + api.AlbumUserRole.viewer => AlbumUserRole.viewer, + _ => throw Exception('Unknown AlbumUserRole value: $this'), + }; +} diff --git a/mobile/lib/infrastructure/utils/asset.mixin.dart b/mobile/lib/infrastructure/utils/asset.mixin.dart index 8649550826..4e14a2919f 100644 --- a/mobile/lib/infrastructure/utils/asset.mixin.dart +++ b/mobile/lib/infrastructure/utils/asset.mixin.dart @@ -6,5 +6,7 @@ mixin AssetEntityMixin on Table { IntColumn get type => intEnum()(); DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + IntColumn get width => integer().nullable()(); + IntColumn get height => integer().nullable()(); IntColumn get durationInSeconds => integer().nullable()(); } diff --git a/mobile/lib/interfaces/asset_api.interface.dart b/mobile/lib/interfaces/asset_api.interface.dart index a17e607d83..71ee993a6b 100644 --- a/mobile/lib/interfaces/asset_api.interface.dart +++ b/mobile/lib/interfaces/asset_api.interface.dart @@ -21,4 +21,6 @@ abstract interface class IAssetApiRepository { List list, AssetVisibilityEnum visibility, ); + + Future getAssetMIMEType(String id); } diff --git a/mobile/lib/interfaces/cast_destination_service.interface.dart b/mobile/lib/interfaces/cast_destination_service.interface.dart new file mode 100644 index 0000000000..add8ad7c51 --- /dev/null +++ b/mobile/lib/interfaces/cast_destination_service.interface.dart @@ -0,0 +1,27 @@ +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/models/cast/cast_manager_state.dart'; + +abstract interface class ICastDestinationService { + Future initialize(); + CastDestinationType getType(); + + void Function(bool)? onConnectionState; + + void Function(Duration)? onCurrentTime; + void Function(Duration)? onDuration; + + void Function(String)? onReceiverName; + void Function(CastState)? onCastState; + + Future connect(dynamic device); + + void loadMedia(Asset asset, bool reload); + + void play(); + void pause(); + void seekTo(Duration position); + void stop(); + Future disconnect(); + + Future> getDevices(); +} diff --git a/mobile/lib/interfaces/sessions_api.interface.dart b/mobile/lib/interfaces/sessions_api.interface.dart new file mode 100644 index 0000000000..4b90b77829 --- /dev/null +++ b/mobile/lib/interfaces/sessions_api.interface.dart @@ -0,0 +1,9 @@ +import 'package:immich_mobile/models/sessions/session_create_response.model.dart'; + +abstract interface class ISessionAPIRepository { + Future createSession( + String deviceName, + String deviceOS, { + int? duration, + }); +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 4695540424..bc9edcd46e 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -205,34 +205,30 @@ class ImmichAppState extends ConsumerState overrides: [ localeProvider.overrideWithValue(context.locale), ], - child: MaterialApp( + child: MaterialApp.router( + title: 'Immich', + debugShowCheckedModeBanner: true, localizationsDelegates: context.localizationDelegates, supportedLocales: context.supportedLocales, locale: context.locale, - debugShowCheckedModeBanner: true, - home: MaterialApp.router( - title: 'Immich', - debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeModeProvider), - darkTheme: getThemeData( - colorScheme: immichTheme.dark, - locale: context.locale, - ), - theme: getThemeData( - colorScheme: immichTheme.light, - locale: context.locale, - ), - routeInformationParser: router.defaultRouteParser(), - routerDelegate: router.delegate( - navigatorObservers: () => [AppNavigationObserver(ref: ref)], - ), + themeMode: ref.watch(immichThemeModeProvider), + darkTheme: getThemeData( + colorScheme: immichTheme.dark, + locale: context.locale, + ), + theme: getThemeData( + colorScheme: immichTheme.light, + locale: context.locale, + ), + routeInformationParser: router.defaultRouteParser(), + routerDelegate: router.delegate( + navigatorObservers: () => [AppNavigationObserver(ref: ref)], ), ), ); } } -// ignore: prefer-single-widget-per-file class MainWidget extends StatelessWidget { const MainWidget({super.key}); diff --git a/mobile/lib/models/backup/available_album.model.dart b/mobile/lib/models/backup/available_album.model.dart index 59c57582ce..c75d6446d8 100644 --- a/mobile/lib/models/backup/available_album.model.dart +++ b/mobile/lib/models/backup/available_album.model.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:immich_mobile/entities/album.entity.dart'; class AvailableAlbum { @@ -16,7 +14,6 @@ class AvailableAlbum { Album? album, int? assetCount, DateTime? lastBackup, - Uint8List? thumbnailData, }) { return AvailableAlbum( album: album ?? this.album, diff --git a/mobile/lib/models/cast/cast_manager_state.dart b/mobile/lib/models/cast/cast_manager_state.dart new file mode 100644 index 0000000000..703ceb4c47 --- /dev/null +++ b/mobile/lib/models/cast/cast_manager_state.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; + +enum CastDestinationType { googleCast } + +enum CastState { idle, playing, paused, buffering } + +class CastManagerState { + final bool isCasting; + final String receiverName; + final CastState castState; + final Duration currentTime; + final Duration duration; + + const CastManagerState({ + required this.isCasting, + required this.receiverName, + required this.castState, + required this.currentTime, + required this.duration, + }); + + CastManagerState copyWith({ + bool? isCasting, + String? receiverName, + CastState? castState, + Duration? currentTime, + Duration? duration, + }) { + return CastManagerState( + isCasting: isCasting ?? this.isCasting, + receiverName: receiverName ?? this.receiverName, + castState: castState ?? this.castState, + currentTime: currentTime ?? this.currentTime, + duration: duration ?? this.duration, + ); + } + + Map toMap() { + final result = {}; + + result.addAll({'isCasting': isCasting}); + result.addAll({'receiverName': receiverName}); + result.addAll({'castState': castState}); + result.addAll({'currentTime': currentTime.inSeconds}); + result.addAll({'duration': duration.inSeconds}); + + return result; + } + + factory CastManagerState.fromMap(Map map) { + return CastManagerState( + isCasting: map['isCasting'] ?? false, + receiverName: map['receiverName'] ?? '', + castState: map['castState'] ?? CastState.idle, + currentTime: Duration(seconds: map['currentTime']?.toInt() ?? 0), + duration: Duration(seconds: map['duration']?.toInt() ?? 0), + ); + } + + String toJson() => json.encode(toMap()); + + factory CastManagerState.fromJson(String source) => + CastManagerState.fromMap(json.decode(source)); + + @override + String toString() => + 'CastManagerState(isCasting: $isCasting, receiverName: $receiverName, castState: $castState, currentTime: $currentTime, duration: $duration)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CastManagerState && + other.isCasting == isCasting && + other.receiverName == receiverName && + other.castState == castState && + other.currentTime == currentTime && + other.duration == duration; + } + + @override + int get hashCode => + isCasting.hashCode ^ + receiverName.hashCode ^ + castState.hashCode ^ + currentTime.hashCode ^ + duration.hashCode; +} diff --git a/mobile/lib/models/map/map_event.model.dart b/mobile/lib/models/map/map_event.model.dart index 0baeefeceb..dd9fec06e6 100644 --- a/mobile/lib/models/map/map_event.model.dart +++ b/mobile/lib/models/map/map_event.model.dart @@ -1,5 +1,3 @@ -// ignore_for_file: add-copy-with - sealed class MapEvent { const MapEvent(); } diff --git a/mobile/lib/models/sessions/session_create_response.model.dart b/mobile/lib/models/sessions/session_create_response.model.dart new file mode 100644 index 0000000000..66b4c6c071 --- /dev/null +++ b/mobile/lib/models/sessions/session_create_response.model.dart @@ -0,0 +1,26 @@ +class SessionCreateResponse { + final String createdAt; + final bool current; + final String deviceOS; + final String deviceType; + final String? expiresAt; + final String id; + final String token; + final String updatedAt; + + const SessionCreateResponse({ + required this.createdAt, + required this.current, + required this.deviceOS, + required this.deviceType, + this.expiresAt, + required this.id, + required this.token, + required this.updatedAt, + }); + + @override + String toString() { + return 'SessionCreateResponse[createdAt=$createdAt, current=$current, deviceOS=$deviceOS, deviceType=$deviceType, expiresAt=$expiresAt, id=$id, token=$token, updatedAt=$updatedAt]'; + } +} diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index bdde338cb3..77b734ce0b 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'dart:ui' as ui; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; @@ -20,6 +21,7 @@ import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; @@ -62,6 +64,7 @@ class GalleryViewerPage extends HookConsumerWidget { final currentIndex = useValueNotifier(initialIndex); final loadAsset = renderList.loadAsset; final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final videoPlayerKeys = useRef>({}); @@ -118,6 +121,36 @@ class GalleryViewerPage extends HookConsumerWidget { const [], ); + useEffect(() { + final asset = loadAsset(currentIndex.value); + + if (asset.isRemote) { + ref.read(castProvider.notifier).loadMedia(asset, false); + } else { + if (isCasting) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + ref.read(castProvider.notifier).stop(); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 1), + content: Text( + "local_asset_cast_failed".tr(), + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), + ); + } + }); + } + } + return null; + }, [ + ref.watch(castProvider).isCasting, + ]); + void showInfo() { final asset = ref.read(currentAssetProvider); if (asset == null) { @@ -203,7 +236,7 @@ class GalleryViewerPage extends HookConsumerWidget { }); }); - PhotoViewGalleryPageOptions buildImage(BuildContext context, Asset asset) { + PhotoViewGalleryPageOptions buildImage(Asset asset) { return PhotoViewGalleryPageOptions( onDragStart: (_, details, __) { localPosition.value = details.localPosition; @@ -279,7 +312,7 @@ class GalleryViewerPage extends HookConsumerWidget { } if (newAsset.isImage && !isPlayingMotionVideo) { - return buildImage(context, newAsset); + return buildImage(newAsset); } return buildVideo(context, newAsset); } @@ -356,6 +389,30 @@ class GalleryViewerPage extends HookConsumerWidget { Timer(const Duration(milliseconds: 400), () { precacheNextImage(next); }); + + context.scaffoldMessenger.hideCurrentSnackBar(); + + // send image to casting if the server has it + if (newAsset.isRemote) { + ref.read(castProvider.notifier).loadMedia(newAsset, false); + } else { + context.scaffoldMessenger.clearSnackBars(); + + if (isCasting) { + ref.read(castProvider.notifier).stop(); + context.scaffoldMessenger.showSnackBar( + SnackBar( + duration: const Duration(seconds: 2), + content: Text( + "local_asset_cast_failed".tr(), + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), + ); + } + } }, builder: buildAsset, ), diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index 957a119f66..8afa6ab4e3 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/asset.service.dart'; @@ -60,6 +61,8 @@ class NativeVideoViewerPage extends HookConsumerWidget { final log = Logger('NativeVideoViewerPage'); + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + Future createSource() async { if (!context.mounted) { return null; @@ -391,7 +394,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { // This remains under the video to avoid flickering // For motion videos, this is the image portion of the asset Center(key: ValueKey(asset.id), child: image), - if (aspectRatio.value != null) + if (aspectRatio.value != null && !isCasting) Visibility.maintain( key: ValueKey(asset), visible: isVisible.value, diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 50126ed1a8..37b5e28797 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/user.model.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/generated/intl_keys.g.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; @@ -41,13 +42,13 @@ class LibraryPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const FavoritesRoute()), icon: Icons.favorite_outline_rounded, - label: 'favorites'.tr(), + label: IntlKeys.favorites.tr(), ), const SizedBox(width: 8), ActionButton( onPressed: () => context.pushRoute(const ArchiveRoute()), icon: Icons.archive_outlined, - label: 'archived'.tr(), + label: IntlKeys.archived.tr(), ), ], ), @@ -58,14 +59,14 @@ class LibraryPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const SharedLinkRoute()), icon: Icons.link_outlined, - label: 'shared_links'.tr(), + label: IntlKeys.shared_links.tr(), ), SizedBox(width: trashEnabled ? 8 : 0), trashEnabled ? ActionButton( onPressed: () => context.pushRoute(const TrashRoute()), icon: Icons.delete_outline_rounded, - label: 'trash'.tr(), + label: IntlKeys.trash.tr(), ) : const SizedBox.shrink(), ], @@ -133,7 +134,7 @@ class QuickAccessButtons extends ConsumerWidget { size: 26, ), title: Text( - 'folders'.tr(), + IntlKeys.folders.tr(), style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, ), @@ -146,7 +147,7 @@ class QuickAccessButtons extends ConsumerWidget { size: 26, ), title: Text( - 'locked_folder'.tr(), + IntlKeys.locked_folder.tr(), style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, ), @@ -159,7 +160,7 @@ class QuickAccessButtons extends ConsumerWidget { size: 26, ), title: Text( - 'partners'.tr(), + IntlKeys.partners.tr(), style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, ), @@ -275,7 +276,7 @@ class PeopleCollectionCard extends ConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'people'.tr(), + IntlKeys.people.tr(), style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -343,7 +344,7 @@ class LocalAlbumsCollectionCard extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'on_this_device'.tr(), + IntlKeys.on_this_device.tr(), style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, @@ -404,7 +405,7 @@ class PlacesCollectionCard extends StatelessWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - 'places'.tr(), + IntlKeys.places.tr(), style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, diff --git a/mobile/lib/pages/library/people/people_collection.page.dart b/mobile/lib/pages/library/people/people_collection.page.dart index 6ec0548546..b98e46aabe 100644 --- a/mobile/lib/pages/library/people/people_collection.page.dart +++ b/mobile/lib/pages/library/people/people_collection.page.dart @@ -27,6 +27,7 @@ class PeopleCollectionPage extends HookConsumerWidget { ) { return showDialog( context: context, + useRootNavigator: false, builder: (BuildContext context) { return PersonNameEditForm(personId: personId, personName: personName); }, @@ -60,80 +61,84 @@ class PeopleCollectionPage extends HookConsumerWidget { ), ], ), - body: people.when( - data: (people) { - if (search.value != null) { - people = people.where((person) { - return person.name - .toLowerCase() - .contains(search.value!.toLowerCase()); - }).toList(); - } - return GridView.builder( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: isTablet ? 6 : 3, - childAspectRatio: 0.85, - mainAxisSpacing: isPortrait && isTablet ? 36 : 0, - ), - padding: const EdgeInsets.symmetric(vertical: 32), - itemCount: people.length, - itemBuilder: (context, index) { - final person = people[index]; + body: SafeArea( + child: people.when( + data: (people) { + if (search.value != null) { + people = people.where((person) { + return person.name + .toLowerCase() + .contains(search.value!.toLowerCase()); + }).toList(); + } + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: isTablet ? 6 : 3, + childAspectRatio: 0.85, + mainAxisSpacing: isPortrait && isTablet ? 36 : 0, + ), + padding: const EdgeInsets.symmetric(vertical: 32), + itemCount: people.length, + itemBuilder: (context, index) { + final person = people[index]; - return Column( - children: [ - GestureDetector( - onTap: () { - context.pushRoute( - PersonResultRoute( - personId: person.id, - personName: person.name, - ), - ); - }, - child: Material( - shape: const CircleBorder(side: BorderSide.none), - elevation: 3, - child: CircleAvatar( - maxRadius: isTablet ? 120 / 2 : 96 / 2, - backgroundImage: NetworkImage( - getFaceThumbnailUrl(person.id), - headers: headers, + return Column( + children: [ + GestureDetector( + onTap: () { + context.pushRoute( + PersonResultRoute( + personId: person.id, + personName: person.name, + ), + ); + }, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + maxRadius: isTablet ? 120 / 2 : 96 / 2, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: headers, + ), ), ), ), - ), - const SizedBox(height: 12), - GestureDetector( - onTap: () => showNameEditModel(person.id, person.name), - child: person.name.isEmpty - ? Text( - 'add_a_name'.tr(), - style: context.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w500, - color: context.colorScheme.primary, - ), - ) - : Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - ), - child: Text( - person.name, - overflow: TextOverflow.ellipsis, + const SizedBox(height: 12), + GestureDetector( + onTap: () => + showNameEditModel(person.id, person.name), + child: person.name.isEmpty + ? Text( + 'add_a_name'.tr(), style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w500, + color: context.colorScheme.primary, + ), + ) + : Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + ), + child: Text( + person.name, + overflow: TextOverflow.ellipsis, + style: + context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), ), ), - ), - ), - ], - ); - }, - ); - }, - error: (error, stack) => const Text("error"), - loading: () => const Center(child: CircularProgressIndicator()), + ), + ], + ); + }, + ); + }, + error: (error, stack) => const Text("error"), + loading: () => const Center(child: CircularProgressIndicator()), + ), ), ); }, diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart index 119589f424..859c7e8a89 100644 --- a/mobile/lib/pages/search/person_result.page.dart +++ b/mobile/lib/pages/search/person_result.page.dart @@ -28,6 +28,7 @@ class PersonResultPage extends HookConsumerWidget { showEditNameDialog() { showDialog( context: context, + useRootNavigator: false, builder: (BuildContext context) { return PersonNameEditForm( personId: personId, diff --git a/mobile/lib/platform/native_sync_api.g.dart b/mobile/lib/platform/native_sync_api.g.dart index ffcef67962..dd6e545f88 100644 --- a/mobile/lib/platform/native_sync_api.g.dart +++ b/mobile/lib/platform/native_sync_api.g.dart @@ -37,6 +37,8 @@ class PlatformAsset { required this.type, this.createdAt, this.updatedAt, + this.width, + this.height, required this.durationInSeconds, }); @@ -50,6 +52,10 @@ class PlatformAsset { int? updatedAt; + int? width; + + int? height; + int durationInSeconds; List _toList() { @@ -59,6 +65,8 @@ class PlatformAsset { type, createdAt, updatedAt, + width, + height, durationInSeconds, ]; } @@ -75,7 +83,9 @@ class PlatformAsset { type: result[2]! as int, createdAt: result[3] as int?, updatedAt: result[4] as int?, - durationInSeconds: result[5]! as int, + width: result[5] as int?, + height: result[6] as int?, + durationInSeconds: result[7]! as int, ); } diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index edbbd23796..08ead96e82 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid-local-functions - import 'dart:async'; import 'package:auto_route/auto_route.dart'; @@ -63,8 +61,10 @@ final _features = [ icon: Icons.delete_sweep_rounded, onTap: (_, ref) async { final db = ref.read(driftProvider); - await db.remoteAssetEntity.deleteAll(); await db.remoteExifEntity.deleteAll(); + await db.remoteAssetEntity.deleteAll(); + await db.remoteAlbumEntity.deleteAll(); + await db.remoteAlbumAssetEntity.deleteAll(); }, ), _Feature( @@ -142,7 +142,6 @@ class _Feature { final Future Function(BuildContext, WidgetRef _) onTap; } -// ignore: prefer-single-widget-per-file class _DevLogs extends StatelessWidget { const _DevLogs(); @@ -170,7 +169,6 @@ class _DevLogs extends StatelessWidget { builder: (_, logMessages) { return ListView.separated( itemBuilder: (ctx, index) { - // ignore: avoid-unsafe-collection-methods final logMessage = logMessages.data![index]; return ListTile( title: Text( diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index c074e524bf..360506e691 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -1,5 +1,3 @@ -// ignore_for_file: prefer-single-widget-per-file - import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -132,6 +130,10 @@ final _remoteStats = [ name: 'Exif Entities', load: (db) => db.managers.remoteExifEntity.count(), ), + _Stat( + name: 'Remote Albums', + load: (db) => db.managers.remoteAlbumEntity.count(), + ), ]; @RoutePage() diff --git a/mobile/lib/providers/api.provider.dart b/mobile/lib/providers/api.provider.dart index a994dacf2f..a54496d94c 100644 --- a/mobile/lib/providers/api.provider.dart +++ b/mobile/lib/providers/api.provider.dart @@ -5,4 +5,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'api.provider.g.dart'; @Riverpod(keepAlive: true) -ApiService apiService(Ref ref) => ApiService(); +ApiService apiService(Ref _) => ApiService(); diff --git a/mobile/lib/providers/api.provider.g.dart b/mobile/lib/providers/api.provider.g.dart index 76ccb4ad6d..8a6f514ce6 100644 --- a/mobile/lib/providers/api.provider.g.dart +++ b/mobile/lib/providers/api.provider.g.dart @@ -6,7 +6,7 @@ part of 'api.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$apiServiceHash() => r'93a7e3b4d3004741abc3061c4688239c3a72f9c4'; +String _$apiServiceHash() => r'187a7de59b064fab1104c23717f18ce0ae3e426c'; /// See also [apiService]. @ProviderFor(apiService) diff --git a/mobile/lib/providers/app_settings.provider.dart b/mobile/lib/providers/app_settings.provider.dart index 81c5c8e201..2d4bdd0eef 100644 --- a/mobile/lib/providers/app_settings.provider.dart +++ b/mobile/lib/providers/app_settings.provider.dart @@ -5,4 +5,4 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'app_settings.provider.g.dart'; @Riverpod(keepAlive: true) -AppSettingsService appSettingsService(Ref ref) => AppSettingsService(); +AppSettingsService appSettingsService(Ref _) => AppSettingsService(); diff --git a/mobile/lib/providers/app_settings.provider.g.dart b/mobile/lib/providers/app_settings.provider.g.dart index 88cab49c1b..66814abd49 100644 --- a/mobile/lib/providers/app_settings.provider.g.dart +++ b/mobile/lib/providers/app_settings.provider.g.dart @@ -7,7 +7,7 @@ part of 'app_settings.provider.dart'; // ************************************************************************** String _$appSettingsServiceHash() => - r'3736e0d384ec7b1f896938589656dd6eb1552d60'; + r'2aa16d76a8df869c39486325efc1d08b2d2c284c'; /// See also [appSettingsService]. @ProviderFor(appSettingsService) diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart index 0edefde526..9bbbfb49aa 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart @@ -39,6 +39,6 @@ final assetStackStateProvider = StateNotifierProvider.autoDispose ); @riverpod -int assetStackIndex(Ref ref, Asset asset) { +int assetStackIndex(Ref _) { return -1; } diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart index 5d4051b285..dcf82cdebd 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.g.dart @@ -6,155 +6,22 @@ part of 'asset_stack.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$assetStackIndexHash() => r'38b4b0116e3e4592620b118ae01cf89b77da9cfe'; - -/// 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)); - } -} +String _$assetStackIndexHash() => r'086ddb782e3eb38b80d755666fe35be8fe7322d7'; /// 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); - } -} +final assetStackIndexProvider = AutoDisposeProvider.internal( + assetStackIndex, + name: r'assetStackIndexProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$assetStackIndexHash, + dependencies: null, + allTransitiveDependencies: null, +); @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -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; -} +typedef AssetStackIndexRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/mobile/lib/providers/asset_viewer/download.provider.dart b/mobile/lib/providers/asset_viewer/download.provider.dart index 7750d6511a..3daed6f686 100644 --- a/mobile/lib/providers/asset_viewer/download.provider.dart +++ b/mobile/lib/providers/asset_viewer/download.provider.dart @@ -144,7 +144,7 @@ class DownloadStateNotifier extends StateNotifier { return await _downloadService.downloadAll(assets); } - void downloadAsset(Asset asset, BuildContext context) async { + void downloadAsset(Asset asset) async { await _downloadService.download(asset); } @@ -186,6 +186,7 @@ class DownloadStateNotifier extends StateNotifier { return const ShareDialog(); }, barrierDismissible: false, + useRootNavigator: false, ); } } diff --git a/mobile/lib/providers/cast.provider.dart b/mobile/lib/providers/cast.provider.dart new file mode 100644 index 0000000000..c80789d2e0 --- /dev/null +++ b/mobile/lib/providers/cast.provider.dart @@ -0,0 +1,93 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart'; +import 'package:immich_mobile/models/cast/cast_manager_state.dart'; +import 'package:immich_mobile/services/gcast.service.dart'; + +final castProvider = StateNotifierProvider( + (ref) => CastNotifier(ref.watch(gCastServiceProvider)), +); + +class CastNotifier extends StateNotifier { + // more cast providers can be added here (ie Fcast) + final ICastDestinationService _gCastService; + + List<(String, CastDestinationType, dynamic)> discovered = List.empty(); + + CastNotifier(this._gCastService) + : super( + const CastManagerState( + isCasting: false, + currentTime: Duration.zero, + duration: Duration.zero, + receiverName: '', + castState: CastState.idle, + ), + ) { + _gCastService.onConnectionState = _onConnectionState; + _gCastService.onCurrentTime = _onCurrentTime; + _gCastService.onDuration = _onDuration; + _gCastService.onReceiverName = _onReceiverName; + _gCastService.onCastState = _onCastState; + } + + void _onConnectionState(bool isCasting) { + state = state.copyWith(isCasting: isCasting); + } + + void _onCurrentTime(Duration currentTime) { + state = state.copyWith(currentTime: currentTime); + } + + void _onDuration(Duration duration) { + state = state.copyWith(duration: duration); + } + + void _onReceiverName(String receiverName) { + state = state.copyWith(receiverName: receiverName); + } + + void _onCastState(CastState castState) { + state = state.copyWith(castState: castState); + } + + void loadMedia(Asset asset, bool reload) { + _gCastService.loadMedia(asset, reload); + } + + Future connect(CastDestinationType type, dynamic device) async { + switch (type) { + case CastDestinationType.googleCast: + await _gCastService.connect(device); + break; + } + } + + Future> getDevices() async { + if (discovered.isEmpty) { + discovered = await _gCastService.getDevices(); + } + + return discovered; + } + + void play() { + _gCastService.play(); + } + + void pause() { + _gCastService.pause(); + } + + void seekTo(Duration position) { + _gCastService.seekTo(position); + } + + void stop() { + _gCastService.stop(); + } + + Future disconnect() async { + await _gCastService.disconnect(); + } +} diff --git a/mobile/lib/providers/immich_logo_provider.dart b/mobile/lib/providers/immich_logo_provider.dart index a52aba5f9e..b24294fc2e 100644 --- a/mobile/lib/providers/immich_logo_provider.dart +++ b/mobile/lib/providers/immich_logo_provider.dart @@ -7,7 +7,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'immich_logo_provider.g.dart'; @riverpod -Future immichLogo(Ref ref) async { +Future immichLogo(Ref _) async { final json = await rootBundle.loadString('assets/immich-logo.json'); final j = jsonDecode(json); return base64Decode(j['content']); diff --git a/mobile/lib/providers/immich_logo_provider.g.dart b/mobile/lib/providers/immich_logo_provider.g.dart index 0889e60fda..90b117d574 100644 --- a/mobile/lib/providers/immich_logo_provider.g.dart +++ b/mobile/lib/providers/immich_logo_provider.g.dart @@ -6,7 +6,7 @@ part of 'immich_logo_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$immichLogoHash() => r'6f23d217c44279537b7edee1ca80ebf47f69a4d0'; +String _$immichLogoHash() => r'6de7fcca1ef9acef6ab7398eb0c664080747e0ea'; /// See also [immichLogo]. @ProviderFor(immichLogo) diff --git a/mobile/lib/repositories/asset_api.repository.dart b/mobile/lib/repositories/asset_api.repository.dart index 45442c2d61..f82df4b774 100644 --- a/mobile/lib/repositories/asset_api.repository.dart +++ b/mobile/lib/repositories/asset_api.repository.dart @@ -72,4 +72,12 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository { return AssetVisibility.archive; } } + + @override + Future getAssetMIMEType(String assetId) async { + final response = await checkNull(_api.getAssetInfo(assetId)); + + // we need to get the MIME of the thumbnail once that gets added to the API + return response.originalMimeType; + } } diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 01d2684faf..69a2fa0244 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -35,8 +35,10 @@ class AuthRepository extends DatabaseRepository implements IAuthRepository { db.albums.clear(), db.eTags.clear(), db.users.clear(), - _drift.remoteAssetEntity.deleteAll(), _drift.remoteExifEntity.deleteAll(), + _drift.remoteAssetEntity.deleteAll(), + _drift.remoteAlbumEntity.deleteAll(), + _drift.remoteAlbumAssetEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/repositories/gcast.repository.dart b/mobile/lib/repositories/gcast.repository.dart new file mode 100644 index 0000000000..11c149ab37 --- /dev/null +++ b/mobile/lib/repositories/gcast.repository.dart @@ -0,0 +1,75 @@ +import 'package:cast/device.dart'; +import 'package:cast/session.dart'; +import 'package:cast/session_manager.dart'; +import 'package:cast/discovery_service.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +final gCastRepositoryProvider = Provider((_) { + return GCastRepository(); +}); + +class GCastRepository { + CastSession? _castSession; + + void Function(CastSessionState)? onCastStatus; + void Function(Map)? onCastMessage; + + Map? _receiverStatus; + + GCastRepository(); + + Future connect(CastDevice device) async { + _castSession = await CastSessionManager().startSession(device); + + _castSession?.stateStream.listen((state) { + onCastStatus?.call(state); + }); + + _castSession?.messageStream.listen((message) { + onCastMessage?.call(message); + if (message['type'] == 'RECEIVER_STATUS') { + _receiverStatus = message; + } + }); + + // open the default receiver + sendMessage(CastSession.kNamespaceReceiver, { + 'type': 'LAUNCH', + 'appId': 'CC1AD845', + }); + } + + Future disconnect() async { + final sessionID = getSessionId(); + + sendMessage(CastSession.kNamespaceReceiver, { + 'type': "STOP", + "sessionId": sessionID, + }); + + // wait 500ms to ensure the stop command is processed + await Future.delayed(const Duration(milliseconds: 500)); + + await _castSession?.close(); + } + + String? getSessionId() { + if (_receiverStatus == null) { + return null; + } + return _receiverStatus!['status']['applications'][0]['sessionId']; + } + + void sendMessage(String namespace, Map message) { + if (_castSession == null) { + throw Exception("Cast session is not established"); + } + + _castSession!.sendMessage(namespace, message); + } + + Future> listDestinations() async { + return await CastDiscoveryService() + .search(timeout: const Duration(seconds: 3)); + } +} diff --git a/mobile/lib/repositories/sessions_api.repository.dart b/mobile/lib/repositories/sessions_api.repository.dart new file mode 100644 index 0000000000..e36331eb79 --- /dev/null +++ b/mobile/lib/repositories/sessions_api.repository.dart @@ -0,0 +1,47 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/interfaces/sessions_api.interface.dart'; +import 'package:immich_mobile/models/sessions/session_create_response.model.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/repositories/api.repository.dart'; +import 'package:openapi/api.dart'; + +final sessionsAPIRepositoryProvider = Provider( + (ref) => SessionsAPIRepository( + ref.watch(apiServiceProvider).sessionsApi, + ), +); + +class SessionsAPIRepository extends ApiRepository + implements ISessionAPIRepository { + final SessionsApi _api; + + SessionsAPIRepository(this._api); + + @override + Future createSession( + String deviceType, + String deviceOS, { + int? duration, + }) async { + final dto = await checkNull( + _api.createSession( + SessionCreateDto( + deviceType: deviceType, + deviceOS: deviceOS, + duration: duration, + ), + ), + ); + + return SessionCreateResponse( + id: dto.id, + current: dto.current, + deviceType: deviceType, + deviceOS: deviceOS, + expiresAt: dto.expiresAt, + createdAt: dto.createdAt, + updatedAt: dto.updatedAt, + token: dto.token, + ); + } +} diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index 24bdccc04d..fe007a2aab 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -34,6 +34,7 @@ class ApiService implements Authentication { late StacksApi stacksApi; late ViewApi viewApi; late MemoriesApi memoriesApi; + late SessionsApi sessionsApi; ApiService() { // The below line ensures that the api clients are initialized when the service is instantiated @@ -72,6 +73,7 @@ class ApiService implements Authentication { stacksApi = StacksApi(_apiClient); viewApi = ViewApi(_apiClient); memoriesApi = MemoriesApi(_apiClient); + sessionsApi = SessionsApi(_apiClient); } Future _setUserAgentHeader() async { diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index 335f71acab..4c4a9b1cff 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -18,7 +18,6 @@ import 'package:immich_mobile/interfaces/backup_album.interface.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; -import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; @@ -489,7 +488,6 @@ class BackgroundService { _cancellationToken!, pmProgressHandler: pmProgressHandler, onSuccess: (result) => _onAssetUploaded( - result: result, shouldNotify: notifyTotalProgress, ), onProgress: (bytes, totalBytes) => @@ -511,7 +509,6 @@ class BackgroundService { } void _onAssetUploaded({ - required SuccessUploadAsset result, bool shouldNotify = false, }) async { if (!shouldNotify) { diff --git a/mobile/lib/services/backup_verification.service.dart b/mobile/lib/services/backup_verification.service.dart index 9aa021a324..408ac51d74 100644 --- a/mobile/lib/services/backup_verification.service.dart +++ b/mobile/lib/services/backup_verification.service.dart @@ -184,10 +184,10 @@ class BackupVerificationService { // for images: make sure they are pixel-wise identical // (skip first few KBs containing metadata) final Uint64List localImage = - _fakeDecodeImg(local, await file.readAsBytes()); + _fakeDecodeImg(await file.readAsBytes()); final res = await apiService.assetsApi .downloadAssetWithHttpInfo(remote.remoteId!); - final Uint64List remoteImage = _fakeDecodeImg(remote, res.bodyBytes); + final Uint64List remoteImage = _fakeDecodeImg(res.bodyBytes); final eq = const ListEquality().equals(remoteImage, localImage); return eq; @@ -198,7 +198,7 @@ class BackupVerificationService { return false; } - static Uint64List _fakeDecodeImg(Asset asset, Uint8List bytes) { + static Uint64List _fakeDecodeImg(Uint8List bytes) { const headerLength = 131072; // assume header is at most 128 KB final start = bytes.length < headerLength * 2 ? (bytes.length ~/ (4 * 8)) * 8 diff --git a/mobile/lib/services/gcast.service.dart b/mobile/lib/services/gcast.service.dart new file mode 100644 index 0000000000..60c94c712c --- /dev/null +++ b/mobile/lib/services/gcast.service.dart @@ -0,0 +1,295 @@ +import 'dart:async'; + +import 'package:cast/session.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/interfaces/cast_destination_service.interface.dart'; +import 'package:immich_mobile/models/cast/cast_manager_state.dart'; +import 'package:immich_mobile/models/sessions/session_create_response.model.dart'; +import 'package:immich_mobile/repositories/asset_api.repository.dart'; +import 'package:immich_mobile/repositories/gcast.repository.dart'; +import 'package:immich_mobile/repositories/sessions_api.repository.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +// ignore: import_rule_openapi, we are only using the AssetMediaSize enum +import 'package:openapi/api.dart'; + +final gCastServiceProvider = Provider( + (ref) => GCastService( + ref.watch(gCastRepositoryProvider), + ref.watch(sessionsAPIRepositoryProvider), + ref.watch(assetApiRepositoryProvider), + ), +); + +class GCastService implements ICastDestinationService { + final GCastRepository _gCastRepository; + final SessionsAPIRepository _sessionsApiService; + final AssetApiRepository _assetApiRepository; + + SessionCreateResponse? sessionKey; + String? currentAssetId; + bool isConnected = false; + int? _sessionId; + Timer? _mediaStatusPollingTimer; + + @override + void Function(bool)? onConnectionState; + @override + void Function(Duration)? onCurrentTime; + @override + void Function(Duration)? onDuration; + @override + void Function(String)? onReceiverName; + @override + void Function(CastState)? onCastState; + + GCastService( + this._gCastRepository, + this._sessionsApiService, + this._assetApiRepository, + ) { + _gCastRepository.onCastStatus = _onCastStatusCallback; + _gCastRepository.onCastMessage = _onCastMessageCallback; + } + + void _onCastStatusCallback(CastSessionState state) { + if (state == CastSessionState.connected) { + onConnectionState?.call(true); + isConnected = true; + } else if (state == CastSessionState.closed) { + onConnectionState?.call(false); + isConnected = false; + onReceiverName?.call(""); + currentAssetId = null; + } + } + + void _onCastMessageCallback(Map message) { + switch (message['type']) { + case "MEDIA_STATUS": + _handleMediaStatus(message); + break; + } + } + + void _handleMediaStatus(Map message) { + final statusList = + (message['status'] as List).whereType>().toList(); + + if (statusList.isEmpty) { + return; + } + + final status = statusList[0]; + switch (status['playerState']) { + case "PLAYING": + onCastState?.call(CastState.playing); + break; + case "PAUSED": + onCastState?.call(CastState.paused); + break; + case "BUFFERING": + onCastState?.call(CastState.buffering); + break; + case "IDLE": + onCastState?.call(CastState.idle); + + // stop polling for media status if the video finished playing + if (status["idleReason"] == "FINISHED") { + _mediaStatusPollingTimer?.cancel(); + } + + break; + } + + if (status["media"] != null && status["media"]["duration"] != null) { + final duration = Duration( + milliseconds: (status["media"]["duration"] * 1000 ?? 0).toInt(), + ); + onDuration?.call(duration); + } + + if (status["mediaSessionId"] != null) { + _sessionId = status["mediaSessionId"]; + } + + if (status["currentTime"] != null) { + final currentTime = + Duration(milliseconds: (status["currentTime"] * 1000 ?? 0).toInt()); + onCurrentTime?.call(currentTime); + } + } + + @override + Future connect(dynamic device) async { + await _gCastRepository.connect(device); + + onReceiverName?.call(device.extras["fn"] ?? "Google Cast"); + } + + @override + CastDestinationType getType() { + return CastDestinationType.googleCast; + } + + @override + Future initialize() async { + // there is nothing blocking us from using Google Cast that we can check for + return true; + } + + @override + Future disconnect() async { + onReceiverName?.call(""); + currentAssetId = null; + await _gCastRepository.disconnect(); + } + + bool isSessionValid() { + // check if we already have a session token + // we should always have a expiration date + if (sessionKey == null || sessionKey?.expiresAt == null) { + return false; + } + + final tokenExpiration = DateTime.parse(sessionKey!.expiresAt!); + + // we want to make sure we have at least 10 seconds remaining in the session + // this is to account for network latency and other delays when sending the request + final bufferedExpiration = + tokenExpiration.subtract(const Duration(seconds: 10)); + + return bufferedExpiration.isAfter(DateTime.now()); + } + + @override + void loadMedia(Asset asset, bool reload) async { + if (!isConnected) { + return; + } else if (asset.remoteId == null) { + return; + } else if (asset.remoteId == currentAssetId && !reload) { + return; + } + + // create a session key + if (!isSessionValid()) { + sessionKey = await _sessionsApiService.createSession( + "Cast", + "Google Cast", + duration: const Duration(minutes: 15).inSeconds, + ); + } + + final unauthenticatedUrl = asset.isVideo + ? getPlaybackUrlForRemoteId( + asset.remoteId!, + ) + : getThumbnailUrlForRemoteId( + asset.remoteId!, + type: AssetMediaSize.fullsize, + ); + + final authenticatedURL = + "$unauthenticatedUrl&sessionKey=${sessionKey?.token}"; + + // get image mime type + final mimeType = + await _assetApiRepository.getAssetMIMEType(asset.remoteId!); + + if (mimeType == null) { + return; + } + + _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { + "type": "LOAD", + "media": { + "contentId": authenticatedURL, + "streamType": "BUFFERED", + "contentType": mimeType, + "contentUrl": authenticatedURL, + }, + "autoplay": true, + }); + + currentAssetId = asset.remoteId; + + // we need to poll for media status since the cast device does not + // send a message when the media is loaded for whatever reason + // only do this on videos + _mediaStatusPollingTimer?.cancel(); + + if (asset.isVideo) { + _mediaStatusPollingTimer = + Timer.periodic(const Duration(milliseconds: 500), (timer) { + if (isConnected) { + _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { + "type": "GET_STATUS", + "mediaSessionId": _sessionId, + }); + } else { + timer.cancel(); + } + }); + } + } + + @override + void play() { + _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { + "type": "PLAY", + "mediaSessionId": _sessionId, + }); + } + + @override + void pause() { + _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { + "type": "PAUSE", + "mediaSessionId": _sessionId, + }); + } + + @override + void seekTo(Duration position) { + _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { + "type": "SEEK", + "mediaSessionId": _sessionId, + "currentTime": position.inSeconds, + }); + } + + @override + void stop() { + _gCastRepository.sendMessage(CastSession.kNamespaceMedia, { + "type": "STOP", + "mediaSessionId": _sessionId, + }); + _mediaStatusPollingTimer?.cancel(); + + currentAssetId = null; + } + + // 0x01 is display capability bitmask + bool isDisplay(int ca) => (ca & 0x01) != 0; + + @override + Future> getDevices() async { + final dests = await _gCastRepository.listDestinations(); + + return dests + .map( + (device) => ( + device.extras["fn"] ?? "Google Cast", + CastDestinationType.googleCast, + device + ), + ) + .where((device) { + final caString = device.$3.extras["ca"]; + final caNumber = int.tryParse(caString ?? "0") ?? 0; + + return isDisplay(caNumber); + }).toList(growable: false); + } +} diff --git a/mobile/lib/services/hash.service.dart b/mobile/lib/services/hash.service.dart index ca2b0ee37e..f8a09471e4 100644 --- a/mobile/lib/services/hash.service.dart +++ b/mobile/lib/services/hash.service.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid-unsafe-collection-methods - import 'dart:convert'; import 'dart:io'; diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index d063b3aa91..50218eaffd 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -73,6 +73,10 @@ String getThumbnailUrlForRemoteId( return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}'; } +String getPlaybackUrlForRemoteId(final String id) { + return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?'; +} + String getFaceThumbnailUrl(final String personId) { return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail'; } diff --git a/mobile/lib/utils/isolate.dart b/mobile/lib/utils/isolate.dart index cfbb1b544f..6b20fa7f37 100644 --- a/mobile/lib/utils/isolate.dart +++ b/mobile/lib/utils/isolate.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/services/log.service.dart'; import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/providers/infrastructure/cancel.provider.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; @@ -58,9 +59,7 @@ Cancelable runInIsolateGentle({ stack, ); } finally { - // Wait for the logs to flush - await Future.delayed(const Duration(seconds: 2)); - // Always close the new db connection on Isolate end + await LogService.I.flushBuffer(); ref.read(driftProvider).close(); ref.read(isarProvider).close(); } diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index a31e441b1f..52c2fc2306 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid-unsafe-collection-methods - import 'dart:async'; import 'dart:convert'; import 'dart:io'; diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index 1ae583bedd..e22076aae9 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -43,6 +43,7 @@ void handleShareAssets( return const ShareDialog(); }, barrierDismissible: false, + useRootNavigator: false, ); } @@ -93,7 +94,7 @@ Future handleFavoriteAssets( ImmichToast.show( context: context, msg: toastMessage, - gravity: ToastGravity.BOTTOM, + gravity: toastGravity, ); } } @@ -163,9 +164,8 @@ Future handleSetAssetsVisibility( WidgetRef ref, BuildContext context, AssetVisibilityEnum visibility, - List selection, { - ToastGravity toastGravity = ToastGravity.BOTTOM, -}) async { + List selection, +) async { if (selection.isNotEmpty) { await ref .watch(assetProvider.notifier) diff --git a/mobile/lib/widgets/asset_grid/asset_drag_region.dart b/mobile/lib/widgets/asset_grid/asset_drag_region.dart index d66220f1ab..6335a1d64d 100644 --- a/mobile/lib/widgets/asset_grid/asset_drag_region.dart +++ b/mobile/lib/widgets/asset_grid/asset_drag_region.dart @@ -1,4 +1,3 @@ -// ignore_for_file: library_private_types_in_public_api // Based on https://stackoverflow.com/a/52625182 import 'dart:async'; @@ -164,7 +163,6 @@ class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer { } } -// ignore: prefer-single-widget-per-file class AssetIndexWrapper extends SingleChildRenderObjectWidget { final int rowIndex; final int sectionIndex; @@ -177,6 +175,7 @@ class AssetIndexWrapper extends SingleChildRenderObjectWidget { }); @override + // ignore: library_private_types_in_public_api _AssetIndexProxy createRenderObject(BuildContext context) { return _AssetIndexProxy( index: AssetIndex(rowIndex: rowIndex, sectionIndex: sectionIndex), @@ -186,6 +185,7 @@ class AssetIndexWrapper extends SingleChildRenderObjectWidget { @override void updateRenderObject( BuildContext context, + // ignore: library_private_types_in_public_api _AssetIndexProxy renderObject, ) { renderObject.index = diff --git a/mobile/lib/widgets/asset_grid/delete_dialog.dart b/mobile/lib/widgets/asset_grid/delete_dialog.dart index 601f7a5e46..ecfb4130dc 100644 --- a/mobile/lib/widgets/asset_grid/delete_dialog.dart +++ b/mobile/lib/widgets/asset_grid/delete_dialog.dart @@ -1,5 +1,3 @@ -// ignore_for_file: prefer-single-widget-per-file - import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart index 8bb45eeb7d..6eddf58adb 100644 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart +++ b/mobile/lib/widgets/asset_grid/draggable_scrollbar.dart @@ -80,8 +80,7 @@ class DraggableScrollbar extends StatefulWidget { this.labelTextBuilder, this.labelConstraints, }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = - _thumbRRectBuilder(scrollThumbKey, alwaysVisibleScrollThumb); + scrollThumbBuilder = _thumbRRectBuilder(alwaysVisibleScrollThumb); DraggableScrollbar.arrows({ super.key, @@ -97,8 +96,7 @@ class DraggableScrollbar extends StatefulWidget { this.labelTextBuilder, this.labelConstraints, }) : assert(child.scrollDirection == Axis.vertical), - scrollThumbBuilder = - _thumbArrowBuilder(scrollThumbKey, alwaysVisibleScrollThumb); + scrollThumbBuilder = _thumbArrowBuilder(alwaysVisibleScrollThumb); DraggableScrollbar.semicircle({ super.key, @@ -201,7 +199,6 @@ class DraggableScrollbar extends StatefulWidget { } static ScrollThumbBuilder _thumbArrowBuilder( - Key? scrollThumbKey, bool alwaysVisibleScrollThumb, ) { return ( @@ -239,7 +236,6 @@ class DraggableScrollbar extends StatefulWidget { } static ScrollThumbBuilder _thumbRRectBuilder( - Key? scrollThumbKey, bool alwaysVisibleScrollThumb, ) { return ( diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index d25b7a3e90..25f65b448c 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -48,7 +48,7 @@ class ThumbnailImage extends ConsumerWidget { // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id final isFromDto = asset.id == noDbId; - Widget buildSelectionIcon(Asset asset) { + Widget buildSelectionIcon() { if (isSelected) { return Container( decoration: BoxDecoration( @@ -233,7 +233,7 @@ class ThumbnailImage extends ConsumerWidget { padding: const EdgeInsets.all(3.0), child: Align( alignment: Alignment.topLeft, - child: buildSelectionIcon(asset), + child: buildSelectionIcon(), ), ), ], diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 1ff8596c43..59d97bf0c7 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -235,7 +235,6 @@ class BottomGalleryBar extends ConsumerWidget { ref.read(downloadStateProvider.notifier).downloadAsset( asset, - context, ); } diff --git a/mobile/lib/widgets/asset_viewer/cast_dialog.dart b/mobile/lib/widgets/asset_viewer/cast_dialog.dart new file mode 100644 index 0000000000..9043ea4bea --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/cast_dialog.dart @@ -0,0 +1,160 @@ +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/models/cast/cast_manager_state.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; + +class CastDialog extends ConsumerWidget { + const CastDialog({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final castManager = ref.watch(castProvider); + + bool isCurrentDevice(String deviceName) { + return castManager.receiverName == deviceName && castManager.isCasting; + } + + bool isDeviceConnecting(String deviceName) { + return castManager.receiverName == deviceName && !castManager.isCasting; + } + + return AlertDialog( + title: const Text( + "cast", + style: TextStyle(fontWeight: FontWeight.bold), + ).tr(), + content: SizedBox( + width: 250, + height: 250, + child: FutureBuilder>( + future: ref.read(castProvider.notifier).getDevices(), + builder: (context, snapshot) { + if (snapshot.hasError) { + return Text( + 'Error: ${snapshot.error.toString()}', + ); + } else if (!snapshot.hasData) { + return const SizedBox( + height: 48, + child: Center(child: CircularProgressIndicator()), + ); + } + + if (snapshot.data!.isEmpty) { + return const Text( + 'no_cast_devices_found', + ).tr(); + } + + final devices = snapshot.data!; + final connected = + devices.where((d) => isCurrentDevice(d.$1)).toList(); + final others = + devices.where((d) => !isCurrentDevice(d.$1)).toList(); + + final List sectionedList = []; + + if (connected.isNotEmpty) { + sectionedList.add("connected_device"); + sectionedList.addAll(connected); + } + + if (others.isNotEmpty) { + sectionedList.add("discovered_devices"); + sectionedList.addAll(others); + } + + return ListView.builder( + shrinkWrap: true, + itemCount: sectionedList.length, + itemBuilder: (context, index) { + final item = sectionedList[index]; + + if (item is String) { + // It's a section header + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text( + item, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ).tr(), + ); + } else { + final (deviceName, type, deviceObj) = + item as (String, CastDestinationType, dynamic); + + return ListTile( + title: Text( + deviceName, + style: TextStyle( + color: isCurrentDevice(deviceName) + ? context.colorScheme.primary + : null, + ), + ), + leading: Icon( + type == CastDestinationType.googleCast + ? Icons.cast + : Icons.cast_connected, + color: isCurrentDevice(deviceName) + ? context.colorScheme.primary + : null, + ), + trailing: isCurrentDevice(deviceName) + ? Icon(Icons.check, color: context.colorScheme.primary) + : isDeviceConnecting(deviceName) + ? const CircularProgressIndicator() + : null, + onTap: () async { + if (isDeviceConnecting(deviceName)) { + return; + } + + if (castManager.isCasting) { + await ref.read(castProvider.notifier).disconnect(); + } + + if (!isCurrentDevice(deviceName)) { + ref + .read(castProvider.notifier) + .connect(type, deviceObj); + } + }, + ); + } + }, + ); + }, + ), + ), + actions: [ + if (castManager.isCasting) + TextButton( + onPressed: () => ref.read(castProvider.notifier).disconnect(), + child: Text( + "stop_casting", + style: TextStyle( + color: context.colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + TextButton( + onPressed: () => context.pop(), + child: Text( + "close", + style: TextStyle( + color: context.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ).tr(), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart index d759b0d80b..d64e507170 100644 --- a/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart +++ b/mobile/lib/widgets/asset_viewer/custom_video_player_controls.dart @@ -1,9 +1,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/cast/cast_manager_state.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/utils/hooks/timer_hook.dart'; import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart'; import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart'; @@ -25,6 +27,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget { final VideoPlaybackState state = ref.watch(videoPlaybackValueProvider.select((value) => value.state)); + final cast = ref.watch(castProvider); + // A timer to hide the controls final hideTimer = useTimer( hideTimerDuration, @@ -42,7 +46,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget { } }, ); - final showBuffering = state == VideoPlaybackState.buffering; + final showBuffering = + state == VideoPlaybackState.buffering && !cast.isCasting; /// Shows the controls and starts the timer to hide them void showControlsAndStartHideTimer() { @@ -59,6 +64,23 @@ class CustomVideoPlayerControls extends HookConsumerWidget { /// Toggles between playing and pausing depending on the state of the video void togglePlay() { showControlsAndStartHideTimer(); + + if (cast.isCasting) { + if (cast.castState == CastState.playing) { + ref.read(castProvider.notifier).pause(); + } else if (cast.castState == CastState.paused) { + ref.read(castProvider.notifier).play(); + } else if (cast.castState == CastState.idle) { + // resend the play command since its finished + final asset = ref.read(currentAssetProvider); + if (asset == null) { + return; + } + ref.read(castProvider.notifier).loadMedia(asset, true); + } + return; + } + if (state == VideoPlaybackState.playing) { ref.read(videoPlayerControlsProvider.notifier).pause(); } else if (state == VideoPlaybackState.completed) { @@ -89,7 +111,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget { backgroundColor: Colors.black54, iconColor: Colors.white, isFinished: state == VideoPlaybackState.completed, - isPlaying: state == VideoPlaybackState.playing, + isPlaying: state == VideoPlaybackState.playing || + (cast.isCasting && cast.castState == CastState.playing), show: assetIsVideo && showControls, onPressed: togglePlay, ), diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart index 8d2a38c700..cbb003bd72 100644 --- a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart @@ -31,6 +31,7 @@ class PeopleInfo extends ConsumerWidget { ) { return showDialog( context: context, + useRootNavigator: false, builder: (BuildContext context) { return PersonNameEditForm(personId: personId, personName: personName); }, diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 4354dceb4f..4ef55f4f76 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -96,7 +96,7 @@ class GalleryAppBar extends ConsumerWidget { } handleDownloadAsset() { - ref.read(downloadStateProvider.notifier).downloadAsset(asset, context); + ref.read(downloadStateProvider.notifier).downloadAsset(asset); } handleLocateAsset() async { diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 64cb1c619f..a868aff617 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -1,12 +1,16 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/activity_statistics.provider.dart'; import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/tab.provider.dart'; +import 'package:immich_mobile/providers/websocket.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; @@ -44,6 +48,10 @@ class TopControlAppBar extends HookConsumerWidget { const double iconSize = 22.0; final a = ref.watch(assetWatcher(asset)).value ?? asset; final album = ref.watch(currentAlbumProvider); + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); + final websocketConnected = + ref.watch(websocketProvider.select((c) => c.isConnected)); + final comments = album != null && album.remoteId != null && asset.remoteId != null @@ -169,6 +177,22 @@ class TopControlAppBar extends HookConsumerWidget { ); } + Widget buildCastButton() { + return IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const CastDialog(), + ); + }, + icon: Icon( + isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + size: 20.0, + color: isCasting ? context.primaryColor : Colors.grey[200], + ), + ); + } + bool isInHomePage = ref.read(tabProvider.notifier).state == TabEnum.home; bool? isInTrash = ref.read(currentAssetProvider)?.isTrashed; @@ -193,6 +217,8 @@ class TopControlAppBar extends HookConsumerWidget { !asset.isTrashed && !isInLockedView) buildAddToAlbumButton(), + if (isCasting || (asset.isRemote && websocketConnected)) + buildCastButton(), if (asset.isTrashed) buildRestoreButton(), if (album != null && album.shared && !isInLockedView) buildActivitiesButton(), diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index 4d0e7aa17f..0e90669fe3 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/formatted_duration.dart'; class VideoPosition extends HookConsumerWidget { @@ -13,9 +14,16 @@ class VideoPosition extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final (position, duration) = ref.watch( - videoPlaybackValueProvider.select((v) => (v.position, v.duration)), - ); + final isCasting = ref.watch(castProvider).isCasting; + + final (position, duration) = isCasting + ? ref.watch( + castProvider.select((c) => (c.currentTime, c.duration)), + ) + : ref.watch( + videoPlaybackValueProvider.select((v) => (v.position, v.duration)), + ); + final wasPlaying = useRef(true); return duration == Duration.zero ? const _VideoPositionPlaceholder() @@ -57,15 +65,22 @@ class VideoPosition extends HookConsumerWidget { } }, onChanged: (value) { - final inSeconds = - (duration * (value / 100.0)).inSeconds; - final position = inSeconds.toDouble(); + final seekToDuration = (duration * (value / 100.0)); + + if (isCasting) { + ref + .read(castProvider.notifier) + .seekTo(seekToDuration); + return; + } + ref .read(videoPlayerControlsProvider.notifier) - .position = position; + .position = seekToDuration.inSeconds.toDouble(); + // This immediately updates the slider position without waiting for the video to update ref.read(videoPlaybackValueProvider.notifier).position = - Duration(seconds: inSeconds); + seekToDuration; }, ), ), diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index a11f47c9b6..6174fad404 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -10,9 +10,11 @@ import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/infrastructure/store.provider.dart'; +import 'package:immich_mobile/providers/cast.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_dialog.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; @@ -34,6 +36,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { final user = ref.watch(currentUserProvider); final isDarkTheme = context.isDarkTheme; const widgetSize = 30.0; + final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); buildProfileIndicator() { return InkWell( @@ -191,6 +194,21 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { icon: const Icon(Icons.science_rounded), onPressed: () => context.pushRoute(const FeatInDevRoute()), ), + if (isCasting) + Padding( + padding: const EdgeInsets.only(right: 12), + child: IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const CastDialog(), + ); + }, + icon: Icon( + isCasting ? Icons.cast_connected_rounded : Icons.cast_rounded, + ), + ), + ), if (showUploadButton) Padding( padding: const EdgeInsets.only(right: 20), diff --git a/mobile/lib/widgets/common/thumbhash_placeholder.dart b/mobile/lib/widgets/common/thumbhash_placeholder.dart index 1f303c67c8..aa320f4230 100644 --- a/mobile/lib/widgets/common/thumbhash_placeholder.dart +++ b/mobile/lib/widgets/common/thumbhash_placeholder.dart @@ -13,7 +13,8 @@ OctoSet blurHashOrPlaceholder( }) { return OctoSet( placeholderBuilder: blurHashPlaceholderBuilder(blurhash, fit: fit), - errorBuilder: blurHashErrorBuilder(blurhash, fit: fit), + errorBuilder: + blurHashErrorBuilder(blurhash, fit: fit, message: errorMessage), ); } diff --git a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart index b12b9a7634..768e5d9cc7 100644 --- a/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart +++ b/mobile/lib/widgets/photo_view/src/core/photo_view_hit_corners.dart @@ -28,7 +28,6 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { bool _shouldMoveAxis( HitCorners hitCorners, double mainAxisMove, - double crossAxisMove, ) { if (mainAxisMove == 0) { return false; @@ -47,17 +46,15 @@ mixin HitCornersDetector on PhotoViewControllerDelegate { bool _shouldMoveX(Offset move) { final hitCornersX = _hitCornersX(); final mainAxisMove = move.dx; - final crossAxisMove = move.dy; - return _shouldMoveAxis(hitCornersX, mainAxisMove, crossAxisMove); + return _shouldMoveAxis(hitCornersX, mainAxisMove); } bool _shouldMoveY(Offset move) { final hitCornersY = _hitCornersY(); final mainAxisMove = move.dy; - final crossAxisMove = move.dx; - return _shouldMoveAxis(hitCornersY, mainAxisMove, crossAxisMove); + return _shouldMoveAxis(hitCornersY, mainAxisMove); } bool shouldMove(Offset move, Axis mainAxis) { diff --git a/mobile/lib/widgets/search/person_name_edit_form.dart b/mobile/lib/widgets/search/person_name_edit_form.dart index ebcfc1cd8d..76d6e80832 100644 --- a/mobile/lib/widgets/search/person_name_edit_form.dart +++ b/mobile/lib/widgets/search/person_name_edit_form.dart @@ -35,6 +35,7 @@ class PersonNameEditForm extends HookConsumerWidget { content: SingleChildScrollView( child: TextFormField( controller: controller, + textCapitalization: TextCapitalization.words, autofocus: true, decoration: InputDecoration( hintText: 'name'.tr(), diff --git a/mobile/lib/widgets/settings/advanced_settings.dart b/mobile/lib/widgets/settings/advanced_settings.dart index eb13c67640..bd501ffcf7 100644 --- a/mobile/lib/widgets/settings/advanced_settings.dart +++ b/mobile/lib/widgets/settings/advanced_settings.dart @@ -41,7 +41,7 @@ class AdvancedSettings extends HookConsumerWidget { useValueChanged( levelId.value, (_, __) => - LogService.I.setlogLevel(Level.LEVELS[levelId.value].toLogLevel()), + LogService.I.setLogLevel(Level.LEVELS[levelId.value].toLogLevel()), ); Future checkAndroidVersion() async { diff --git a/mobile/makefile b/mobile/makefile index ec0d08f087..64992ec946 100644 --- a/mobile/makefile +++ b/mobile/makefile @@ -26,4 +26,6 @@ migration: translation: dart run easy_localization:generate -S ../i18n - dart format lib/generated/codegen_loader.g.dart \ No newline at end of file + dart run bin/generate_keys.dart + dart format lib/generated/codegen_loader.g.dart + dart format lib/generated/intl_keys.g.dart \ No newline at end of file diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 74597b43bc..ee646354d1 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -177,6 +177,7 @@ Class | Method | HTTP request | Description *SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | +*SearchApi* | [**searchAssetStatistics**](doc//SearchApi.md#searchassetstatistics) | **POST** /search/statistics | *SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | @@ -425,6 +426,7 @@ Class | Method | HTTP request | Description - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md) - [SearchResponseDto](doc//SearchResponseDto.md) + - [SearchStatisticsResponseDto](doc//SearchStatisticsResponseDto.md) - [SearchSuggestionType](doc//SearchSuggestionType.md) - [ServerAboutResponseDto](doc//ServerAboutResponseDto.md) - [ServerApkLinksDto](doc//ServerApkLinksDto.md) @@ -453,6 +455,7 @@ Class | Method | HTTP request | Description - [StackCreateDto](doc//StackCreateDto.md) - [StackResponseDto](doc//StackResponseDto.md) - [StackUpdateDto](doc//StackUpdateDto.md) + - [StatisticsSearchDto](doc//StatisticsSearchDto.md) - [SyncAckDeleteDto](doc//SyncAckDeleteDto.md) - [SyncAckDto](doc//SyncAckDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 7b49661844..9bf4026320 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -214,6 +214,7 @@ part 'model/search_explore_response_dto.dart'; part 'model/search_facet_count_response_dto.dart'; part 'model/search_facet_response_dto.dart'; part 'model/search_response_dto.dart'; +part 'model/search_statistics_response_dto.dart'; part 'model/search_suggestion_type.dart'; part 'model/server_about_response_dto.dart'; part 'model/server_apk_links_dto.dart'; @@ -242,6 +243,7 @@ part 'model/source_type.dart'; part 'model/stack_create_dto.dart'; part 'model/stack_response_dto.dart'; part 'model/stack_update_dto.dart'; +part 'model/statistics_search_dto.dart'; part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_dto.dart'; part 'model/sync_ack_set_dto.dart'; diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 632107ff79..5c7a8de59d 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -193,6 +193,53 @@ class SearchApi { return null; } + /// Performs an HTTP 'POST /search/statistics' operation and returns the [Response]. + /// Parameters: + /// + /// * [StatisticsSearchDto] statisticsSearchDto (required): + Future searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto,) async { + // ignore: prefer_const_declarations + final apiPath = r'/search/statistics'; + + // ignore: prefer_final_locals + Object? postBody = statisticsSearchDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + apiPath, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [StatisticsSearchDto] statisticsSearchDto (required): + Future searchAssetStatistics(StatisticsSearchDto statisticsSearchDto,) async { + final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchStatisticsResponseDto',) as SearchStatisticsResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /search/metadata' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index a96b895655..bc7f48255c 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -484,6 +484,8 @@ class ApiClient { return SearchFacetResponseDto.fromJson(value); case 'SearchResponseDto': return SearchResponseDto.fromJson(value); + case 'SearchStatisticsResponseDto': + return SearchStatisticsResponseDto.fromJson(value); case 'SearchSuggestionType': return SearchSuggestionTypeTypeTransformer().decode(value); case 'ServerAboutResponseDto': @@ -540,6 +542,8 @@ class ApiClient { return StackResponseDto.fromJson(value); case 'StackUpdateDto': return StackUpdateDto.fromJson(value); + case 'StatisticsSearchDto': + return StatisticsSearchDto.fromJson(value); case 'SyncAckDeleteDto': return SyncAckDeleteDto.fromJson(value); case 'SyncAckDto': diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 7f1184467b..520777a45d 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class MetadataSearchDto { /// Returns a new [MetadataSearchDto] instance. MetadataSearchDto({ + this.albumIds = const [], this.checksum, this.city, this.country, @@ -57,6 +58,8 @@ class MetadataSearchDto { this.withStacked, }); + List albumIds; + /// /// 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 @@ -346,6 +349,7 @@ class MetadataSearchDto { @override bool operator ==(Object other) => identical(this, other) || other is MetadataSearchDto && + _deepEquality.equals(other.albumIds, albumIds) && other.checksum == checksum && other.city == city && other.country == country && @@ -392,6 +396,7 @@ class MetadataSearchDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (albumIds.hashCode) + (checksum == null ? 0 : checksum!.hashCode) + (city == null ? 0 : city!.hashCode) + (country == null ? 0 : country!.hashCode) + @@ -436,10 +441,11 @@ class MetadataSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'MetadataSearchDto[checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceAssetId=$deviceAssetId, deviceId=$deviceId, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; + json[r'albumIds'] = this.albumIds; if (this.checksum != null) { json[r'checksum'] = this.checksum; } else { @@ -650,6 +656,9 @@ class MetadataSearchDto { final json = value.cast(); return MetadataSearchDto( + albumIds: json[r'albumIds'] is Iterable + ? (json[r'albumIds'] as Iterable).cast().toList(growable: false) + : const [], checksum: mapValueOfType(json, r'checksum'), city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index 0284212efc..c5914f9fa3 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class RandomSearchDto { /// Returns a new [RandomSearchDto] instance. RandomSearchDto({ + this.albumIds = const [], this.city, this.country, this.createdAfter, @@ -46,6 +47,8 @@ class RandomSearchDto { this.withStacked, }); + List albumIds; + String? city; String? country; @@ -252,6 +255,7 @@ class RandomSearchDto { @override bool operator ==(Object other) => identical(this, other) || other is RandomSearchDto && + _deepEquality.equals(other.albumIds, albumIds) && other.city == city && other.country == country && other.createdAfter == createdAfter && @@ -287,6 +291,7 @@ class RandomSearchDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (albumIds.hashCode) + (city == null ? 0 : city!.hashCode) + (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + @@ -320,10 +325,11 @@ class RandomSearchDto { (withStacked == null ? 0 : withStacked!.hashCode); @override - String toString() => 'RandomSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; + String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]'; Map toJson() { final json = {}; + json[r'albumIds'] = this.albumIds; if (this.city != null) { json[r'city'] = this.city; } else { @@ -483,6 +489,9 @@ class RandomSearchDto { final json = value.cast(); return RandomSearchDto( + albumIds: json[r'albumIds'] is Iterable + ? (json[r'albumIds'] as Iterable).cast().toList(growable: false) + : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), createdAfter: mapDateTime(json, r'createdAfter', r''), diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart new file mode 100644 index 0000000000..84f31373d8 --- /dev/null +++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart @@ -0,0 +1,99 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class SearchStatisticsResponseDto { + /// Returns a new [SearchStatisticsResponseDto] instance. + SearchStatisticsResponseDto({ + required this.total, + }); + + int total; + + @override + bool operator ==(Object other) => identical(this, other) || other is SearchStatisticsResponseDto && + other.total == total; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (total.hashCode); + + @override + String toString() => 'SearchStatisticsResponseDto[total=$total]'; + + Map toJson() { + final json = {}; + json[r'total'] = this.total; + return json; + } + + /// Returns a new [SearchStatisticsResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SearchStatisticsResponseDto? fromJson(dynamic value) { + upgradeDto(value, "SearchStatisticsResponseDto"); + if (value is Map) { + final json = value.cast(); + + return SearchStatisticsResponseDto( + total: mapValueOfType(json, r'total')!, + ); + } + 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 = SearchStatisticsResponseDto.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 = SearchStatisticsResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SearchStatisticsResponseDto-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] = SearchStatisticsResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'total', + }; +} + diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index a915d97b31..c221340553 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -13,6 +13,7 @@ part of openapi.api; class SmartSearchDto { /// Returns a new [SmartSearchDto] instance. SmartSearchDto({ + this.albumIds = const [], this.city, this.country, this.createdAfter, @@ -47,6 +48,8 @@ class SmartSearchDto { this.withExif, }); + List albumIds; + String? city; String? country; @@ -256,6 +259,7 @@ class SmartSearchDto { @override bool operator ==(Object other) => identical(this, other) || other is SmartSearchDto && + _deepEquality.equals(other.albumIds, albumIds) && other.city == city && other.country == country && other.createdAfter == createdAfter && @@ -292,6 +296,7 @@ class SmartSearchDto { @override int get hashCode => // ignore: unnecessary_parenthesis + (albumIds.hashCode) + (city == null ? 0 : city!.hashCode) + (country == null ? 0 : country!.hashCode) + (createdAfter == null ? 0 : createdAfter!.hashCode) + @@ -326,10 +331,11 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, personIds=$personIds, query=$query, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; + json[r'albumIds'] = this.albumIds; if (this.city != null) { json[r'city'] = this.city; } else { @@ -490,6 +496,9 @@ class SmartSearchDto { final json = value.cast(); return SmartSearchDto( + albumIds: json[r'albumIds'] is Iterable + ? (json[r'albumIds'] as Iterable).cast().toList(growable: false) + : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), createdAfter: mapDateTime(json, r'createdAfter', r''), diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart new file mode 100644 index 0000000000..55de23ba32 --- /dev/null +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -0,0 +1,509 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StatisticsSearchDto { + /// Returns a new [StatisticsSearchDto] instance. + StatisticsSearchDto({ + this.albumIds = const [], + this.city, + this.country, + this.createdAfter, + this.createdBefore, + this.description, + this.deviceId, + this.isEncoded, + this.isFavorite, + this.isMotion, + this.isNotInAlbum, + this.isOffline, + this.lensModel, + this.libraryId, + this.make, + this.model, + this.personIds = const [], + this.rating, + this.state, + this.tagIds = const [], + this.takenAfter, + this.takenBefore, + this.trashedAfter, + this.trashedBefore, + this.type, + this.updatedAfter, + this.updatedBefore, + this.visibility, + }); + + List albumIds; + + String? city; + + String? country; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? createdAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? createdBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? deviceId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isEncoded; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isFavorite; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isMotion; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isNotInAlbum; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isOffline; + + String? lensModel; + + String? libraryId; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? make; + + String? model; + + List personIds; + + /// Minimum value: -1 + /// Maximum value: 5 + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + num? rating; + + String? state; + + List tagIds; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? takenAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? takenBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? trashedAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? trashedBefore; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetTypeEnum? type; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? updatedAfter; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + DateTime? updatedBefore; + + /// + /// 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. + /// + AssetVisibility? visibility; + + @override + bool operator ==(Object other) => identical(this, other) || other is StatisticsSearchDto && + _deepEquality.equals(other.albumIds, albumIds) && + other.city == city && + other.country == country && + other.createdAfter == createdAfter && + other.createdBefore == createdBefore && + other.description == description && + other.deviceId == deviceId && + other.isEncoded == isEncoded && + other.isFavorite == isFavorite && + other.isMotion == isMotion && + other.isNotInAlbum == isNotInAlbum && + other.isOffline == isOffline && + other.lensModel == lensModel && + other.libraryId == libraryId && + other.make == make && + other.model == model && + _deepEquality.equals(other.personIds, personIds) && + other.rating == rating && + other.state == state && + _deepEquality.equals(other.tagIds, tagIds) && + other.takenAfter == takenAfter && + other.takenBefore == takenBefore && + other.trashedAfter == trashedAfter && + other.trashedBefore == trashedBefore && + other.type == type && + other.updatedAfter == updatedAfter && + other.updatedBefore == updatedBefore && + other.visibility == visibility; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (albumIds.hashCode) + + (city == null ? 0 : city!.hashCode) + + (country == null ? 0 : country!.hashCode) + + (createdAfter == null ? 0 : createdAfter!.hashCode) + + (createdBefore == null ? 0 : createdBefore!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (deviceId == null ? 0 : deviceId!.hashCode) + + (isEncoded == null ? 0 : isEncoded!.hashCode) + + (isFavorite == null ? 0 : isFavorite!.hashCode) + + (isMotion == null ? 0 : isMotion!.hashCode) + + (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + + (isOffline == null ? 0 : isOffline!.hashCode) + + (lensModel == null ? 0 : lensModel!.hashCode) + + (libraryId == null ? 0 : libraryId!.hashCode) + + (make == null ? 0 : make!.hashCode) + + (model == null ? 0 : model!.hashCode) + + (personIds.hashCode) + + (rating == null ? 0 : rating!.hashCode) + + (state == null ? 0 : state!.hashCode) + + (tagIds.hashCode) + + (takenAfter == null ? 0 : takenAfter!.hashCode) + + (takenBefore == null ? 0 : takenBefore!.hashCode) + + (trashedAfter == null ? 0 : trashedAfter!.hashCode) + + (trashedBefore == null ? 0 : trashedBefore!.hashCode) + + (type == null ? 0 : type!.hashCode) + + (updatedAfter == null ? 0 : updatedAfter!.hashCode) + + (updatedBefore == null ? 0 : updatedBefore!.hashCode) + + (visibility == null ? 0 : visibility!.hashCode); + + @override + String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]'; + + Map toJson() { + final json = {}; + json[r'albumIds'] = this.albumIds; + if (this.city != null) { + json[r'city'] = this.city; + } else { + // json[r'city'] = null; + } + if (this.country != null) { + json[r'country'] = this.country; + } else { + // json[r'country'] = null; + } + if (this.createdAfter != null) { + json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + } else { + // json[r'createdAfter'] = null; + } + if (this.createdBefore != null) { + json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + } else { + // json[r'createdBefore'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + if (this.deviceId != null) { + json[r'deviceId'] = this.deviceId; + } else { + // json[r'deviceId'] = null; + } + if (this.isEncoded != null) { + json[r'isEncoded'] = this.isEncoded; + } else { + // json[r'isEncoded'] = null; + } + if (this.isFavorite != null) { + json[r'isFavorite'] = this.isFavorite; + } else { + // json[r'isFavorite'] = null; + } + if (this.isMotion != null) { + json[r'isMotion'] = this.isMotion; + } else { + // json[r'isMotion'] = null; + } + if (this.isNotInAlbum != null) { + json[r'isNotInAlbum'] = this.isNotInAlbum; + } else { + // json[r'isNotInAlbum'] = null; + } + if (this.isOffline != null) { + json[r'isOffline'] = this.isOffline; + } else { + // json[r'isOffline'] = null; + } + if (this.lensModel != null) { + json[r'lensModel'] = this.lensModel; + } else { + // json[r'lensModel'] = null; + } + if (this.libraryId != null) { + json[r'libraryId'] = this.libraryId; + } else { + // json[r'libraryId'] = null; + } + if (this.make != null) { + json[r'make'] = this.make; + } else { + // json[r'make'] = null; + } + if (this.model != null) { + json[r'model'] = this.model; + } else { + // json[r'model'] = null; + } + json[r'personIds'] = this.personIds; + if (this.rating != null) { + json[r'rating'] = this.rating; + } else { + // json[r'rating'] = null; + } + if (this.state != null) { + json[r'state'] = this.state; + } else { + // json[r'state'] = null; + } + json[r'tagIds'] = this.tagIds; + if (this.takenAfter != null) { + json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + } else { + // json[r'takenAfter'] = null; + } + if (this.takenBefore != null) { + json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + } else { + // json[r'takenBefore'] = null; + } + if (this.trashedAfter != null) { + json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + } else { + // json[r'trashedAfter'] = null; + } + if (this.trashedBefore != null) { + json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + } else { + // json[r'trashedBefore'] = null; + } + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + if (this.updatedAfter != null) { + json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + } else { + // json[r'updatedAfter'] = null; + } + if (this.updatedBefore != null) { + json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + } else { + // json[r'updatedBefore'] = null; + } + if (this.visibility != null) { + json[r'visibility'] = this.visibility; + } else { + // json[r'visibility'] = null; + } + return json; + } + + /// Returns a new [StatisticsSearchDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StatisticsSearchDto? fromJson(dynamic value) { + upgradeDto(value, "StatisticsSearchDto"); + if (value is Map) { + final json = value.cast(); + + return StatisticsSearchDto( + albumIds: json[r'albumIds'] is Iterable + ? (json[r'albumIds'] as Iterable).cast().toList(growable: false) + : const [], + city: mapValueOfType(json, r'city'), + country: mapValueOfType(json, r'country'), + createdAfter: mapDateTime(json, r'createdAfter', r''), + createdBefore: mapDateTime(json, r'createdBefore', r''), + description: mapValueOfType(json, r'description'), + deviceId: mapValueOfType(json, r'deviceId'), + isEncoded: mapValueOfType(json, r'isEncoded'), + isFavorite: mapValueOfType(json, r'isFavorite'), + isMotion: mapValueOfType(json, r'isMotion'), + isNotInAlbum: mapValueOfType(json, r'isNotInAlbum'), + isOffline: mapValueOfType(json, r'isOffline'), + lensModel: mapValueOfType(json, r'lensModel'), + libraryId: mapValueOfType(json, r'libraryId'), + make: mapValueOfType(json, r'make'), + model: mapValueOfType(json, r'model'), + personIds: json[r'personIds'] is Iterable + ? (json[r'personIds'] as Iterable).cast().toList(growable: false) + : const [], + rating: num.parse('${json[r'rating']}'), + state: mapValueOfType(json, r'state'), + tagIds: json[r'tagIds'] is Iterable + ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) + : const [], + takenAfter: mapDateTime(json, r'takenAfter', r''), + takenBefore: mapDateTime(json, r'takenBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', r''), + trashedBefore: mapDateTime(json, r'trashedBefore', r''), + type: AssetTypeEnum.fromJson(json[r'type']), + updatedAfter: mapDateTime(json, r'updatedAfter', r''), + updatedBefore: mapDateTime(json, r'updatedBefore', r''), + visibility: AssetVisibility.fromJson(json[r'visibility']), + ); + } + 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 = StatisticsSearchDto.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 = StatisticsSearchDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StatisticsSearchDto-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] = StatisticsSearchDto.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/pigeon/native_sync_api.dart b/mobile/pigeon/native_sync_api.dart index 9bcb816a64..33e2429405 100644 --- a/mobile/pigeon/native_sync_api.dart +++ b/mobile/pigeon/native_sync_api.dart @@ -20,6 +20,8 @@ class PlatformAsset { // Seconds since epoch final int? createdAt; final int? updatedAt; + final int? width; + final int? height; final int durationInSeconds; const PlatformAsset({ @@ -28,6 +30,8 @@ class PlatformAsset { required this.type, this.createdAt, this.updatedAt, + this.width, + this.height, this.durationInSeconds = 0, }); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5c54a2c349..a02379a6bd 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -81,6 +81,54 @@ packages: url: "https://pub.dev" source: hosted version: "9.2.0" + bonsoir: + dependency: transitive + description: + name: bonsoir + sha256: "2e2cf3be580deccad9a48dcaddddf90de092e74b7de2015ef58fb24e11d66496" + url: "https://pub.dev" + source: hosted + version: "5.1.11" + bonsoir_android: + dependency: transitive + description: + name: bonsoir_android + sha256: "9a65b6e50c5718c3f1a7ed6ff57ab9ed8ae990ff9c36d2b1ab3d1b90f28f7d1b" + url: "https://pub.dev" + source: hosted + version: "5.1.6" + bonsoir_darwin: + dependency: transitive + description: + name: bonsoir_darwin + sha256: "2d25c70f0d09260be1c2ab583b80dd89cbbfd59997579dadf789c5af00c7b2e4" + url: "https://pub.dev" + source: hosted + version: "5.1.3" + bonsoir_linux: + dependency: transitive + description: + name: bonsoir_linux + sha256: f2639aded6e15943a9822de98a663a1056f37cbfd0a74d72c9eaa941965945c2 + url: "https://pub.dev" + source: hosted + version: "5.1.3" + bonsoir_platform_interface: + dependency: transitive + description: + name: bonsoir_platform_interface + sha256: "08bb8b35d0198168b3bce87dbc718e4e510336cff1d97e43762e030c01636d45" + url: "https://pub.dev" + source: hosted + version: "5.1.3" + bonsoir_windows: + dependency: transitive + description: + name: bonsoir_windows + sha256: d4a0ca479d4f3679487a61f3174fb9fe1651e323c778b02dfa630490366be65d + url: "https://pub.dev" + source: hosted + version: "5.1.5" boolean_selector: dependency: transitive description: @@ -193,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + cast: + dependency: "direct main" + description: + name: cast + sha256: de1856e1a31aa60a6fed627f827921f7ec6539c67c60d0c899e89646dcbe773e + url: "https://pub.dev" + source: hosted + version: "2.1.0" characters: dependency: transitive description: @@ -1080,26 +1136,26 @@ packages: dependency: "direct main" description: name: maplibre_gl - sha256: cd0adf2da87149cab556ac70977783d6dcb3bd73b17a5583cc8366a5aafa46f8 + sha256: "5c7b1008396b2a321bada7d986ed60f9423406fbc7bd16f7ce91b385dfa054cd" url: "https://pub.dev" source: hosted - version: "0.21.0" + version: "0.22.0" maplibre_gl_platform_interface: dependency: transitive description: name: maplibre_gl_platform_interface - sha256: "6db8234705e58c09b6fd5a43747a817ba1e6e91a76deb3ed057a36a994d86f22" + sha256: "08ee0a2d0853ea945a0ab619d52c0c714f43144145cd67478fc6880b52f37509" url: "https://pub.dev" source: hosted - version: "0.21.0" + version: "0.22.0" maplibre_gl_web: dependency: transitive description: name: maplibre_gl_web - sha256: e1cbe04594fdb0d76de7cd448c0048290df8dc69dc37a85d23307dd595779141 + sha256: "2b13d4b1955a9a54e38a718f2324e56e4983c080fc6de316f6f4b5458baacb58" url: "https://pub.dev" source: hosted - version: "0.21.0" + version: "0.22.0" matcher: dependency: transitive description: @@ -1404,6 +1460,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" pub_semver: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 81249fdcfa..52b04e0d9a 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -19,6 +19,7 @@ dependencies: background_downloader: ^9.2.0 cached_network_image: ^3.4.1 cancellation_token_http: ^2.1.0 + cast: ^2.1.0 collection: ^1.18.0 connectivity_plus: ^6.1.3 crop_image: ^1.0.16 @@ -44,7 +45,7 @@ dependencies: intl: ^0.19.0 local_auth: ^2.3.0 logging: ^1.3.0 - maplibre_gl: ^0.21.0 + maplibre_gl: ^0.22.0 network_info_plus: ^6.1.3 octo_image: ^2.1.0 package_info_plus: ^8.3.0 diff --git a/mobile/test/domain/services/hash_service_test.dart b/mobile/test/domain/services/hash_service_test.dart index 1401f5d2a0..623aed5409 100644 --- a/mobile/test/domain/services/hash_service_test.dart +++ b/mobile/test/domain/services/hash_service_test.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/domain/services/hash.service.dart'; import 'package:mocktail/mocktail.dart'; @@ -21,6 +21,10 @@ void main() { late MockLocalAssetRepository mockAssetRepo; late MockStorageRepository mockStorageRepo; late MockNativeSyncApi mockNativeApi; + final sortBy = { + SortLocalAlbumsBy.backupSelection, + SortLocalAlbumsBy.isIosSharedAlbum, + }; setUp(() { mockAlbumRepo = MockLocalAlbumRepository(); @@ -42,37 +46,8 @@ void main() { }); group('HashService hashAssets', () { - test('processes albums in correct order', () async { - final album1 = LocalAlbumStub.recent - .copyWith(id: "1", backupSelection: BackupSelection.none); - final album2 = LocalAlbumStub.recent - .copyWith(id: "2", backupSelection: BackupSelection.excluded); - final album3 = LocalAlbumStub.recent - .copyWith(id: "3", backupSelection: BackupSelection.selected); - final album4 = LocalAlbumStub.recent.copyWith( - id: "4", - backupSelection: BackupSelection.selected, - isIosSharedAlbum: true, - ); - - when(() => mockAlbumRepo.getAll()) - .thenAnswer((_) async => [album1, album2, album4, album3]); - when(() => mockAlbumRepo.getAssetsToHash(any())) - .thenAnswer((_) async => []); - - await sut.hashAssets(); - - verifyInOrder([ - () => mockAlbumRepo.getAll(), - () => mockAlbumRepo.getAssetsToHash(album3.id), - () => mockAlbumRepo.getAssetsToHash(album4.id), - () => mockAlbumRepo.getAssetsToHash(album1.id), - () => mockAlbumRepo.getAssetsToHash(album2.id), - ]); - }); - test('skips albums with no assets to hash', () async { - when(() => mockAlbumRepo.getAll()).thenAnswer( + when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer( (_) async => [LocalAlbumStub.recent.copyWith(assetCount: 0)], ); when(() => mockAlbumRepo.getAssetsToHash(LocalAlbumStub.recent.id)) @@ -89,7 +64,8 @@ void main() { test('skips assets without files', () async { final album = LocalAlbumStub.recent; final asset = LocalAssetStub.image1; - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); when(() => mockStorageRepo.getFileForAsset(asset)) @@ -109,7 +85,8 @@ void main() { when(() => mockFile.length()).thenAnswer((_) async => 1000); when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); when(() => mockStorageRepo.getFileForAsset(asset)) @@ -135,7 +112,8 @@ void main() { when(() => mockFile.length()).thenAnswer((_) async => 1000); when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); when(() => mockStorageRepo.getFileForAsset(asset)) @@ -159,7 +137,8 @@ void main() { when(() => mockFile.length()).thenAnswer((_) async => 1000); when(() => mockFile.path).thenReturn('image-path'); - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset]); when(() => mockStorageRepo.getFileForAsset(asset)) @@ -197,7 +176,8 @@ void main() { when(() => mockFile2.length()).thenAnswer((_) async => 100); when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset1, asset2]); when(() => mockStorageRepo.getFileForAsset(asset1)) @@ -236,7 +216,8 @@ void main() { when(() => mockFile2.length()).thenAnswer((_) async => 100); when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset1, asset2]); when(() => mockStorageRepo.getFileForAsset(asset1)) @@ -267,7 +248,8 @@ void main() { when(() => mockFile2.length()).thenAnswer((_) async => 100); when(() => mockFile2.path).thenReturn('path-2'); - when(() => mockAlbumRepo.getAll()).thenAnswer((_) async => [album]); + when(() => mockAlbumRepo.getAll(sortBy: sortBy)) + .thenAnswer((_) async => [album]); when(() => mockAlbumRepo.getAssetsToHash(album.id)) .thenAnswer((_) async => [asset1, asset2]); when(() => mockStorageRepo.getFileForAsset(asset1)) diff --git a/mobile/test/domain/services/log_service_test.dart b/mobile/test/domain/services/log_service_test.dart index 5811a8c430..fd9a86ce4e 100644 --- a/mobile/test/domain/services/log_service_test.dart +++ b/mobile/test/domain/services/log_service_test.dart @@ -74,7 +74,7 @@ void main() { setUp(() async { when(() => mockStoreRepo.insert(StoreKey.logLevel, any())) .thenAnswer((_) async => true); - await sut.setlogLevel(LogLevel.shout); + await sut.setLogLevel(LogLevel.shout); }); test('Updates the log level in store', () { diff --git a/mobile/test/domain/services/store_service_test.dart b/mobile/test/domain/services/store_service_test.dart index 554ca73500..8f4749ac62 100644 --- a/mobile/test/domain/services/store_service_test.dart +++ b/mobile/test/domain/services/store_service_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid-dynamic - import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; @@ -18,10 +16,10 @@ final _kBackupFailedSince = DateTime.utc(2023); void main() { late StoreService sut; late IStoreRepository mockStoreRepo; - late StreamController controller; + late StreamController> controller; setUp(() async { - controller = StreamController.broadcast(); + controller = StreamController>.broadcast(); mockStoreRepo = MockStoreRepository(); // For generics, we need to provide fallback to each concrete type to avoid runtime errors registerFallbackValue(StoreKey.accessToken); @@ -29,18 +27,14 @@ void main() { registerFallbackValue(StoreKey.backgroundBackup); registerFallbackValue(StoreKey.backupFailedSince); - when(() => mockStoreRepo.tryGet(any>())) - .thenAnswer((invocation) async { - final key = invocation.positionalArguments.firstOrNull as StoreKey; - return switch (key) { - StoreKey.accessToken => _kAccessToken, - StoreKey.backgroundBackup => _kBackgroundBackup, - StoreKey.groupAssetsBy => _kGroupAssetsBy, - StoreKey.backupFailedSince => _kBackupFailedSince, - // ignore: avoid-wildcard-cases-with-enums - _ => null, - }; - }); + when(() => mockStoreRepo.getAll()).thenAnswer( + (_) async => [ + const StoreDto(StoreKey.accessToken, _kAccessToken), + const StoreDto(StoreKey.backgroundBackup, _kBackgroundBackup), + const StoreDto(StoreKey.groupAssetsBy, _kGroupAssetsBy), + StoreDto(StoreKey.backupFailedSince, _kBackupFailedSince), + ], + ); when(() => mockStoreRepo.watchAll()).thenAnswer((_) => controller.stream); sut = await StoreService.create(storeRepository: mockStoreRepo); @@ -53,8 +47,7 @@ void main() { group("Store Service Init:", () { test('Populates the internal cache on init', () { - verify(() => mockStoreRepo.tryGet(any>())) - .called(equals(StoreKey.values.length)); + verify(() => mockStoreRepo.getAll()).called(1); expect(sut.tryGet(StoreKey.accessToken), _kAccessToken); expect(sut.tryGet(StoreKey.backgroundBackup), _kBackgroundBackup); expect(sut.tryGet(StoreKey.groupAssetsBy), _kGroupAssetsBy); @@ -64,8 +57,7 @@ void main() { }); test('Listens to stream of store updates', () async { - final event = - StoreUpdateEvent(StoreKey.accessToken, _kAccessToken.toUpperCase()); + final event = StoreDto(StoreKey.accessToken, _kAccessToken.toUpperCase()); controller.add(event); await pumpEventQueue(); diff --git a/mobile/test/infrastructure/repositories/local_album_repository_test.dart b/mobile/test/infrastructure/repositories/local_album_repository_test.dart new file mode 100644 index 0000000000..827d81c79b --- /dev/null +++ b/mobile/test/infrastructure/repositories/local_album_repository_test.dart @@ -0,0 +1,66 @@ +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +import '../../test_utils/medium_factory.dart'; + +void main() { + late Drift db; + late MediumFactory mediumFactory; + + setUp(() { + db = Drift( + DatabaseConnection( + NativeDatabase.memory(), + closeStreamsSynchronously: true, + ), + ); + mediumFactory = MediumFactory(db); + }); + + group('getAll', () { + test('sorts albums by backupSelection & isIosSharedAlbum', () async { + final localAlbumRepo = + mediumFactory.getRepository(); + await localAlbumRepo.upsert( + mediumFactory.localAlbum( + id: '1', + backupSelection: BackupSelection.none, + ), + ); + await localAlbumRepo.upsert( + mediumFactory.localAlbum( + id: '2', + backupSelection: BackupSelection.excluded, + ), + ); + await localAlbumRepo.upsert( + mediumFactory.localAlbum( + id: '3', + backupSelection: BackupSelection.selected, + isIosSharedAlbum: true, + ), + ); + await localAlbumRepo.upsert( + mediumFactory.localAlbum( + id: '4', + backupSelection: BackupSelection.selected, + ), + ); + final albums = await localAlbumRepo.getAll( + sortBy: { + SortLocalAlbumsBy.backupSelection, + SortLocalAlbumsBy.isIosSharedAlbum, + }, + ); + expect(albums.length, 4); + expect(albums[0].id, '4'); // selected + expect(albums[1].id, '3'); // selected & isIosSharedAlbum + expect(albums[2].id, '1'); // none + expect(albums[3].id, '2'); // excluded + }); + }); +} diff --git a/mobile/test/infrastructure/repositories/store_repository_test.dart b/mobile/test/infrastructure/repositories/store_repository_test.dart index 16e0632d83..528e17ba3d 100644 --- a/mobile/test/infrastructure/repositories/store_repository_test.dart +++ b/mobile/test/infrastructure/repositories/store_repository_test.dart @@ -1,5 +1,3 @@ -// ignore_for_file: avoid-dynamic - import 'package:flutter_test/flutter_test.dart'; import 'package:immich_mobile/domain/interfaces/store.interface.dart'; import 'package:immich_mobile/domain/models/store.model.dart'; @@ -146,32 +144,21 @@ void main() { expectLater( stream, emitsInAnyOrder([ + emits(const StoreDto(StoreKey.version, _kTestVersion)), emits( - const StoreUpdateEvent(StoreKey.version, _kTestVersion), + StoreDto(StoreKey.backupFailedSince, _kTestBackupFailed), ), emits( - StoreUpdateEvent( - StoreKey.backupFailedSince, - _kTestBackupFailed, - ), + const StoreDto(StoreKey.accessToken, _kTestAccessToken), ), emits( - const StoreUpdateEvent( - StoreKey.accessToken, - _kTestAccessToken, - ), - ), - emits( - const StoreUpdateEvent( + const StoreDto( StoreKey.colorfulInterface, _kTestColorfulInterface, ), ), emits( - const StoreUpdateEvent( - StoreKey.version, - _kTestVersion + 10, - ), + const StoreDto(StoreKey.version, _kTestVersion + 10), ), ]), ); diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart new file mode 100644 index 0000000000..64c23321aa --- /dev/null +++ b/mobile/test/test_utils/medium_factory.dart @@ -0,0 +1,65 @@ +import 'dart:math'; + +import 'package:immich_mobile/domain/interfaces/local_album.interface.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; + +class MediumFactory { + final Drift _db; + + const MediumFactory(Drift db) : _db = db; + + LocalAsset localAsset({ + String? id, + String? name, + AssetType? type, + DateTime? createdAt, + DateTime? updatedAt, + String? checksum, + }) { + final random = Random(); + + return LocalAsset( + id: id ?? '${random.nextInt(1000000)}', + name: name ?? 'Asset ${random.nextInt(1000000)}', + checksum: checksum ?? '${random.nextInt(1000000)}', + type: type ?? AssetType.image, + createdAt: createdAt ?? + DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + updatedAt: updatedAt ?? + DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + ); + } + + LocalAlbum localAlbum({ + String? id, + String? name, + DateTime? updatedAt, + int? assetCount, + BackupSelection? backupSelection, + bool? isIosSharedAlbum, + }) { + final random = Random(); + + return LocalAlbum( + id: id ?? '${random.nextInt(1000000)}', + name: name ?? 'Album ${random.nextInt(1000000)}', + updatedAt: updatedAt ?? + DateTime.fromMillisecondsSinceEpoch(random.nextInt(1000000000)), + assetCount: assetCount ?? random.nextInt(100), + backupSelection: backupSelection ?? BackupSelection.none, + isIosSharedAlbum: isIosSharedAlbum ?? false, + ); + } + + T getRepository() { + switch (T) { + case const (ILocalAlbumRepository): + return DriftLocalAlbumRepository(_db) as T; + default: + throw Exception('Unknown repository: $T'); + } + } +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 0e35be2ee0..775fe117ad 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -5158,6 +5158,48 @@ ] } }, + "/search/statistics": { + "post": { + "operationId": "searchAssetStatistics", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StatisticsSearchDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchStatisticsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Search" + ] + } + }, "/search/suggestions": { "get": { "operationId": "getSearchSuggestions", @@ -10824,6 +10866,13 @@ }, "MetadataSearchDto": { "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, "checksum": { "type": "string" }, @@ -11743,6 +11792,13 @@ }, "RandomSearchDto": { "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, "city": { "nullable": true, "type": "string" @@ -12069,6 +12125,17 @@ ], "type": "object" }, + "SearchStatisticsResponseDto": { + "properties": { + "total": { + "type": "integer" + } + }, + "required": [ + "total" + ], + "type": "object" + }, "SearchSuggestionType": { "enum": [ "country", @@ -12780,6 +12847,13 @@ }, "SmartSearchDto": { "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, "city": { "nullable": true, "type": "string" @@ -12974,6 +13048,132 @@ }, "type": "object" }, + "StatisticsSearchDto": { + "properties": { + "albumIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "city": { + "nullable": true, + "type": "string" + }, + "country": { + "nullable": true, + "type": "string" + }, + "createdAfter": { + "format": "date-time", + "type": "string" + }, + "createdBefore": { + "format": "date-time", + "type": "string" + }, + "description": { + "type": "string" + }, + "deviceId": { + "type": "string" + }, + "isEncoded": { + "type": "boolean" + }, + "isFavorite": { + "type": "boolean" + }, + "isMotion": { + "type": "boolean" + }, + "isNotInAlbum": { + "type": "boolean" + }, + "isOffline": { + "type": "boolean" + }, + "lensModel": { + "nullable": true, + "type": "string" + }, + "libraryId": { + "format": "uuid", + "nullable": true, + "type": "string" + }, + "make": { + "type": "string" + }, + "model": { + "nullable": true, + "type": "string" + }, + "personIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "rating": { + "maximum": 5, + "minimum": -1, + "type": "number" + }, + "state": { + "nullable": true, + "type": "string" + }, + "tagIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "takenAfter": { + "format": "date-time", + "type": "string" + }, + "takenBefore": { + "format": "date-time", + "type": "string" + }, + "trashedAfter": { + "format": "date-time", + "type": "string" + }, + "trashedBefore": { + "format": "date-time", + "type": "string" + }, + "type": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetTypeEnum" + } + ] + }, + "updatedAfter": { + "format": "date-time", + "type": "string" + }, + "updatedBefore": { + "format": "date-time", + "type": "string" + }, + "visibility": { + "allOf": [ + { + "$ref": "#/components/schemas/AssetVisibility" + } + ] + } + }, + "type": "object" + }, "SyncAckDeleteDto": { "properties": { "types": { diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index e524b5e27e..b8e9986772 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "typescript": "^5.3.3" } }, @@ -23,9 +23,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index cf73d261ff..3299a3a5be 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "typescript": "^5.3.3" }, "repository": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index fa75049168..fee00acffd 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -853,6 +853,7 @@ export type SearchExploreResponseDto = { items: SearchExploreItem[]; }; export type MetadataSearchDto = { + albumIds?: string[]; checksum?: string; city?: string | null; country?: string | null; @@ -929,6 +930,7 @@ export type PlacesResponseDto = { name: string; }; export type RandomSearchDto = { + albumIds?: string[]; city?: string | null; country?: string | null; createdAfter?: string; @@ -962,6 +964,7 @@ export type RandomSearchDto = { withStacked?: boolean; }; export type SmartSearchDto = { + albumIds?: string[]; city?: string | null; country?: string | null; createdAfter?: string; @@ -995,6 +998,39 @@ export type SmartSearchDto = { withDeleted?: boolean; withExif?: boolean; }; +export type StatisticsSearchDto = { + albumIds?: string[]; + city?: string | null; + country?: string | null; + createdAfter?: string; + createdBefore?: string; + description?: string; + deviceId?: string; + isEncoded?: boolean; + isFavorite?: boolean; + isMotion?: boolean; + isNotInAlbum?: boolean; + isOffline?: boolean; + lensModel?: string | null; + libraryId?: string | null; + make?: string; + model?: string | null; + personIds?: string[]; + rating?: number; + state?: string | null; + tagIds?: string[]; + takenAfter?: string; + takenBefore?: string; + trashedAfter?: string; + trashedBefore?: string; + "type"?: AssetTypeEnum; + updatedAfter?: string; + updatedBefore?: string; + visibility?: AssetVisibility; +}; +export type SearchStatisticsResponseDto = { + total: number; +}; export type ServerAboutResponseDto = { build?: string; buildImage?: string; @@ -2882,6 +2918,18 @@ export function searchSmart({ smartSearchDto }: { body: smartSearchDto }))); } +export function searchAssetStatistics({ statisticsSearchDto }: { + statisticsSearchDto: StatisticsSearchDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: SearchStatisticsResponseDto; + }>("/search/statistics", oazapfts.json({ + ...opts, + method: "POST", + body: statisticsSearchDto + }))); +} export function getSearchSuggestions({ country, includeNull, make, model, state, $type }: { country?: string; includeNull?: boolean; diff --git a/server/package-lock.json b/server/package-lock.json index 8df5b7f249..6f40c42fbf 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -27,7 +27,7 @@ "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", - "bcrypt": "^5.1.1", + "bcrypt": "^6.0.0", "bullmq": "^5.51.0", "chokidar": "^3.5.3", "class-transformer": "^0.5.1", @@ -52,13 +52,13 @@ "nestjs-cls": "^5.0.0", "nestjs-kysely": "^1.1.0", "nestjs-otel": "^6.0.0", - "nodemailer": "^6.9.13", + "nodemailer": "^7.0.0", "openid-client": "^6.3.3", "pg": "^8.11.3", "picomatch": "^4.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-email": "^3.0.4", + "react-email": "^4.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -79,8 +79,8 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.4", "@swc/core": "^1.4.14", - "@testcontainers/postgresql": "^10.2.1", - "@testcontainers/redis": "^10.18.0", + "@testcontainers/postgresql": "^11.0.0", + "@testcontainers/redis": "^11.0.0", "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^5.0.0", @@ -92,7 +92,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -105,7 +105,7 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", "jsdom": "^26.1.0", "mock-fs": "^5.2.0", @@ -119,7 +119,7 @@ "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", "supertest": "^7.1.0", - "testcontainers": "^10.18.0", + "testcontainers": "^11.0.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", @@ -146,6 +146,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -486,54 +487,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", - "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", - "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.2", - "@babel/generator": "^7.24.5", - "@babel/helper-compilation-targets": "^7.23.6", - "@babel/helper-module-transforms": "^7.24.5", - "@babel/helpers": "^7.24.5", - "@babel/parser": "^7.24.5", - "@babel/template": "^7.24.0", - "@babel/traverse": "^7.24.5", - "@babel/types": "^7.24.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/generator": { "version": "7.27.0", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.0.tgz", @@ -550,76 +503,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.0.tgz", - "integrity": "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-validator-option": "^7.25.9", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -638,28 +521,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.0.tgz", - "integrity": "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/parser": { "version": "7.27.3", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.3.tgz", @@ -882,10 +743,266 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.0.tgz", - "integrity": "sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], @@ -898,6 +1015,134 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1052,16 +1297,6 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@fastify/busboy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", - "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - } - }, "node_modules/@golevelup/nestjs-discovery": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.3.tgz", @@ -2081,6 +2316,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { "detect-libc": "^2.0.0", @@ -2102,6 +2338,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -2123,6 +2360,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "license": "ISC", "dependencies": { "glob": "^7.1.3" @@ -2714,15 +2952,15 @@ } }, "node_modules/@next/env": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.2.tgz", - "integrity": "sha512-Hm3jIGsoUl6RLB1vzY+dZeqb+/kWPZ+h34yiWxW0dV87l8Im/eMOwpOA+a0L78U0HM04syEjXuRlCozqpwuojQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", + "integrity": "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.2.tgz", - "integrity": "sha512-b9TN7q+j5/7+rGLhFAVZiKJGIASuo8tWvInGfAd8wsULjB1uNGRCj1z1WZwwPWzVQbIKWFYqc+9L7W09qwt52w==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz", + "integrity": "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==", "cpu": [ "arm64" ], @@ -2736,9 +2974,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.2.tgz", - "integrity": "sha512-caR62jNDUCU+qobStO6YJ05p9E+LR0EoXh1EEmyU69cYydsAy7drMcOlUlRtQihM6K6QfvNwJuLhsHcCzNpqtA==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", + "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", "cpu": [ "x64" ], @@ -2752,9 +2990,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.2.tgz", - "integrity": "sha512-fHHXBusURjBmN6VBUtu6/5s7cCeEkuGAb/ZZiGHBLVBXMBy4D5QpM8P33Or8JD1nlOjm/ZT9sEE5HouQ0F+hUA==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", + "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", "cpu": [ "arm64" ], @@ -2768,9 +3006,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.2.tgz", - "integrity": "sha512-9CF1Pnivij7+M3G74lxr+e9h6o2YNIe7QtExWq1KUK4hsOLTBv6FJikEwCaC3NeYTflzrm69E5UfwEAbV2U9/g==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", + "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", "cpu": [ "arm64" ], @@ -2784,9 +3022,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.2.tgz", - "integrity": "sha512-tINV7WmcTUf4oM/eN3Yuu/f8jQ5C6AkueZPKeALs/qfdfX57eNv4Ij7rt0SA6iZ8+fMobVfcFVv664Op0caCCg==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", + "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", "cpu": [ "x64" ], @@ -2800,9 +3038,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.2.tgz", - "integrity": "sha512-jf2IseC4WRsGkzeUw/cK3wci9pxR53GlLAt30+y+B+2qAQxMw6WAC3QrANIKxkcoPU3JFh/10uFfmoMDF9JXKg==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", + "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", "cpu": [ "x64" ], @@ -2816,9 +3054,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.2.tgz", - "integrity": "sha512-wvg7MlfnaociP7k8lxLX4s2iBJm4BrNiNFhVUY+Yur5yhAJHfkS8qPPeDEUH8rQiY0PX3u/P7Q/wcg6Mv6GSAA==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", + "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", "cpu": [ "arm64" ], @@ -2832,9 +3070,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.2.tgz", - "integrity": "sha512-D3cNA8NoT3aWISWmo7HF5Eyko/0OdOO+VagkoJuiTk7pyX3P/b+n8XA/MYvyR+xSVcbKn68B1rY9fgqjNISqzQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", + "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", "cpu": [ "x64" ], @@ -5104,23 +5342,23 @@ } }, "node_modules/@testcontainers/postgresql": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.28.0.tgz", - "integrity": "sha512-NN25rruG5D4Q7pCNIJuHwB+G85OSeJ3xHZ2fWx0O6sPoPEfCYwvpj8mq99cyn68nxFkFYZeyrZJtSFO+FnydiA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-11.0.2.tgz", + "integrity": "sha512-Z7ZrFRAien1W1NlYxCK3PP0zUk468UHwtJubJRmktDPHTE6DLITmn+U565I7roMk66NjN3eWhly1zoUM48PtmA==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^10.28.0" + "testcontainers": "^11.0.2" } }, "node_modules/@testcontainers/redis": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-10.28.0.tgz", - "integrity": "sha512-xDNKSJTBmQca/3v5sdHmqSCYr68vjvAGSxoHCuWylha77gAYn88g5nUZK0ocNbUZgBq69KhIzj/f9zlHkw34uA==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@testcontainers/redis/-/redis-11.0.2.tgz", + "integrity": "sha512-hLaTvjv74HtAWZFz2U/GJc5cYCIyB37TR3yCBjtBZ+dp3wZ0gc1PtsKs6qMw4fOfUVPzFm1f1pj32DF55F1+KQ==", "dev": true, "license": "MIT", "dependencies": { - "testcontainers": "^10.28.0" + "testcontainers": "^11.0.2" } }, "node_modules/@tokenizer/inflate": { @@ -5301,9 +5539,9 @@ } }, "node_modules/@types/dockerode": { - "version": "3.3.38", - "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.38.tgz", - "integrity": "sha512-nnrcfUe2iR+RyOuz0B4bZgQwD9djQa9ADEjp7OAgBs10pYT0KSCtplJjcmBDJz0qaReX5T7GbE5i4VplvzUHvA==", + "version": "3.3.40", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.40.tgz", + "integrity": "sha512-O1ckSFYbcYv/KcnAHMLCnKQYY8/5+6CRzpsOPcQIePHRX2jG4Gmz8uXPMCXIxTGN9OYkE5eox/L67l2sGY1UYg==", "dev": true, "license": "MIT", "dependencies": { @@ -5480,9 +5718,9 @@ } }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", + "version": "22.15.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", + "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -5508,13 +5746,6 @@ "@types/node": "*" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/oracledb": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/@types/oracledb/-/oracledb-6.5.2.tgz", @@ -6292,6 +6523,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, "license": "ISC" }, "node_modules/abort-controller": { @@ -6354,6 +6586,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, "license": "MIT", "dependencies": { "debug": "4" @@ -6543,6 +6776,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, "license": "ISC" }, "node_modules/archiver": { @@ -6661,6 +6895,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", "deprecated": "This package is no longer supported.", + "dev": true, "license": "ISC", "dependencies": { "delegates": "^1.0.0", @@ -6674,6 +6909,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -6892,17 +7128,17 @@ } }, "node_modules/bcrypt": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", - "integrity": "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", + "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.11", - "node-addon-api": "^5.0.0" + "node-addon-api": "^8.3.0", + "node-gyp-build": "^4.8.4" }, "engines": { - "node": ">= 10.0.0" + "node": ">= 18" } }, "node_modules/bcrypt-pbkdf": { @@ -6915,12 +7151,6 @@ "tweetnacl": "^0.14.3" } }, - "node_modules/bcrypt/node_modules/node-addon-api": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", - "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==", - "license": "MIT" - }, "node_modules/bignumber.js": { "version": "9.2.1", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.2.1.tgz", @@ -6991,6 +7221,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7013,6 +7244,7 @@ "version": "4.24.4", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -7091,9 +7323,9 @@ } }, "node_modules/builtin-modules": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", - "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", "dev": true, "license": "MIT", "engines": { @@ -7506,6 +7738,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -7754,6 +7987,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, "license": "ISC", "bin": { "color-support": "bin.js" @@ -7881,6 +8115,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -7925,6 +8160,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, "license": "ISC" }, "node_modules/content-disposition": { @@ -7948,12 +8184,6 @@ "node": ">= 0.6" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -8246,9 +8476,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -8352,6 +8582,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, "license": "MIT" }, "node_modules/denque": { @@ -8440,9 +8671,9 @@ "peer": true }, "node_modules/docker-compose": { - "version": "0.24.8", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.8.tgz", - "integrity": "sha512-plizRs/Vf15H+GCVxq2EUvyPK7ei9b/cVesHvjnX4xaXjM9spHe2Ytq0BitndFgvTJ3E3NljPNUEl7BAN43iZw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-1.2.0.tgz", + "integrity": "sha512-wIU1eHk3Op7dFgELRdmOYlPYS4gP8HhH1ZmZa13QZF59y0fblzFDFmKPhyc05phCy2hze9OEvNZAsoljrs+72w==", "dev": true, "license": "MIT", "dependencies": { @@ -8484,9 +8715,9 @@ } }, "node_modules/dockerode": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.5.tgz", - "integrity": "sha512-ZPmKSr1k1571Mrh7oIBS/j0AqAccoecY2yH420ni5j1KyNMgnoTh4Nu4FWunh0HZIJmRSmSysJjBIpa/zyWUEA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.7.tgz", + "integrity": "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -8665,6 +8896,7 @@ "version": "1.5.137", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.137.tgz", "integrity": "sha512-/QSJaU2JyIuTbbABAo/crOs+SuAZLS+fVVS10PVrIT9hrRkmZl8Hb0xPSkKRUUWHQtYzXHpQUW3Dy5hwMzGZkA==", + "dev": true, "license": "ISC" }, "node_modules/emoji-regex": { @@ -8905,9 +9137,9 @@ } }, "node_modules/esbuild": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", - "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -8917,30 +9149,31 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.23.0", - "@esbuild/android-arm": "0.23.0", - "@esbuild/android-arm64": "0.23.0", - "@esbuild/android-x64": "0.23.0", - "@esbuild/darwin-arm64": "0.23.0", - "@esbuild/darwin-x64": "0.23.0", - "@esbuild/freebsd-arm64": "0.23.0", - "@esbuild/freebsd-x64": "0.23.0", - "@esbuild/linux-arm": "0.23.0", - "@esbuild/linux-arm64": "0.23.0", - "@esbuild/linux-ia32": "0.23.0", - "@esbuild/linux-loong64": "0.23.0", - "@esbuild/linux-mips64el": "0.23.0", - "@esbuild/linux-ppc64": "0.23.0", - "@esbuild/linux-riscv64": "0.23.0", - "@esbuild/linux-s390x": "0.23.0", - "@esbuild/linux-x64": "0.23.0", - "@esbuild/netbsd-x64": "0.23.0", - "@esbuild/openbsd-arm64": "0.23.0", - "@esbuild/openbsd-x64": "0.23.0", - "@esbuild/sunos-x64": "0.23.0", - "@esbuild/win32-arm64": "0.23.0", - "@esbuild/win32-ia32": "0.23.0", - "@esbuild/win32-x64": "0.23.0" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/escalade": { @@ -9079,50 +9312,65 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz", - "integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==", + "version": "59.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz", + "integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", - "@eslint-community/eslint-utils": "^4.4.1", - "ci-info": "^4.1.0", + "@eslint-community/eslint-utils": "^4.5.1", + "@eslint/plugin-kit": "^0.2.7", + "ci-info": "^4.2.0", "clean-regexp": "^1.0.0", - "core-js-compat": "^3.40.0", + "core-js-compat": "^3.41.0", "esquery": "^1.6.0", - "globals": "^15.15.0", + "find-up-simple": "^1.0.1", + "globals": "^16.0.0", "indent-string": "^5.0.0", - "is-builtin-module": "^4.0.0", + "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", - "read-package-up": "^11.0.0", "regexp-tree": "^0.1.27", "regjsparser": "^0.12.0", "semver": "^7.7.1", "strip-indent": "^4.0.0" }, "engines": { - "node": ">=18.18" + "node": "^18.20.0 || ^20.10.0 || >=21.0.0" }, "funding": { "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=9.20.0" + "eslint": ">=9.22.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/eslint-scope": { @@ -9887,6 +10135,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -9899,6 +10148,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -9918,6 +10168,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, "license": "ISC" }, "node_modules/function-bind": { @@ -9934,6 +10185,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", "deprecated": "This package is no longer supported.", + "dev": true, "license": "ISC", "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -9954,6 +10206,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -9963,12 +10216,14 @@ "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, "license": "ISC" }, "node_modules/gauge/node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -10029,15 +10284,6 @@ "node": ">=14" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/geo-tz": { "version": "8.1.4", "resolved": "https://registry.npmjs.org/geo-tz/-/geo-tz-8.1.4.tgz", @@ -10081,6 +10327,18 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -10135,7 +10393,6 @@ "version": "11.0.1", "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", - "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", @@ -10178,7 +10435,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -10188,7 +10444,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" @@ -10338,6 +10593,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, "license": "ISC" }, "node_modules/hasown": { @@ -10361,19 +10617,6 @@ "he": "bin/he" } }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -10480,6 +10723,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, "license": "MIT", "dependencies": { "agent-base": "6", @@ -10594,24 +10838,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/index-to-position": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -10752,13 +10984,13 @@ } }, "node_modules/is-builtin-module": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", - "integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", "dev": true, "license": "MIT", "dependencies": { - "builtin-modules": "^4.0.0" + "builtin-modules": "^5.0.0" }, "engines": { "node": ">=18.20" @@ -11021,7 +11253,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.0.tgz", "integrity": "sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -11316,6 +11547,7 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -11645,6 +11877,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, "license": "MIT", "dependencies": { "semver": "^6.0.0" @@ -11660,6 +11893,7 @@ "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -11849,6 +12083,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -11863,6 +12109,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -12015,6 +12262,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, "license": "MIT", "dependencies": { "minipass": "^3.0.0", @@ -12028,6 +12276,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -12418,12 +12667,12 @@ } }, "node_modules/next": { - "version": "15.1.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.2.tgz", - "integrity": "sha512-nLJDV7peNy+0oHlmY2JZjzMfJ8Aj0/dd3jCwSZS8ZiO5nkQfcZRqDrRN3U5rJtqVTQneIOGZzb6LCNrk7trMCQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz", + "integrity": "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==", "license": "MIT", "dependencies": { - "@next/env": "15.1.2", + "@next/env": "15.3.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -12438,15 +12687,15 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.2", - "@next/swc-darwin-x64": "15.1.2", - "@next/swc-linux-arm64-gnu": "15.1.2", - "@next/swc-linux-arm64-musl": "15.1.2", - "@next/swc-linux-x64-gnu": "15.1.2", - "@next/swc-linux-x64-musl": "15.1.2", - "@next/swc-win32-arm64-msvc": "15.1.2", - "@next/swc-win32-x64-msvc": "15.1.2", - "sharp": "^0.33.5" + "@next/swc-darwin-arm64": "15.3.3", + "@next/swc-darwin-x64": "15.3.3", + "@next/swc-linux-arm64-gnu": "15.3.3", + "@next/swc-linux-arm64-musl": "15.3.3", + "@next/swc-linux-x64-gnu": "15.3.3", + "@next/swc-linux-x64-musl": "15.3.3", + "@next/swc-win32-arm64-msvc": "15.3.3", + "@next/swc-win32-x64-msvc": "15.3.3", + "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -12490,7 +12739,6 @@ "version": "8.3.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.3.1.tgz", "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==", - "dev": true, "license": "MIT", "engines": { "node": "^18 || ^20 || >= 21" @@ -12551,6 +12799,17 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-gyp-build-optional-packages": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", @@ -12689,12 +12948,13 @@ "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, "license": "MIT" }, "node_modules/nodemailer": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz", - "integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-7.0.3.tgz", + "integrity": "sha512-Ajq6Sz1x7cIK3pN6KesGTah+1gnwMnx5gKl3piQlQQE/PwyJ4Mbc8is2psWYxK3RJTVeqsDaCv8ZzXLCDHMTZw==", "license": "MIT-0", "engines": { "node": ">=6.0.0" @@ -12704,6 +12964,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, "license": "ISC", "dependencies": { "abbrev": "1" @@ -12715,21 +12976,6 @@ "node": ">=6" } }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -12750,6 +12996,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", "deprecated": "This package is no longer supported.", + "dev": true, "license": "ISC", "dependencies": { "are-we-there-yet": "^2.0.0", @@ -13208,6 +13455,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -13232,7 +13480,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", - "dev": true, "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", @@ -13249,7 +13496,6 @@ "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", - "dev": true, "license": "ISC", "engines": { "node": "20 || >=22" @@ -14021,52 +14267,43 @@ } }, "node_modules/react-email": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.7.tgz", - "integrity": "sha512-lX9dFCPtTG+79aP9uTdx763byshptPYbOi0KXwxn6nPJoDP/Ty/G1W5fx1lbrmec+pk38MTDZPrzJ/UYIxgP/Q==", + "version": "4.0.16", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.0.16.tgz", + "integrity": "sha512-auhFU+nQxAkKkP6lQhPyGsa9exwfUEzp2BwZnjHokCwphZlg30tu4t1LgdKRwGPYsi7XNGy6asbVLAUhOVpzzg==", "license": "MIT", "dependencies": { - "@babel/core": "7.24.5", - "@babel/parser": "7.24.5", - "chalk": "4.1.2", - "chokidar": "4.0.3", - "commander": "11.1.0", - "debounce": "2.0.0", - "esbuild": "0.23.0", - "glob": "10.3.4", - "log-symbols": "4.1.0", - "mime-types": "2.1.35", - "next": "15.1.2", - "normalize-path": "3.0.0", - "ora": "5.4.1", - "socket.io": "4.8.1" + "@babel/parser": "^7.27.0", + "@babel/traverse": "^7.27.0", + "chalk": "^5.0.0", + "chokidar": "^4.0.3", + "commander": "^13.0.0", + "debounce": "^2.0.0", + "esbuild": "^0.25.0", + "glob": "^11.0.0", + "log-symbols": "^7.0.0", + "mime-types": "^3.0.0", + "next": "^15.3.1", + "normalize-path": "^3.0.0", + "ora": "^8.0.0", + "socket.io": "^4.8.1" }, "bin": { - "email": "dist/cli/index.js" + "email": "dist/cli/index.mjs" }, "engines": { "node": ">=18.0.0" } }, - "node_modules/react-email/node_modules/@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "node_modules/react-email/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/react-email/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==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/react-email/node_modules/chokidar": { @@ -14084,105 +14321,140 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/react-email/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-email/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/react-email/node_modules/glob": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", - "integrity": "sha512-6LFElP3A+i/Q8XQKEvZjkEWEOTgAIALR9AO2rwT8bgPhDd1anmqDJDZ6lLddI4ehxxxR1S5RIqKe1uapMQfYaQ==", - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^2.0.3", - "minimatch": "^9.0.1", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0", - "path-scurry": "^1.10.1" - }, - "bin": { - "glob": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "node_modules/react-email/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" }, - "node_modules/react-email/node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/react-email/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/react-email/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/react-email/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/log-symbols": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", + "integrity": "sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==", "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/react-email/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "license": "BlueOak-1.0.0", + "node_modules/react-email/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=16 || 14 >=14.18" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/react-email/node_modules/readdirp": { @@ -14198,6 +14470,39 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/react-email/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/react-email/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -14223,88 +14528,6 @@ "pify": "^2.3.0" } }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", - "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", - "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", @@ -14897,6 +15120,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, "license": "ISC" }, "node_modules/set-function-length": { @@ -15379,42 +15603,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/split-ca": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", @@ -15550,6 +15738,18 @@ "dev": true, "license": "MIT" }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/stream-source": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/stream-source/-/stream-source-0.3.5.tgz", @@ -16135,6 +16335,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", @@ -16149,9 +16350,9 @@ } }, "node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz", + "integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==", "dev": true, "license": "MIT", "dependencies": { @@ -16178,6 +16379,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, "license": "ISC", "engines": { "node": ">=8" @@ -16187,6 +16389,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -16427,27 +16630,27 @@ } }, "node_modules/testcontainers": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.28.0.tgz", - "integrity": "sha512-1fKrRRCsgAQNkarjHCMKzBKXSJFmzNTiTbhb5E/j5hflRXChEtHvkefjaHlgkNUjfw92/Dq8LTgwQn6RDBFbMg==", + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-11.0.2.tgz", + "integrity": "sha512-rI+CiMYGxd1t/fTW+xs79OHkhBIVLCh7Xstm6duYhKH+LZGyoWtXJsljLvkvWV0J8y8FovcG2Z6/1j1KPqM3rA==", "dev": true, "license": "MIT", "dependencies": { "@balena/dockerignore": "^1.0.2", - "@types/dockerode": "^3.3.35", + "@types/dockerode": "^3.3.39", "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", - "debug": "^4.3.5", - "docker-compose": "^0.24.8", - "dockerode": "^4.0.5", + "debug": "^4.4.1", + "docker-compose": "^1.2.0", + "dockerode": "^4.0.6", "get-port": "^7.1.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", - "tar-fs": "^3.0.7", + "tar-fs": "^3.0.9", "tmp": "^0.2.3", - "undici": "^5.29.0" + "undici": "^7.10.0" } }, "node_modules/testcontainers/node_modules/tmp": { @@ -17174,16 +17377,13 @@ } }, "node_modules/undici": { - "version": "5.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", - "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.10.0.tgz", + "integrity": "sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw==", "dev": true, "license": "MIT", - "dependencies": { - "@fastify/busboy": "^2.0.0" - }, "engines": { - "node": ">=14.0" + "node": ">=20.18.1" } }, "node_modules/undici-types": { @@ -17192,19 +17392,6 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unique-filename": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", @@ -17284,6 +17471,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -17367,17 +17555,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/validator": { "version": "13.15.0", "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.0.tgz", @@ -17511,64 +17688,6 @@ } } }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" - } - }, "node_modules/vite/node_modules/postcss": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", @@ -17976,6 +18095,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" @@ -18137,6 +18257,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC" }, "node_modules/yaml": { @@ -18191,6 +18312,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", + "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/yoctocolors-cjs": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", diff --git a/server/package.json b/server/package.json index ffd8876a1c..d2fd777be3 100644 --- a/server/package.json +++ b/server/package.json @@ -53,7 +53,7 @@ "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", - "bcrypt": "^5.1.1", + "bcrypt": "^6.0.0", "bullmq": "^5.51.0", "chokidar": "^3.5.3", "class-transformer": "^0.5.1", @@ -78,13 +78,13 @@ "nestjs-cls": "^5.0.0", "nestjs-kysely": "^1.1.0", "nestjs-otel": "^6.0.0", - "nodemailer": "^6.9.13", + "nodemailer": "^7.0.0", "openid-client": "^6.3.3", "pg": "^8.11.3", "picomatch": "^4.0.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-email": "^3.0.4", + "react-email": "^4.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -105,8 +105,8 @@ "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^11.0.4", "@swc/core": "^1.4.14", - "@testcontainers/postgresql": "^10.2.1", - "@testcontainers/redis": "^10.18.0", + "@testcontainers/postgresql": "^11.0.0", + "@testcontainers/redis": "^11.0.0", "@types/archiver": "^6.0.0", "@types/async-lock": "^1.4.2", "@types/bcrypt": "^5.0.0", @@ -118,7 +118,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^22.15.21", + "@types/node": "^22.15.29", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^4.0.0", "@types/pngjs": "^6.0.5", @@ -131,7 +131,7 @@ "eslint": "^9.14.0", "eslint-config-prettier": "^10.0.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "globals": "^16.0.0", "jsdom": "^26.1.0", "mock-fs": "^5.2.0", @@ -145,7 +145,7 @@ "source-map-support": "^0.5.21", "sql-formatter": "^15.0.0", "supertest": "^7.1.0", - "testcontainers": "^10.18.0", + "testcontainers": "^11.0.0", "tsconfig-paths": "^4.2.0", "typescript": "^5.3.3", "typescript-eslint": "^8.28.0", diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts index c51ad8e06a..9bda1fcada 100644 --- a/server/src/controllers/search.controller.ts +++ b/server/src/controllers/search.controller.ts @@ -11,8 +11,10 @@ import { SearchPeopleDto, SearchPlacesDto, SearchResponseDto, + SearchStatisticsResponseDto, SearchSuggestionRequestDto, SmartSearchDto, + StatisticsSearchDto, } from 'src/dtos/search.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { SearchService } from 'src/services/search.service'; @@ -29,6 +31,13 @@ export class SearchController { return this.service.searchMetadata(auth, dto); } + @Post('statistics') + @HttpCode(HttpStatus.OK) + @Authenticated() + searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise { + return this.service.searchStatistics(auth, dto); + } + @Post('random') @HttpCode(HttpStatus.OK) @Authenticated() diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 579cba680e..d0427ef322 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -37,12 +37,6 @@ class BaseSearchDto { @ValidateAssetVisibility({ optional: true }) visibility?: AssetVisibility; - @ValidateBoolean({ optional: true }) - withDeleted?: boolean; - - @ValidateBoolean({ optional: true }) - withExif?: boolean; - @ValidateDate({ optional: true }) createdBefore?: Date; @@ -92,13 +86,6 @@ class BaseSearchDto { @Optional({ nullable: true, emptyToNull: true }) lensModel?: string | null; - @IsInt() - @Min(1) - @Max(1000) - @Type(() => Number) - @Optional() - size?: number; - @ValidateBoolean({ optional: true }) isNotInAlbum?: boolean; @@ -108,6 +95,9 @@ class BaseSearchDto { @ValidateUUID({ each: true, optional: true }) tagIds?: string[]; + @ValidateUUID({ each: true, optional: true }) + albumIds?: string[]; + @Optional() @IsInt() @Max(5) @@ -115,7 +105,22 @@ class BaseSearchDto { rating?: number; } -export class RandomSearchDto extends BaseSearchDto { +class BaseSearchWithResultsDto extends BaseSearchDto { + @ValidateBoolean({ optional: true }) + withDeleted?: boolean; + + @ValidateBoolean({ optional: true }) + withExif?: boolean; + + @IsInt() + @Min(1) + @Max(1000) + @Type(() => Number) + @Optional() + size?: number; +} + +export class RandomSearchDto extends BaseSearchWithResultsDto { @ValidateBoolean({ optional: true }) withStacked?: boolean; @@ -179,7 +184,14 @@ export class MetadataSearchDto extends RandomSearchDto { page?: number; } -export class SmartSearchDto extends BaseSearchDto { +export class StatisticsSearchDto extends BaseSearchDto { + @IsString() + @IsNotEmpty() + @Optional() + description?: string; +} + +export class SmartSearchDto extends BaseSearchWithResultsDto { @IsString() @IsNotEmpty() query!: string; @@ -299,6 +311,11 @@ export class SearchResponseDto { assets!: SearchAssetResponseDto; } +export class SearchStatisticsResponseDto { + @ApiProperty({ type: 'integer' }) + total!: number; +} + class SearchExploreItem { value!: string; data!: AssetResponseDto; diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 806fdb1c70..b68e0e177e 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -20,6 +20,20 @@ limit offset $7 +-- SearchRepository.searchStatistics +select + count(*) as "total" +from + "assets" + inner join "exif" on "assets"."id" = "exif"."assetId" +where + "assets"."visibility" = $1 + and "assets"."fileCreatedAt" >= $2 + and "exif"."lensModel" = $3 + and "assets"."ownerId" = any ($4::uuid[]) + and "assets"."isFavorite" = $5 + and "assets"."deletedAt" is null + -- SearchRepository.searchRandom ( select diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 747a59c65b..068de1eb4d 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -91,6 +91,10 @@ export interface SearchTagOptions { tagIds?: string[]; } +export interface SearchAlbumOptions { + albumIds?: string[]; +} + export interface SearchOrderOptions { orderDirection?: 'asc' | 'desc'; } @@ -108,7 +112,8 @@ type BaseAssetSearchOptions = SearchDateOptions & SearchStatusOptions & SearchUserIdOptions & SearchPeopleOptions & - SearchTagOptions; + SearchTagOptions & + SearchAlbumOptions; export type AssetSearchOptions = BaseAssetSearchOptions & SearchRelationOptions; @@ -185,6 +190,7 @@ export class SearchRepository { async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) { const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection; const items = await searchAssetBuilder(this.db, options) + .selectAll('assets') .orderBy('assets.fileCreatedAt', orderDirection) .limit(pagination.size + 1) .offset((pagination.page - 1) * pagination.size) @@ -193,6 +199,22 @@ export class SearchRepository { return paginationHelper(items, pagination.size); } + @GenerateSql({ + params: [ + { + takenAfter: DummyValue.DATE, + lensModel: DummyValue.STRING, + isFavorite: true, + userIds: [DummyValue.UUID], + }, + ], + }) + searchStatistics(options: AssetSearchOptions) { + return searchAssetBuilder(this.db, options) + .select((qb) => qb.fn.countAll().as('total')) + .executeTakeFirstOrThrow(); + } + @GenerateSql({ params: [ 100, @@ -209,10 +231,12 @@ export class SearchRepository { const uuid = randomUUID(); const builder = searchAssetBuilder(this.db, options); const lessThan = builder + .selectAll('assets') .where('assets.id', '<', uuid) .orderBy(sql`random()`) .limit(size); const greaterThan = builder + .selectAll('assets') .where('assets.id', '>', uuid) .orderBy(sql`random()`) .limit(size); @@ -241,6 +265,7 @@ export class SearchRepository { return this.db.transaction().execute(async (trx) => { await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); const items = await searchAssetBuilder(trx, options) + .selectAll('assets') .innerJoin('smart_search', 'assets.id', 'smart_search.assetId') .orderBy(sql`smart_search.embedding <=> ${options.embedding}`) .limit(pagination.size + 1) diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 3f122b5e74..73678f05af 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -10,9 +10,11 @@ import { SearchPeopleDto, SearchPlacesDto, SearchResponseDto, + SearchStatisticsResponseDto, SearchSuggestionRequestDto, SearchSuggestionType, SmartSearchDto, + StatisticsSearchDto, } from 'src/dtos/search.dto'; import { AssetOrder, AssetVisibility } from 'src/enum'; import { BaseService } from 'src/services/base.service'; @@ -67,6 +69,15 @@ export class SearchService extends BaseService { return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); } + async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise { + const userIds = await this.getUserIdsToSearch(auth); + + return await this.searchRepository.searchStatistics({ + ...dto, + userIds, + }); + } + async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise { if (dto.visibility === AssetVisibility.LOCKED) { requireElevatedPermission(auth); diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 5e5c6c5fb4..d7417becf1 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -228,6 +228,20 @@ export function hasPeople(qb: SelectQueryBuilder, personIds: ); } +export function inAlbums(qb: SelectQueryBuilder, albumIds: string[]) { + return qb.innerJoin( + (eb) => + eb + .selectFrom('albums_assets_assets') + .select('assetsId') + .where('albumsId', '=', anyUuid(albumIds!)) + .groupBy('assetsId') + .having((eb) => eb.fn.count('albumsId').distinct(), '=', albumIds.length) + .as('has_album'), + (join) => join.onRef('has_album.assetsId', '=', 'assets.id'), + ); +} + export function hasTags(qb: SelectQueryBuilder, tagIds: string[]) { return qb.innerJoin( (eb) => @@ -291,8 +305,8 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild return kysely .withPlugin(joinDeduplicationPlugin) .selectFrom('assets') - .selectAll('assets') .where('assets.visibility', '=', visibility) + .$if(!!options.albumIds && options.albumIds.length > 0, (qb) => inAlbums(qb, options.albumIds!)) .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) .$if(!!options.createdBefore, (qb) => qb.where('assets.createdAt', '<=', options.createdBefore!)) @@ -369,7 +383,7 @@ export function searchAssetBuilder(kysely: Kysely, options: AssetSearchBuild .$if(options.isMotion !== undefined, (qb) => qb.where('assets.livePhotoVideoId', options.isMotion ? 'is not' : 'is', null), ) - .$if(!!options.isNotInAlbum, (qb) => + .$if(!!options.isNotInAlbum && (!options.albumIds || options.albumIds.length === 0), (qb) => qb.where((eb) => eb.not(eb.exists((eb) => eb.selectFrom('albums_assets_assets').whereRef('assetsId', '=', 'assets.id'))), ), diff --git a/typescript-open-api/typescript-sdk/package-lock.json b/typescript-open-api/typescript-sdk/package-lock.json deleted file mode 100644 index ca6fc5e1de..0000000000 --- a/typescript-open-api/typescript-sdk/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "typescript-sdk", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/web/package-lock.json b/web/package-lock.json index ef3b105660..034604130e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -64,14 +64,14 @@ "eslint-config-prettier": "^10.0.0", "eslint-p": "^0.23.0", "eslint-plugin-svelte": "^3.9.0", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "factory.ts": "^1.4.1", "globals": "^16.0.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", - "rollup-plugin-visualizer": "^5.14.0", + "rollup-plugin-visualizer": "^6.0.0", "svelte": "^5.25.3", "svelte-check": "^4.1.5", "tailwindcss": "^4.1.7", @@ -2915,13 +2915,6 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/pbf": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", @@ -3663,9 +3656,9 @@ "license": "MIT" }, "node_modules/builtin-modules": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz", - "integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", "dev": true, "license": "MIT", "engines": { @@ -4759,50 +4752,65 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "57.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz", - "integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==", + "version": "59.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz", + "integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", - "@eslint-community/eslint-utils": "^4.4.1", - "ci-info": "^4.1.0", + "@eslint-community/eslint-utils": "^4.5.1", + "@eslint/plugin-kit": "^0.2.7", + "ci-info": "^4.2.0", "clean-regexp": "^1.0.0", - "core-js-compat": "^3.40.0", + "core-js-compat": "^3.41.0", "esquery": "^1.6.0", - "globals": "^15.15.0", + "find-up-simple": "^1.0.1", + "globals": "^16.0.0", "indent-string": "^5.0.0", - "is-builtin-module": "^4.0.0", + "is-builtin-module": "^5.0.0", "jsesc": "^3.1.0", "pluralize": "^8.0.0", - "read-package-up": "^11.0.0", "regexp-tree": "^0.1.27", "regjsparser": "^0.12.0", "semver": "^7.7.1", "strip-indent": "^4.0.0" }, "engines": { - "node": ">=18.18" + "node": "^18.20.0 || ^20.10.0 || >=21.0.0" }, "funding": { "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" }, "peerDependencies": { - "eslint": ">=9.20.0" + "eslint": ">=9.22.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.15.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", - "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz", + "integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz", + "integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.13.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/eslint-scope": { @@ -5589,19 +5597,6 @@ "node": ">= 0.4" } }, - "node_modules/hosted-git-info": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", - "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^10.0.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -5744,19 +5739,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/index-to-position": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz", - "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -5820,13 +5802,13 @@ "license": "MIT" }, "node_modules/is-builtin-module": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz", - "integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", "dev": true, "license": "MIT", "dependencies": { - "builtin-modules": "^4.0.0" + "builtin-modules": "^5.0.0" }, "engines": { "node": ">=18.20" @@ -7063,21 +7045,6 @@ "node": ">=6" } }, - "node_modules/normalize-package-data": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", - "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^7.0.0", - "semver": "^7.3.5", - "validate-npm-package-license": "^3.0.4" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, "node_modules/normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", @@ -7226,24 +7193,6 @@ "node": ">=6" } }, - "node_modules/parse-json": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", - "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "index-to-position": "^1.1.0", - "type-fest": "^4.39.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse5": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", @@ -7715,44 +7664,6 @@ "dev": true, "license": "MIT" }, - "node_modules/read-package-up": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", - "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up-simple": "^1.0.0", - "read-pkg": "^9.0.0", - "type-fest": "^4.6.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", - "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.3", - "normalize-package-data": "^6.0.0", - "parse-json": "^8.0.0", - "type-fest": "^4.6.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -7972,13 +7883,13 @@ } }, "node_modules/rollup-plugin-visualizer": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.14.0.tgz", - "integrity": "sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-6.0.3.tgz", + "integrity": "sha512-ZU41GwrkDcCpVoffviuM9Clwjy5fcUxlz0oMoTXTYsK+tcIFzbdacnrr2n8TXcHxbGKKXtOdjxM2HUS4HjkwIw==", "dev": true, "license": "MIT", "dependencies": { - "open": "^8.4.0", + "open": "^8.0.0", "picomatch": "^4.0.2", "source-map": "^0.7.4", "yargs": "^17.5.1" @@ -7990,7 +7901,7 @@ "node": ">=18" }, "peerDependencies": { - "rolldown": "1.x", + "rolldown": "1.x || ^1.0.0-beta", "rollup": "2.x || 3.x || 4.x" }, "peerDependenciesMeta": { @@ -8449,42 +8360,6 @@ "source-map": "^0.6.0" } }, - "node_modules/spdx-correct": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", - "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-exceptions": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", - "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", - "dev": true, - "license": "CC-BY-3.0" - }, - "node_modules/spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "node_modules/spdx-license-ids": { - "version": "3.0.21", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", - "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -9183,19 +9058,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "4.40.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz", - "integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.8.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", @@ -9253,19 +9115,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", @@ -9335,17 +9184,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, "node_modules/vite": { "version": "6.3.5", "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", diff --git a/web/package.json b/web/package.json index c1fd724704..0aa70a4b73 100644 --- a/web/package.json +++ b/web/package.json @@ -81,14 +81,14 @@ "eslint-config-prettier": "^10.0.0", "eslint-p": "^0.23.0", "eslint-plugin-svelte": "^3.9.0", - "eslint-plugin-unicorn": "^57.0.0", + "eslint-plugin-unicorn": "^59.0.0", "factory.ts": "^1.4.1", "globals": "^16.0.0", "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-svelte": "^3.3.3", - "rollup-plugin-visualizer": "^5.14.0", + "rollup-plugin-visualizer": "^6.0.0", "svelte": "^5.25.3", "svelte-check": "^4.1.5", "tailwindcss": "^4.1.7", diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts index fd37d253aa..d823f17df4 100644 --- a/web/src/lib/components/asset-viewer/actions/action.ts +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -1,6 +1,6 @@ import type { AssetAction } from '$lib/constants'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; -import type { AlbumResponseDto } from '@immich/sdk'; +import type { AlbumResponseDto, StackResponseDto } from '@immich/sdk'; type ActionMap = { [AssetAction.ARCHIVE]: { asset: TimelineAsset }; @@ -14,6 +14,7 @@ type ActionMap = { [AssetAction.ADD_TO_ALBUM]: { asset: TimelineAsset; album: AlbumResponseDto }; [AssetAction.UNSTACK]: { assets: TimelineAsset[] }; [AssetAction.KEEP_THIS_DELETE_OTHERS]: { asset: TimelineAsset }; + [AssetAction.SET_STACK_PRIMARY_ASSET]: { stack: StackResponseDto }; [AssetAction.SET_VISIBILITY_LOCKED]: { asset: TimelineAsset }; [AssetAction.SET_VISIBILITY_TIMELINE]: { asset: TimelineAsset }; }; diff --git a/web/src/lib/components/asset-viewer/actions/set-stack-primary-asset.svelte b/web/src/lib/components/asset-viewer/actions/set-stack-primary-asset.svelte new file mode 100644 index 0000000000..6ca1548f4b --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-stack-primary-asset.svelte @@ -0,0 +1,26 @@ + + + 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 e562f60319..a376c37139 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 @@ -13,6 +13,7 @@ import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte'; import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte'; import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte'; + import SetStackPrimaryAsset from '$lib/components/asset-viewer/actions/set-stack-primary-asset.svelte'; import SetVisibilityAction from '$lib/components/asset-viewer/actions/set-visibility-action.svelte'; import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte'; import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte'; @@ -192,6 +193,9 @@ {#if stack} + {#if stack?.primaryAssetId !== asset.id} + + {/if} {/if} {#if album} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index e5d2554e2c..a448f96c32 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -338,7 +338,10 @@ await handleGetAllAlbums(); break; } - + case AssetAction.SET_STACK_PRIMARY_ASSET: { + stack = action.stack; + break; + } case AssetAction.KEEP_THIS_DELETE_OTHERS: case AssetAction.UNSTACK: { closeViewer(); diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte index 39afc1e506..2ce6d3d17f 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -67,7 +67,7 @@ {/each} diff --git a/web/src/lib/components/shared-components/tree/tree-items.svelte b/web/src/lib/components/shared-components/tree/tree-items.svelte index 0df71ca605..9c2fb6638e 100644 --- a/web/src/lib/components/shared-components/tree/tree-items.svelte +++ b/web/src/lib/components/shared-components/tree/tree-items.svelte @@ -1,28 +1,21 @@
    - - {#each Object.entries(items).sort() as [path, tree]} - {@const value = normalizeTreePath(`${parent}/${path}`)} - {@const key = value + getColor(value)} - {#key key} -
  • - -
  • - {/key} + {#each tree.children as node (node.color ? node.path + node.color : node.path)} +
  • + +
  • {/each}
diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte index 25c40f096e..6311b69831 100644 --- a/web/src/lib/components/shared-components/tree/tree.svelte +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -1,25 +1,20 @@ - -
+ {#if node.size > 0} + + {/if} +
- {value} + {node.value}
{#if isOpen} - + {/if} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index ae53b4e7f3..c27727d4b3 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -10,6 +10,7 @@ export enum AssetAction { ADD_TO_ALBUM = 'add-to-album', UNSTACK = 'unstack', KEEP_THIS_DELETE_OTHERS = 'keep-this-delete-others', + SET_STACK_PRIMARY_ASSET = 'set-stack-primary-asset', SET_VISIBILITY_LOCKED = 'set-visibility-locked', SET_VISIBILITY_TIMELINE = 'set-visibility-timeline', } diff --git a/web/src/lib/modals/AssetTagModal.svelte b/web/src/lib/modals/AssetTagModal.svelte index 2a61f0b945..2daf17dd97 100644 --- a/web/src/lib/modals/AssetTagModal.svelte +++ b/web/src/lib/modals/AssetTagModal.svelte @@ -66,6 +66,7 @@ defaultFirstOption options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} placeholder={$t('search_tags')} + forceFocus />
diff --git a/web/src/lib/stores/folders.svelte.ts b/web/src/lib/stores/folders.svelte.ts index 6f3eb8b66a..f77b67bb7c 100644 --- a/web/src/lib/stores/folders.svelte.ts +++ b/web/src/lib/stores/folders.svelte.ts @@ -1,4 +1,5 @@ import { eventManager } from '$lib/managers/event-manager.svelte'; +import { TreeNode } from '$lib/utils/tree-utils'; import { getAssetsByOriginalPath, getUniqueOriginalPaths, @@ -13,47 +14,41 @@ type AssetCache = { }; class FoldersStore { + folders = $state.raw(null); private initialized = false; - uniquePaths = $state([]); - assets = $state({}); + private assets = $state({}); constructor() { eventManager.on('auth.logout', () => this.clearCache()); } - async fetchUniquePaths() { + async fetchTree(): Promise { if (this.initialized) { - return; + return this.folders!; } this.initialized = true; - const uniquePaths = await getUniqueOriginalPaths(); - this.uniquePaths.push(...uniquePaths); + this.folders = TreeNode.fromPaths(await getUniqueOriginalPaths()); + this.folders.collapse(); + return this.folders; } bustAssetCache() { this.assets = {}; } - async refreshAssetsByPath(path: string | null) { - if (!path) { - return; - } - this.assets[path] = await getAssetsByOriginalPath({ path }); + async refreshAssetsByPath(path: string) { + return (this.assets[path] = await getAssetsByOriginalPath({ path })); } async fetchAssetsByPath(path: string) { - if (this.assets[path]) { - return; - } - - this.assets[path] = await getAssetsByOriginalPath({ path }); + return (this.assets[path] ??= await getAssetsByOriginalPath({ path })); } clearCache() { this.initialized = false; - this.uniquePaths = []; this.assets = {}; + this.folders = null; } } diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index c3b4d83f38..89e1f9f5f0 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -23,7 +23,7 @@ export const isAssetViewerRoute = (target?: NavigationTarget | null) => !!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {})); export function getAssetInfoFromParam({ assetId, key }: { assetId?: string; key?: string }) { - return assetId && getAssetInfo({ id: assetId, key }); + return assetId ? getAssetInfo({ id: assetId, key }) : undefined; } function currentUrlWithoutAsset() { diff --git a/web/src/lib/utils/tree-utils.ts b/web/src/lib/utils/tree-utils.ts index 5a6e917079..267bb2eec7 100644 --- a/web/src/lib/utils/tree-utils.ts +++ b/web/src/lib/utils/tree-utils.ts @@ -1,21 +1,144 @@ -export interface RecursiveObject { - [key: string]: RecursiveObject; -} +/* eslint-disable @typescript-eslint/no-this-alias */ +/* eslint-disable unicorn/no-this-assignment */ +/* eslint-disable unicorn/prefer-at */ +import type { TagResponseDto } from '@immich/sdk'; -export const normalizeTreePath = (path: string) => path.replace(/^\//, '').replace(/\/$/, ''); +export class TreeNode extends Map { + value: string; + path: string; + parent: TreeNode | null; + hasAssets: boolean; + id: string | undefined; + color: string | undefined; + private _parents: TreeNode[] | undefined; + private _children: TreeNode[] | undefined; -export function buildTree(paths: string[]) { - const root: RecursiveObject = {}; + private constructor(value: string, path: string, parent: TreeNode | null) { + super(); + this.value = value; + this.parent = parent; + this.path = path; + this.hasAssets = false; + } - for (const path of paths) { - const parts = path.split('/'); - let current = root; + static fromPaths(paths: string[]) { + const root = new TreeNode('', '', null); + for (const path of paths) { + const current = root.add(path); + current.hasAssets = true; + } + return root; + } + + static fromTags(tags: TagResponseDto[]) { + const root = new TreeNode('', '', null); + for (const tag of tags) { + const current = root.add(tag.value); + current.hasAssets = true; + current.id = tag.id; + current.color = tag.color; + } + return root; + } + + traverse(path: string) { + const parts = getPathParts(path); + let current: TreeNode = this; + let curPart = null; for (const part of parts) { - if (!current[part]) { - current[part] = {}; + // segments common to all subtrees can be collapsed together + curPart = curPart === null ? part : joinPaths(curPart, part); + const next = current.get(curPart); + if (next) { + current = next; + curPart = null; } - current = current[part]; + } + return current; + } + + collapse() { + if (this.size === 1 && !this.hasAssets && this.parent !== null) { + const child = this.values().next().value!; + child.value = joinPaths(this.value, child.value); + child.parent = this.parent; + this.parent.delete(this.value); + this.parent.set(child.value, child); + } + + for (const child of this.values()) { + child.collapse(); } } - return root; + + private add(path: string) { + let current: TreeNode = this; + for (const part of getPathParts(path)) { + let next = current.get(part); + if (next === undefined) { + next = new TreeNode(part, joinPaths(current.path, part), current); + current.set(part, next); + } + current = next; + } + return current; + } + + get parents(): TreeNode[] { + if (this._parents) { + return this._parents; + } + const parents: TreeNode[] = []; + let current: TreeNode | null = this.parent; + while (current !== null && current.parent !== null) { + parents.push(current); + current = current.parent; + } + return (this._parents = parents.reverse()); + } + + get children(): TreeNode[] { + return (this._children ??= Array.from(this.values())); + } +} + +export const normalizeTreePath = (path: string) => + path.length > 1 && path[path.length - 1] === '/' ? path.slice(0, -1) : path; + +export function getPathParts(path: string) { + const parts = path.split('/'); + if (path[0] === '/') { + parts[0] = '/'; + } + + if (path[path.length - 1] === '/') { + parts.pop(); + } + + return parts; +} + +export function joinPaths(path1: string, path2: string) { + if (!path1) { + return path2; + } + + if (!path2) { + return path1; + } + + if (path1[path1.length - 1] === '/') { + return path1 + path2; + } + + return path1 + '/' + path2; +} + +export function getParentPath(path: string) { + const normalized = normalizeTreePath(path); + const last = normalized.lastIndexOf('/'); + if (last > 0) { + return normalized.slice(0, last); + } + return last === 0 ? '/' : normalized; } diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 89df31562e..84ae328ccc 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,6 +1,5 @@ @@ -99,8 +86,8 @@
@@ -108,10 +95,10 @@ {/snippet} - +
- + {#if data.pathAssets && data.pathAssets.length > 0} diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts index 7fd0a749c0..72fb102e53 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -3,38 +3,28 @@ import { foldersStore } from '$lib/stores/folders.svelte'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; -import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import type { PageLoad } from './$types'; export const load = (async ({ params, url }) => { await authenticate(url); - const asset = await getAssetInfoFromParam(params); - const $t = await getFormatter(); - - await foldersStore.fetchUniquePaths(); - - let pathAssets = null; + const [, asset, $t] = await Promise.all([foldersStore.fetchTree(), getAssetInfoFromParam(params), getFormatter()]); + let tree = foldersStore.folders!; const path = url.searchParams.get(QueryParameter.PATH); if (path) { - await foldersStore.fetchAssetsByPath(path); - pathAssets = foldersStore.assets[path] || null; - } else { - // If no path is provided, we we're at the root level + tree = tree.traverse(path); + } else if (path === null) { + // If no path is provided, we've just navigated to the folders page. // We should bust the asset cache of the folder store, to make sure we don't show stale data foldersStore.bustAssetCache(); } - let tree = buildTree(foldersStore.uniquePaths); - const parts = normalizeTreePath(path || '').split('/'); - for (const part of parts) { - tree = tree?.[part]; - } + // only fetch assets if the folder has assets + const pathAssets = tree.hasAssets ? await foldersStore.fetchAssetsByPath(tree.path) : null; return { asset, - path, - currentFolders: Object.keys(tree || {}).sort(), + tree, pathAssets, meta: { title: $t('folders'), diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index a42c6534a7..e344c448b0 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,6 +1,5 @@