diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 3cfaf0b2f2..8b89cba107 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -1,4 +1,4 @@ -name: Clean up actions cache on PR close +name: Cache Cleanup on: pull_request: types: @@ -10,6 +10,7 @@ concurrency: jobs: cleanup: + name: Cleanup runs-on: ubuntu-latest steps: - name: Check out code diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index ac80b5388b..c66dcfb3cf 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -1,9 +1,10 @@ -name: Publish Package to npmjs +name: CLI Release on: workflow_dispatch: jobs: publish: + name: Publish runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index 6de84423fe..a49ba55912 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -5,7 +5,7 @@ # # This workflow will not trigger runs on forked repos. -name: Cleanup Old Docker Images +name: Docker Cleanup on: pull_request: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 2e88f9883f..4bebc75028 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,4 +1,4 @@ -name: Build and Push Docker Images +name: Docker on: workflow_dispatch: @@ -18,6 +18,7 @@ permissions: jobs: build_and_push: + name: Build and Push runs-on: ubuntu-latest strategy: # Prevent a failure in one image from stopping the other builds diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aee1ec7882..a2f946f029 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ concurrency: jobs: e2e-tests: - name: Run end-to-end test suites + name: Server (e2e) runs-on: ubuntu-latest steps: @@ -24,7 +24,7 @@ jobs: run: docker compose -f ./docker/docker-compose.test.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build doc-tests: - name: Run documentation checks + name: Docs runs-on: ubuntu-latest defaults: run: @@ -45,8 +45,12 @@ jobs: run: npm run check if: ${{ !cancelled() }} + - name: Run build + run: npm run build + if: ${{ !cancelled() }} + server-unit-tests: - name: Run server unit test suites and checks + name: Server runs-on: ubuntu-latest defaults: run: @@ -76,7 +80,7 @@ jobs: if: ${{ !cancelled() }} cli-unit-tests: - name: Run cli test suites + name: CLI runs-on: ubuntu-latest defaults: run: @@ -106,7 +110,7 @@ jobs: if: ${{ !cancelled() }} web-unit-tests: - name: Run web unit test suites and checks + name: Web runs-on: ubuntu-latest defaults: run: @@ -140,7 +144,7 @@ jobs: # if: ${{ !cancelled() }} mobile-unit-tests: - name: Run mobile unit tests + name: Mobile runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -154,7 +158,7 @@ jobs: run: flutter test -j 1 ml-unit-tests: - name: Run ML unit tests and checks + name: Machine Learning runs-on: ubuntu-latest defaults: run: @@ -184,7 +188,7 @@ jobs: poetry run pytest --cov app generated-api-up-to-date: - name: Check generated files are up-to-date + name: OpenAPI Clients runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -205,11 +209,11 @@ jobs: exit 1 generated-typeorm-migrations-up-to-date: - name: Check generated TypeORM migrations are up-to-date + name: TypeORM Migrations runs-on: ubuntu-latest services: postgres: - image: postgres + image: postgres@sha256:71da05df8c4f1e1bac9b92ebfba2a0eeb183f6ac6a972fd5e55e8146e29efe9c env: POSTGRES_PASSWORD: postgres POSTGRES_USER: postgres diff --git a/cli/package-lock.json b/cli/package-lock.json index 4547cfc3c1..e51f03cfa8 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -29,8 +29,8 @@ "@types/mime-types": "^2.1.1", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.48.1", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "chai": "^4.3.7", "eslint": "^8.43.0", "eslint-config-prettier": "^9.0.0", @@ -46,7 +46,7 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "typescript": "^4.9.4" + "typescript": "^5.0.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1604,9 +1604,9 @@ } }, "node_modules/@types/node": { - "version": "20.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", - "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==", + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1646,32 +1646,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1679,26 +1680,126 @@ } } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "debug": "^4.3.4" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1706,6 +1807,80 @@ } } }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/scope-manager": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", @@ -1724,25 +1899,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1750,6 +1925,105 @@ } } }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@typescript-eslint/types": { "version": "5.62.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", @@ -4864,12 +5138,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -6023,6 +6291,18 @@ "node": ">=8.0" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -6170,16 +6450,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/undici-types": { @@ -7710,9 +7990,9 @@ } }, "@types/node": { - "version": "20.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.4.tgz", - "integrity": "sha512-wmyg8HUhcn6ACjsn8oKYjkN/zUzQeNtMy44weTJSM6p4MMzEOuKbA3OjJ267uPCOW7Xex9dyrNTful8XTQYoDA==", + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -7752,33 +8032,136 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "requires": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + } + }, + "@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + } + } } }, "@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + } + }, + "@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + } + } } }, "@typescript-eslint/scope-manager": { @@ -7792,15 +8175,73 @@ } }, "@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" + }, + "dependencies": { + "@typescript-eslint/scope-manager": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" + } + }, + "@typescript-eslint/types": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" + } + } } }, "@typescript-eslint/types": { @@ -10035,12 +10476,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10865,6 +11300,13 @@ "is-number": "^7.0.0" } }, + "ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "requires": {} + }, "ts-jest": { "version": "29.1.1", "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.1.1.tgz", @@ -10947,9 +11389,9 @@ "dev": true }, "typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true }, "undici-types": { diff --git a/cli/package.json b/cli/package.json index c1fe546d73..a1550f7b95 100644 --- a/cli/package.json +++ b/cli/package.json @@ -29,8 +29,8 @@ "@types/mime-types": "^2.1.1", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", - "@typescript-eslint/eslint-plugin": "^5.60.1", - "@typescript-eslint/parser": "^5.48.1", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "chai": "^4.3.7", "eslint": "^8.43.0", "eslint-config-prettier": "^9.0.0", @@ -46,7 +46,7 @@ "ts-jest": "^29.1.0", "ts-node": "^10.9.1", "tslib": "^2.5.3", - "typescript": "^4.9.4" + "typescript": "^5.0.0" }, "scripts": { "build": "tsc --project tsconfig.build.json", diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 6bbe321aa0..fa714fbf75 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -1182,22 +1182,6 @@ export interface CheckExistingAssetsResponseDto { */ 'existingIds': Array; } -/** - * - * @export - * @enum {string} - */ - -export const CitiesFile = { - Cities15000: 'cities15000', - Cities5000: 'cities5000', - Cities1000: 'cities1000', - Cities500: 'cities500' -} as const; - -export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile]; - - /** * * @export @@ -3850,12 +3834,6 @@ export interface SystemConfigPasswordLoginDto { * @interface SystemConfigReverseGeocodingDto */ export interface SystemConfigReverseGeocodingDto { - /** - * - * @type {CitiesFile} - * @memberof SystemConfigReverseGeocodingDto - */ - 'citiesFileOverride': CitiesFile; /** * * @type {boolean} @@ -3863,8 +3841,6 @@ export interface SystemConfigReverseGeocodingDto { */ 'enabled': boolean; } - - /** * * @export @@ -6844,6 +6820,48 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deviceId' is not null or undefined + assertParamExists('getAllUserAssetsByDeviceId', 'deviceId', deviceId) + const localVarPath = `/asset/device/{deviceId}` + .replace(`{${"deviceId"}}`, encodeURIComponent(String(deviceId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -7513,9 +7531,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { @@ -8347,6 +8367,16 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllUserAssetsByDeviceId(deviceId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Get a single asset\'s information * @param {string} id @@ -8494,9 +8524,11 @@ export const AssetApiFp = function(configuration?: Configuration) { return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { @@ -8722,6 +8754,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); + }, /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -8828,9 +8869,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId(requestParameters: AssetApiGetUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { @@ -9066,6 +9109,20 @@ export interface AssetApiGetAllAssetsRequest { readonly ifNoneMatch?: string } +/** + * Request parameters for getAllUserAssetsByDeviceId operation in AssetApi. + * @export + * @interface AssetApiGetAllUserAssetsByDeviceIdRequest + */ +export interface AssetApiGetAllUserAssetsByDeviceIdRequest { + /** + * + * @type {string} + * @memberof AssetApiGetAllUserAssetsByDeviceId + */ + readonly deviceId: string +} + /** * Request parameters for getAssetById operation in AssetApi. * @export @@ -10010,6 +10067,17 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -10140,9 +10208,11 @@ export class AssetApi extends BaseAPI { } /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AssetApi */ diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index f988616733..0c9de8d348 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -59,7 +59,7 @@ services: build: context: ../web dockerfile: Dockerfile - command: npm run dev --host + command: "node ./node_modules/.bin/vite dev --host 0.0.0.0 --port 3000" env_file: - .env ports: diff --git a/docs/docs/guides/docker-help.md b/docs/docs/guides/docker-help.md index 7ea0f6d674..37fdf88cba 100644 --- a/docs/docs/guides/docker-help.md +++ b/docs/docs/guides/docker-help.md @@ -14,8 +14,6 @@ docker exec -it # attach to a container with a c docker exec -it immich_server sh docker exec -it immich_microservices sh docker exec -it immich_machine_learning sh -docker exec -it immich_web sh -docker exec -it immich_proxy sh ``` ## Logs @@ -26,8 +24,6 @@ docker logs # see the logs for a specific container (by id docker logs immich_server docker logs immich_microservices docker logs immich_machine_learning -docker logs immich_web -docker logs immich_proxy ``` :::tip Follow a log diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md new file mode 100644 index 0000000000..b72bf5a032 --- /dev/null +++ b/docs/docs/guides/remote-access.md @@ -0,0 +1,58 @@ +# Remote Access + +This page gives a few pointers on how to access your Immich instance from outside your LAN. + +:::danger +Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you succeptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks. +::: + +## Option 1: VPN to home network + +You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/) + +### Pros: + +- Simple to set up and very secure. +- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk. +- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal. + +### Cons: + +- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider. +- VPN software needs to be installed and active on both server-side and client-side. +- Requires you to open a port on your router to your server. + +## Option 2: Tailscale + +If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation). + +### Pros + +- Minimal configuration needed on server and client sides. +- You are protected against zero-day vulnerabilities on Immich. + +### Cons + +- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022. +- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices. +- Tailscale needs to be installed and running on both server-side and client-side. + +## Option 3: Reverse Proxy + +A reverse proxy is a service that sits between web servers and clients. A reverse proxy can either be hosted on the server itself or remotely. Clients can connect to the reverse proxy via https, and the proxy relays data to Immich. This setup makes most sense if you have your own domain and want to access your Immich instance just like any other website, from outside your LAN. You can also use a DDNS provider like DuckDNS or no-ip if you don't have a domain. This configuration allows the Immich Android and iphone apps to connect to your server without a VPN or tailscale app on the client side. + +If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](https://immich.app/docs/administration/reverse-proxy). + +You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser. + +A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder. + +### Pros + +- No additional software needs to be installed client-side +- If you only need access to the web interface remotely, it is possible to set up access controls that shield you from zero-day vulnerabilities on Immich. [Cloudflare Access](https://www.cloudflare.com/zero-trust/products/access/) has a generous free tier. + +### Cons + +- Complex configuration +- Depending on your configuration, both the Immich web interface and API may be exposed to the internet. Immich is under very active developement and the existence of severe security vulnerabilities cannot be ruled out. diff --git a/docs/docs/overview/support-the-project.md b/docs/docs/overview/support-the-project.md index 8819cdafd8..7bd473eb1e 100644 --- a/docs/docs/overview/support-the-project.md +++ b/docs/docs/overview/support-the-project.md @@ -12,8 +12,8 @@ If you feel like this is the right cause and the app is something you see yourse ## Donation -- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502) -- One-time donation via [GitHub Sponsors](https://github.com/sponsors/alextran1502?frequency=one-time&sponsor=alextran1502) +- Monthly donation via [GitHub Sponsors](https://github.com/sponsors/immich-app) +- One-time donation via [GitHub Sponsors](https://github.com/sponsors/immich-app?frequency=one-time) - [Librepay](https://liberapay.com/alex.tran1502/) - [buymeacoffee](https://www.buymeacoffee.com/altran1502) - Bitcoin: 1FvEp6P6NM8EZEkpGUFAN2LqJ1gxusNxZX diff --git a/docs/package-lock.json b/docs/package-lock.json index 565e81b742..c550a9debe 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -15,7 +15,7 @@ "@mdx-js/react": "^1.6.22", "autoprefixer": "^10.4.13", "classnames": "^2.3.2", - "clsx": "^1.2.1", + "clsx": "^2.0.0", "docusaurus-lunr-search": "^2.3.2", "docusaurus-preset-openapi": "^0.6.3", "postcss": "^8.4.25", @@ -28,7 +28,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", "@tsconfig/docusaurus": "^1.0.5", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "typescript": "^5.1.6" }, "engines": { @@ -2603,6 +2603,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/@docusaurus/theme-classic/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@docusaurus/theme-common": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-2.4.3.tgz", @@ -2633,6 +2641,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/@docusaurus/theme-common/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@docusaurus/theme-search-algolia": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-2.4.3.tgz", @@ -2663,6 +2679,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/@docusaurus/theme-search-algolia/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/@docusaurus/theme-translations": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-2.4.3.tgz", @@ -4948,9 +4972,9 @@ } }, "node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", "engines": { "node": ">=6" } @@ -5995,6 +6019,14 @@ "react-dom": "^16.8.4 || ^17" } }, + "node_modules/docusaurus-lunr-search/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/docusaurus-plugin-openapi": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.6.4.tgz", @@ -6025,6 +6057,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/docusaurus-plugin-openapi/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/docusaurus-plugin-openapi/node_modules/fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -6098,6 +6138,14 @@ "react-dom": "^16.8.4 || ^17.0.0" } }, + "node_modules/docusaurus-theme-openapi/node_modules/clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", + "engines": { + "node": ">=6" + } + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -10801,15 +10849,15 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" @@ -16745,6 +16793,13 @@ "rtlcss": "^3.5.0", "tslib": "^2.4.0", "utility-types": "^3.10.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "@docusaurus/theme-common": { @@ -16768,6 +16823,13 @@ "tslib": "^2.4.0", "use-sync-external-store": "^1.2.0", "utility-types": "^3.10.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "@docusaurus/theme-search-algolia": { @@ -16791,6 +16853,13 @@ "lodash": "^4.17.21", "tslib": "^2.4.0", "utility-types": "^3.10.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "@docusaurus/theme-translations": { @@ -18515,9 +18584,9 @@ } }, "clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==" }, "collapse-white-space": { "version": "1.0.6", @@ -19243,6 +19312,13 @@ "to-vfile": "^6.1.0", "unified": "^9.0.0", "unist-util-is": "^4.0.2" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "docusaurus-plugin-openapi": { @@ -19268,6 +19344,11 @@ "webpack": "^5.73.0" }, "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + }, "fs-extra": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", @@ -19321,6 +19402,13 @@ "react-redux": "^7.2.0", "redux-devtools-extension": "^2.13.8", "webpack": "^5.73.0" + }, + "dependencies": { + "clsx": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", + "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" + } } }, "dom-converter": { @@ -22663,9 +22751,9 @@ "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==" }, "prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true }, "pretty-error": { diff --git a/docs/package.json b/docs/package.json index 19c972fbb2..fa0b5b3e65 100644 --- a/docs/package.json +++ b/docs/package.json @@ -24,7 +24,7 @@ "@mdx-js/react": "^1.6.22", "autoprefixer": "^10.4.13", "classnames": "^2.3.2", - "clsx": "^1.2.1", + "clsx": "^2.0.0", "docusaurus-lunr-search": "^2.3.2", "docusaurus-preset-openapi": "^0.6.3", "postcss": "^8.4.25", @@ -37,7 +37,7 @@ "devDependencies": { "@docusaurus/module-type-aliases": "^2.4.1", "@tsconfig/docusaurus": "^1.0.5", - "prettier": "^2.8.8", + "prettier": "^3.0.0", "typescript": "^5.1.6" }, "browserslist": { diff --git a/docs/src/theme/SearchBar/algolia.css b/docs/src/theme/SearchBar/algolia.css index 8bea784b9b..66e696e96f 100644 --- a/docs/src/theme/SearchBar/algolia.css +++ b/docs/src/theme/SearchBar/algolia.css @@ -61,8 +61,12 @@ .searchbox__input { display: inline-block; box-sizing: border-box; - -webkit-transition: box-shadow 0.4s ease, background 0.4s ease; - transition: box-shadow 0.4s ease, background 0.4s ease; + -webkit-transition: + box-shadow 0.4s ease, + background 0.4s ease; + transition: + box-shadow 0.4s ease, + background 0.4s ease; border: 0; border-radius: 16px; box-shadow: inset 0 0 0 1px #cccccc; @@ -243,7 +247,9 @@ } .algolia-autocomplete .ds-dropdown-menu { - box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.2), 0 2px 3px 0 rgba(0, 0, 0, 0.1); + box-shadow: + 0 1px 0 0 rgba(0, 0, 0, 0.2), + 0 2px 3px 0 rgba(0, 0, 0, 0.1); } @media (min-width: 601px) { diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index d438553201..8d7f400d9f 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-bookworm as builder +FROM python:3.11-bookworm@sha256:e5a1b0a194a5fbf94f6e350b31c9a508723f9eeb2f9e9e32c3b65df8520a40cc as builder ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ @@ -13,7 +13,7 @@ ENV VIRTUAL_ENV="/opt/venv" PATH="/opt/venv/bin:${PATH}" COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --only main -FROM python:3.11-slim-bookworm +FROM python:3.11-slim-bookworm@sha256:1bc6a3e9356d64ea632791653bc71a56340e8741dab66434ab2739ebf6aed29d RUN apt-get update && apt-get install -y --no-install-recommends tini libmimalloc2.0 && rm -rf /var/lib/apt/lists/* diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 1c74546bc9..7bd04e0c26 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim as builder +FROM mambaorg/micromamba:bookworm-slim@sha256:d20c621f3ae42f50f380166b15b6c88b14fa62ab6ea188f2cef33451d64057c7 as builder ENV NODE_ENV=production \ TRANSFORMERS_CACHE=/cache \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 8edb14818f..e9ec397ff3 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. [[package]] name = "aiocache" @@ -585,68 +585,6 @@ files = [ test = ["PyYAML", "mock", "pytest"] yaml = ["PyYAML"] -[[package]] -name = "contourpy" -version = "1.1.0" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.8" -files = [ - {file = "contourpy-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:89f06eff3ce2f4b3eb24c1055a26981bffe4e7264acd86f15b97e40530b794bc"}, - {file = "contourpy-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dffcc2ddec1782dd2f2ce1ef16f070861af4fb78c69862ce0aab801495dda6a3"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25ae46595e22f93592d39a7eac3d638cda552c3e1160255258b695f7b58e5655"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17cfaf5ec9862bc93af1ec1f302457371c34e688fbd381f4035a06cd47324f48"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18a64814ae7bce73925131381603fff0116e2df25230dfc80d6d690aa6e20b37"}, - {file = "contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90c81f22b4f572f8a2110b0b741bb64e5a6427e0a198b2cdc1fbaf85f352a3aa"}, - {file = "contourpy-1.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53cc3a40635abedbec7f1bde60f8c189c49e84ac180c665f2cd7c162cc454baa"}, - {file = "contourpy-1.1.0-cp310-cp310-win32.whl", hash = "sha256:9b2dd2ca3ac561aceef4c7c13ba654aaa404cf885b187427760d7f7d4c57cff8"}, - {file = "contourpy-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:1f795597073b09d631782e7245016a4323cf1cf0b4e06eef7ea6627e06a37ff2"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0b7b04ed0961647691cfe5d82115dd072af7ce8846d31a5fac6c142dcce8b882"}, - {file = "contourpy-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:27bc79200c742f9746d7dd51a734ee326a292d77e7d94c8af6e08d1e6c15d545"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:052cc634bf903c604ef1a00a5aa093c54f81a2612faedaa43295809ffdde885e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9382a1c0bc46230fb881c36229bfa23d8c303b889b788b939365578d762b5c18"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5cec36c5090e75a9ac9dbd0ff4a8cf7cecd60f1b6dc23a374c7d980a1cd710e"}, - {file = "contourpy-1.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f0cbd657e9bde94cd0e33aa7df94fb73c1ab7799378d3b3f902eb8eb2e04a3a"}, - {file = "contourpy-1.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:181cbace49874f4358e2929aaf7ba84006acb76694102e88dd15af861996c16e"}, - {file = "contourpy-1.1.0-cp311-cp311-win32.whl", hash = "sha256:edb989d31065b1acef3828a3688f88b2abb799a7db891c9e282df5ec7e46221b"}, - {file = "contourpy-1.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb3b7d9e6243bfa1efb93ccfe64ec610d85cfe5aec2c25f97fbbd2e58b531256"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bcb41692aa09aeb19c7c213411854402f29f6613845ad2453d30bf421fe68fed"}, - {file = "contourpy-1.1.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5d123a5bc63cd34c27ff9c7ac1cd978909e9c71da12e05be0231c608048bb2ae"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62013a2cf68abc80dadfd2307299bfa8f5aa0dcaec5b2954caeb5fa094171103"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0b6616375d7de55797d7a66ee7d087efe27f03d336c27cf1f32c02b8c1a5ac70"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:317267d915490d1e84577924bd61ba71bf8681a30e0d6c545f577363157e5e94"}, - {file = "contourpy-1.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d551f3a442655f3dcc1285723f9acd646ca5858834efeab4598d706206b09c9f"}, - {file = "contourpy-1.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e7a117ce7df5a938fe035cad481b0189049e8d92433b4b33aa7fc609344aafa1"}, - {file = "contourpy-1.1.0-cp38-cp38-win32.whl", hash = "sha256:108dfb5b3e731046a96c60bdc46a1a0ebee0760418951abecbe0fc07b5b93b27"}, - {file = "contourpy-1.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:d4f26b25b4f86087e7d75e63212756c38546e70f2a92d2be44f80114826e1cd4"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc00bb4225d57bff7ebb634646c0ee2a1298402ec10a5fe7af79df9a51c1bfd9"}, - {file = "contourpy-1.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:189ceb1525eb0655ab8487a9a9c41f42a73ba52d6789754788d1883fb06b2d8a"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f2931ed4741f98f74b410b16e5213f71dcccee67518970c42f64153ea9313b9"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30f511c05fab7f12e0b1b7730ebdc2ec8deedcfb505bc27eb570ff47c51a8f15"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:143dde50520a9f90e4a2703f367cf8ec96a73042b72e68fcd184e1279962eb6f"}, - {file = "contourpy-1.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e94bef2580e25b5fdb183bf98a2faa2adc5b638736b2c0a4da98691da641316a"}, - {file = "contourpy-1.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ed614aea8462735e7d70141374bd7650afd1c3f3cb0c2dbbcbe44e14331bf002"}, - {file = "contourpy-1.1.0-cp39-cp39-win32.whl", hash = "sha256:71551f9520f008b2950bef5f16b0e3587506ef4f23c734b71ffb7b89f8721999"}, - {file = "contourpy-1.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:438ba416d02f82b692e371858143970ed2eb6337d9cdbbede0d8ad9f3d7dd17d"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a698c6a7a432789e587168573a864a7ea374c6be8d4f31f9d87c001d5a843493"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:397b0ac8a12880412da3551a8cb5a187d3298a72802b45a3bd1805e204ad8439"}, - {file = "contourpy-1.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:a67259c2b493b00e5a4d0f7bfae51fb4b3371395e47d079a4446e9b0f4d70e76"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2b836d22bd2c7bb2700348e4521b25e077255ebb6ab68e351ab5aa91ca27e027"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084eaa568400cfaf7179b847ac871582199b1b44d5699198e9602ecbbb5f6104"}, - {file = "contourpy-1.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:911ff4fd53e26b019f898f32db0d4956c9d227d51338fb3b03ec72ff0084ee5f"}, - {file = "contourpy-1.1.0.tar.gz", hash = "sha256:e53046c3863828d21d531cc3b53786e6580eb1ba02477e8681009b6aa0870b21"}, -] - -[package.dependencies] -numpy = ">=1.16" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx-copybutton"] -mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.2.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "wurlitzer"] - [[package]] name = "contourpy" version = "1.1.1" @@ -2578,64 +2516,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pandas" -version = "2.1.0" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -files = [ - {file = "pandas-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:40dd20439ff94f1b2ed55b393ecee9cb6f3b08104c2c40b0cb7186a2f0046242"}, - {file = "pandas-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d4f38e4fedeba580285eaac7ede4f686c6701a9e618d8a857b138a126d067f2f"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e6a0fe052cf27ceb29be9429428b4918f3740e37ff185658f40d8702f0b3e09"}, - {file = "pandas-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d81e1813191070440d4c7a413cb673052b3b4a984ffd86b8dd468c45742d3cc"}, - {file = "pandas-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb20252720b1cc1b7d0b2879ffc7e0542dd568f24d7c4b2347cb035206936421"}, - {file = "pandas-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:38f74ef7ebc0ffb43b3d633e23d74882bce7e27bfa09607f3c5d3e03ffd9a4a5"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cda72cc8c4761c8f1d97b169661f23a86b16fdb240bdc341173aee17e4d6cedd"}, - {file = "pandas-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d97daeac0db8c993420b10da4f5f5b39b01fc9ca689a17844e07c0a35ac96b4b"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8c58b1113892e0c8078f006a167cc210a92bdae23322bb4614f2f0b7a4b510f"}, - {file = "pandas-2.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:629124923bcf798965b054a540f9ccdfd60f71361255c81fa1ecd94a904b9dd3"}, - {file = "pandas-2.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:70cf866af3ab346a10debba8ea78077cf3a8cd14bd5e4bed3d41555a3280041c"}, - {file = "pandas-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d53c8c1001f6a192ff1de1efe03b31a423d0eee2e9e855e69d004308e046e694"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:86f100b3876b8c6d1a2c66207288ead435dc71041ee4aea789e55ef0e06408cb"}, - {file = "pandas-2.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28f330845ad21c11db51e02d8d69acc9035edfd1116926ff7245c7215db57957"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9a6ccf0963db88f9b12df6720e55f337447aea217f426a22d71f4213a3099a6"}, - {file = "pandas-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99e678180bc59b0c9443314297bddce4ad35727a1a2656dbe585fd78710b3b9"}, - {file = "pandas-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b31da36d376d50a1a492efb18097b9101bdbd8b3fbb3f49006e02d4495d4c644"}, - {file = "pandas-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0164b85937707ec7f70b34a6c3a578dbf0f50787f910f21ca3b26a7fd3363437"}, - {file = "pandas-2.1.0.tar.gz", hash = "sha256:62c24c7fc59e42b775ce0679cfa7b14a5f9bfb7643cfbe708c960699e05fb918"}, -] - -[package.dependencies] -numpy = {version = ">=1.23.2", markers = "python_version >= \"3.11\""} -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.1" - -[package.extras] -all = ["PyQt5 (>=5.15.6)", "SQLAlchemy (>=1.4.36)", "beautifulsoup4 (>=4.11.1)", "bottleneck (>=1.3.4)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=0.8.1)", "fsspec (>=2022.05.0)", "gcsfs (>=2022.05.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.8.0)", "matplotlib (>=3.6.1)", "numba (>=0.55.2)", "numexpr (>=2.8.0)", "odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pandas-gbq (>=0.17.5)", "psycopg2 (>=2.9.3)", "pyarrow (>=7.0.0)", "pymysql (>=1.0.2)", "pyreadstat (>=1.1.5)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)", "pyxlsb (>=1.0.9)", "qtpy (>=2.2.0)", "s3fs (>=2022.05.0)", "scipy (>=1.8.1)", "tables (>=3.7.0)", "tabulate (>=0.8.10)", "xarray (>=2022.03.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)", "zstandard (>=0.17.0)"] -aws = ["s3fs (>=2022.05.0)"] -clipboard = ["PyQt5 (>=5.15.6)", "qtpy (>=2.2.0)"] -compression = ["zstandard (>=0.17.0)"] -computation = ["scipy (>=1.8.1)", "xarray (>=2022.03.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.0.10)", "pyxlsb (>=1.0.9)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.3)"] -feather = ["pyarrow (>=7.0.0)"] -fss = ["fsspec (>=2022.05.0)"] -gcp = ["gcsfs (>=2022.05.0)", "pandas-gbq (>=0.17.5)"] -hdf5 = ["tables (>=3.7.0)"] -html = ["beautifulsoup4 (>=4.11.1)", "html5lib (>=1.1)", "lxml (>=4.8.0)"] -mysql = ["SQLAlchemy (>=1.4.36)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.8.10)"] -parquet = ["pyarrow (>=7.0.0)"] -performance = ["bottleneck (>=1.3.4)", "numba (>=0.55.2)", "numexpr (>=2.8.0)"] -plot = ["matplotlib (>=3.6.1)"] -postgresql = ["SQLAlchemy (>=1.4.36)", "psycopg2 (>=2.9.3)"] -spss = ["pyreadstat (>=1.1.5)"] -sql-other = ["SQLAlchemy (>=1.4.36)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-asyncio (>=0.17.0)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.8.0)"] - [[package]] name = "pandas" version = "2.1.2" @@ -4771,5 +4651,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" -python-versions = "^3.11" -content-hash = "bba5f87aa67bc1d2283a9f4b471ef78e572337f22413870d324e908014410d53" +python-versions = "~3.11" +content-hash = "a4c9b3550bb2a67a54b9ab70e700b24fb9eb0b652e90d7dd8ec92abd121ca6e3" diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 8b321a6b1a..2779be554f 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -7,7 +7,7 @@ readme = "README.md" packages = [{include = "app"}] [tool.poetry.dependencies] -python = "^3.11" +python = "~3.11" torch = [ {markers = "platform_machine == 'arm64' or platform_machine == 'aarch64'", version = "=2.1.0", source = "pypi"}, {markers = "platform_machine == 'amd64' or platform_machine == 'x86_64'", version = "=2.1.0", source = "pytorch-cpu"} diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml index d026b42fe2..230e2a0d77 100644 --- a/mobile/analysis_options.yaml +++ b/mobile/analysis_options.yaml @@ -49,7 +49,6 @@ dart_code_metrics: # Common - avoid-accessing-collections-by-constant-index - avoid-accessing-other-classes-private-members - - avoid-async-call-in-sync-function - avoid-cascade-after-if-null - avoid-collapsible-if - avoid-collection-methods-with-unrelated-types diff --git a/mobile/assets/i18n/ca.json b/mobile/assets/i18n/ca.json index 6bdfb6255f..36aad19570 100644 --- a/mobile/assets/i18n/ca.json +++ b/mobile/assets/i18n/ca.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Afegeix usuaris", "all_people_page_title": "Persones", "all_videos_page_title": "Vídeos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No s'ha trobat res arxivat", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Configuració de la memòria cau", "change_password_form_confirm_password": "Confirma la contrasenya", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json index 2e8f00fe48..8290c8f8ca 100644 --- a/mobile/assets/i18n/cs-CZ.json +++ b/mobile/assets/i18n/cs-CZ.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Místní úložiště", "cache_settings_title": "Nastavení vyrovnávací paměti", "change_password_form_confirm_password": "Potvrďte heslo", - "change_password_form_description": "Dobrý den, {firstName} {lastName},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.", + "change_password_form_description": "Dobrý den, {name},\n\nje to buď poprvé, co se přihlašujete do systému, nebo byl vytvořen požadavek na změnu hesla. Níže zadejte nové heslo.", "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Hesla se neshodují", "change_password_form_reenter_new_password": "Znovu zadejte nové heslo", diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json index 45658bee19..b5b7c7e45d 100644 --- a/mobile/assets/i18n/da-DK.json +++ b/mobile/assets/i18n/da-DK.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Tilføj brugere", "all_people_page_title": "Personer", "all_videos_page_title": "Videoer", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Cache-indstillinger", "change_password_form_confirm_password": "Bekræft kodeord", - "change_password_form_description": "Hej {firstName} {lastName},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.", + "change_password_form_description": "Hej {name},\n\nDette er enten første gang du logger ind eller også er der lavet en anmodning om at ændre dit kodeord. Indtast venligst et nyt kodeord nedenfor.", "change_password_form_new_password": "Nyt kodeord", "change_password_form_password_mismatch": "Kodeord er ikke ens", "change_password_form_reenter_new_password": "Gentag nyt kodeord", diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index 0c871afc84..2af7aecc16 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Lokaler Speicher", "cache_settings_title": "Zwischenspeicher Einstellungen", "change_password_form_confirm_password": "Passwort bestätigen", - "change_password_form_description": "Hallo {firstName} {lastName}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.", + "change_password_form_description": "Hallo {name}\n\nDas ist entweder das erste Mal dass du dich einloggst oder eine Anfrage zur Änderung deines Passwortes wurde gestellt. Bitte gebe das neue Passwort ein.", "change_password_form_new_password": "Neues Passwort", "change_password_form_password_mismatch": "Passwörter stimmen nicht überein", "change_password_form_reenter_new_password": "Passwort erneut eingeben", diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 387a6e370b..6d28890ebc 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -28,7 +28,7 @@ "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", @@ -123,7 +123,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", @@ -390,6 +390,28 @@ "shared_link_edit_show_meta": "Show metadata", "shared_link_edit_submit_button": "Update link", "shared_link_empty": "You don't have any shared links", + "shared_link_error_server_url_fetch": "Cannot fetch the server url", + "shared_link_expired": "Expired", + "shared_link_expires_days": { + "one": "Expires in {} day", + "other": "Expires in {} days" + }, + "shared_link_expires_hours": { + "one": "Expires in {} hour", + "other": "Expires in {} hours" + }, + "shared_link_expires_minutes": { + "one": "Expires in {} minute", + "other": "Expires in {} minutes" + }, + "shared_link_expires_seconds": { + "one": "Expires in {} second", + "other": "Expires in {} seconds" + }, + "shared_link_expires_never": "Expires ∞", + "shared_link_info_chip_download": "Download", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "Manage Shared links", "share_done": "Done", "share_invite": "Invite to album", diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index f6b342f33f..f32a8b1f8e 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", "change_password_form_confirm_password": "Confirmar Contraseña", - "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json index c1d68cf87d..d140b60ee2 100644 --- a/mobile/assets/i18n/es-MX.json +++ b/mobile/assets/i18n/es-MX.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", "change_password_form_confirm_password": "Confirmar Contraseña", - "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json index 388fe5ea1c..0e03e2fd6d 100644 --- a/mobile/assets/i18n/es-PE.json +++ b/mobile/assets/i18n/es-PE.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Almacenamiento local", "cache_settings_title": "Configuración de la caché", "change_password_form_confirm_password": "Confirmar Contraseña", - "change_password_form_description": "Hola {firstName} {lastName},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", + "change_password_form_description": "Hola {name},\n\nEsta es la primera vez que inicias sesión en el sistema o se ha solicitado cambiar tu contraseña. Por favor, introduce la nueva contraseña a continuación.", "change_password_form_new_password": "Nueva Contraseña", "change_password_form_password_mismatch": "Las contraseñas no coinciden", "change_password_form_reenter_new_password": "Vuelve a ingresar la nueva contraseña", diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json index 24ea49a9df..1694f5a1fb 100644 --- a/mobile/assets/i18n/es-US.json +++ b/mobile/assets/i18n/es-US.json @@ -390,6 +390,28 @@ "shared_link_edit_show_meta": "Mostrar metadatos", "shared_link_edit_submit_button": "Actualizar enlace", "shared_link_empty": "No tienes ningún enlace compartido", + "shared_link_error_server_url_fetch": "No se puede obtener la URL del servidor", + "shared_link_expired": "Expirado", + "shared_link_expires_days": { + "one": "Expira en {} día", + "other": "Expira en {} días" + }, + "shared_link_expires_hours": { + "one": "Expira en {} hora", + "other": "Expira en {} horas" + }, + "shared_link_expires_minutes": { + "one": "Expira en {} minuto", + "other": "Expira en {} minutos" + }, + "shared_link_expires_seconds": { + "one": "Expira en {} segundo", + "other": "Expira en {} segundos" + }, + "shared_link_expires_never": "Sin expiración", + "shared_link_info_chip_download": "Descargar", + "shared_link_info_chip_metadata": "EXIF", + "shared_link_info_chip_upload": "Subir", "shared_link_manage_links": "Administrar enlaces compartidos", "share_done": "Hecho", "share_invite": "Invitar al álbum", diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json index 898b7fac4c..d746f99c1d 100644 --- a/mobile/assets/i18n/fi-FI.json +++ b/mobile/assets/i18n/fi-FI.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Paikallinen tallennustila", "cache_settings_title": "Välimuistin asetukset", "change_password_form_confirm_password": "Vahvista salasana", - "change_password_form_description": "Hei {firstName} {lastName},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.", + "change_password_form_description": "Hei {name},\n\nTämä on joko ensimmäinen kirjautumisesi järjestelmään tai salasanan vaihtaminen vaihtaminen on pakotettu. Ole hyvä ja syötä uusi salasana alle.", "change_password_form_new_password": "Uusi salasana", "change_password_form_password_mismatch": "Salasanat eivät täsmää", "change_password_form_reenter_new_password": "Uusi salasana uudelleen", diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json index ee06e82ca7..25e2615d9c 100644 --- a/mobile/assets/i18n/fr-CA.json +++ b/mobile/assets/i18n/fr-CA.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", "change_password_form_confirm_password": "Confirmez le mot de passe", - "change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", + "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé de changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", "change_password_form_new_password": "Nouveau mot de passe", "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas", "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index 84c1ffd6af..22050072d9 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Stockage local", "cache_settings_title": "Paramètres de mise en cache", "change_password_form_confirm_password": "Confirmez le mot de passe", - "change_password_form_description": "Bonjour {firstName} {lastName},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", + "change_password_form_description": "Bonjour {name},\n\nC'est la première fois que vous vous connectez au système ou vous avez demandé à changer votre mot de passe. Veuillez saisir le nouveau mot de passe ci-dessous.", "change_password_form_new_password": "Nouveau mot de passe", "change_password_form_password_mismatch": "Les mots de passe ne correspondent pas", "change_password_form_reenter_new_password": "Saisissez à nouveau le nouveau mot de passe", diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json index 7e3015c9fb..9c42afb7d1 100644 --- a/mobile/assets/i18n/hi-IN.json +++ b/mobile/assets/i18n/hi-IN.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json index 0b9ed145f0..60bf00d994 100644 --- a/mobile/assets/i18n/hu-HU.json +++ b/mobile/assets/i18n/hu-HU.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Felhasználók hozzáadása", "all_people_page_title": "Emberek", "all_videos_page_title": "Videók", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Nem található archivált média", diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json index 77fed857b7..d8a3725275 100644 --- a/mobile/assets/i18n/it-IT.json +++ b/mobile/assets/i18n/it-IT.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Aggiungi utenti", "all_people_page_title": "Persone", "all_videos_page_title": "Video", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Nessuna oggetto archiviato", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Impostazioni della Cache", "change_password_form_confirm_password": "Conferma Password ", - "change_password_form_description": "Ciao {firstName} {lastName},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto", + "change_password_form_description": "Ciao {name},\n\nQuesto è la prima volta che accedi al sistema oppure è stato fatto una richiesta di cambiare la password. Per favore inserisca la nuova password qui sotto", "change_password_form_new_password": "Nuova Password", "change_password_form_password_mismatch": "Le password non coincidono", "change_password_form_reenter_new_password": "Inserisci ancora la nuova password ", diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index 58c738af6e..4fa57d9831 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "로컬 저장소", "cache_settings_title": "캐시 설정", "change_password_form_confirm_password": "비밀번호 확인", - "change_password_form_description": "{firstName} {lastName} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.", + "change_password_form_description": "{name} 님, 안녕하세요.\n\n시스템에 처음 로그인했거나 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력하세요.", "change_password_form_new_password": "새 비밀번호", "change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다", "change_password_form_reenter_new_password": "새 비밀번호 재입력", diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json index daf639d46e..875044c299 100644 --- a/mobile/assets/i18n/lv-LV.json +++ b/mobile/assets/i18n/lv-LV.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Pievienot lietotājus", "all_people_page_title": "Cilvēki", "all_videos_page_title": "Videoklipi", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Nav atrasts neviens arhivēts aktīvs", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Kešdarbes iestatījumi", "change_password_form_confirm_password": "Apstiprināt Paroli", - "change_password_form_description": "Sveiki {FirstName} {LastName},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.", + "change_password_form_description": "Sveiki {name},\n\nŠī ir pirmā reize, kad pierakstāties sistēmā, vai arī ir iesniegts pieprasījums mainīt paroli. Lūdzu, zemāk ievadiet jauno paroli.", "change_password_form_new_password": "Jauna Parole", "change_password_form_password_mismatch": "Paroles nesakrīt", "change_password_form_reenter_new_password": "Atkārtoti ievadīt jaunu paroli", diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn.json index 60828faa2c..dfbb54d89a 100644 --- a/mobile/assets/i18n/mn.json +++ b/mobile/assets/i18n/mn.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index 0f919be543..cd2e9be27c 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Lokal lagring", "cache_settings_title": "Bufringsinnstillinger", "change_password_form_confirm_password": "Bekreft passord", - "change_password_form_description": "Hei {firstName} {lastName}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", + "change_password_form_description": "Hei {name}!\n\nDette er enten første gang du logger på systemet, eller det er sendt en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", "change_password_form_new_password": "Nytt passord", "change_password_form_password_mismatch": "Passordene stemmer ikke", "change_password_form_reenter_new_password": "Skriv nytt passord igjen", diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index 842460652d..eece8bd402 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Gebruikers toevoegen", "all_people_page_title": "Personen", "all_videos_page_title": "Video's", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Geen gearchiveerde items gevonden", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Cache-instellingen", "change_password_form_confirm_password": "Bevestig wachtwoord", - "change_password_form_description": "Hallo {firstName} {lastName},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.", + "change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.", "change_password_form_new_password": "Nieuw wachtwoord", "change_password_form_password_mismatch": "Wachtwoorden komen niet overeen", "change_password_form_reenter_new_password": "Vul het wachtwoord opnieuw in", diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index 53395ff799..0c7cebe246 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Lokalny magazyn", "cache_settings_title": "Ustawienia Buforowania", "change_password_form_confirm_password": "Potwierdź Hasło", - "change_password_form_description": "Cześć {firstName} {lastName},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.", + "change_password_form_description": "Cześć {name},\n\nPierwszy raz logujesz się do systemu, albo złożono prośbę o zmianę hasła. Wpisz poniżej nowe hasło.", "change_password_form_new_password": "Nowe Hasło", "change_password_form_password_mismatch": "Hasła nie są zgodne", "change_password_form_reenter_new_password": "Wprowadź ponownie Nowe Hasło", diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index 62fdb4d70e..2767a82003 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Локальное хранилище", "cache_settings_title": "Настройки кэширования", "change_password_form_confirm_password": "Подтвердите пароль", - "change_password_form_description": "Привет {firstName} {lastName},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.", + "change_password_form_description": "Привет {name},\n\nЭто либо ваш первый вход в систему, либо был сделан запрос на смену пароля. Пожалуйста, введите новый пароль ниже.", "change_password_form_new_password": "Новый пароль", "change_password_form_password_mismatch": "Пароли не совпадают", "change_password_form_reenter_new_password": "Повторно введите новый пароль", diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json index aa0f230ddf..3e4921ffe3 100644 --- a/mobile/assets/i18n/sk-SK.json +++ b/mobile/assets/i18n/sk-SK.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Lokálne úložisko", "cache_settings_title": "Nastavenia vyrovnávacej pamäte", "change_password_form_confirm_password": "Potvrďte heslo", - "change_password_form_description": "Dobrý deň, {firstName} {lastName},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.", + "change_password_form_description": "Dobrý deň, {name},\n\nBuď sa do systému prihlasujete prvýkrát, alebo bola podaná žiadosť o zmenu hesla. Prosím, zadajte nové heslo nižšie.", "change_password_form_new_password": "Nové heslo", "change_password_form_password_mismatch": "Heslá sa nezhodujú", "change_password_form_reenter_new_password": "Znova zadajte nové heslo", diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json index 7e3015c9fb..9c42afb7d1 100644 --- a/mobile/assets/i18n/sr-Cyrl.json +++ b/mobile/assets/i18n/sr-Cyrl.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json index 2cdc37acf8..9588a2ede7 100644 --- a/mobile/assets/i18n/sr-Latn.json +++ b/mobile/assets/i18n/sr-Latn.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Dodaj korisnike", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json index 7e3015c9fb..9c42afb7d1 100644 --- a/mobile/assets/i18n/sv-FI.json +++ b/mobile/assets/i18n/sv-FI.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Add users", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index 31e15c6435..026219fe5e 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Lägg till användare", "all_people_page_title": "People", "all_videos_page_title": "Videos", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "No archived assets found", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Cache Inställningar", "change_password_form_confirm_password": "Bekräfta lösenord", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "Nytt lösenord", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json index ab2e93525e..0cfad7effa 100644 --- a/mobile/assets/i18n/th-TH.json +++ b/mobile/assets/i18n/th-TH.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "เพิ่มผู้ใช้งาน", "all_people_page_title": "ผู้คน", "all_videos_page_title": "วิดีโอ", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "ไม่พบทรัพยากรในที่เก็บถาวร", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "ตั้งค่าแคช", "change_password_form_confirm_password": "ยืนยันรหัสผ่าน", - "change_password_form_description": "สวัสดี {firstName} {lastName},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง", + "change_password_form_description": "สวัสดี {name},\n\nครั้งนี้อาจจะเป็นครั้งแรกที่คุณเข้าสู่ระบบ หรือมีคำขอเพื่อที่จะเปลี่ยนรหัสผ่านของคุI กรุณาเพิ่มรหัสผ่านใหม่ข้างล่าง", "change_password_form_new_password": "รหัสผ่านใหม่", "change_password_form_password_mismatch": "รหัสผ่านไม่ตรงกัน", "change_password_form_reenter_new_password": "กรอกรหัสผ่านใหม่", diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index 3e44a505e0..d3df92692d 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -27,7 +27,7 @@ "album_viewer_page_share_add_users": "Додати користувачів", "all_people_page_title": "Люди", "all_videos_page_title": "Відео", - "app_bar_signout_dialog_content": "Are you sure you wanna sign out?", + "app_bar_signout_dialog_content": "Are you sure you want to sign out?", "app_bar_signout_dialog_ok": "Yes", "app_bar_signout_dialog_title": "Sign out", "archive_page_no_archived_assets": "Немає архівних елементів", @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Local Storage", "cache_settings_title": "Налаштування Кешування", "change_password_form_confirm_password": "Підтвердити пароль", - "change_password_form_description": "Привіт {firstName} {lastName},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", + "change_password_form_description": "Привіт {name},\n\nВи або або вперше входите у систему, або було зроблено запит на зміну вашого пароля. \nВведіть ваш новий пароль.", "change_password_form_new_password": "Новий Пароль", "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть Новий Пароль", diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index 20edf605af..60edb10b48 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "Lưu trữ cục bộ", "cache_settings_title": "Caching Settings", "change_password_form_confirm_password": "Confirm Password", - "change_password_form_description": "Hi {firstName} {lastName},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", + "change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.", "change_password_form_new_password": "New Password", "change_password_form_password_mismatch": "Passwords do not match", "change_password_form_reenter_new_password": "Re-enter New Password", diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json index 00b3501c43..28591e3959 100644 --- a/mobile/assets/i18n/zh-CN.json +++ b/mobile/assets/i18n/zh-CN.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", "change_password_form_confirm_password": "确认密码", - "change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", + "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "重新输入新的密码", diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json index 3673afacb5..cc8b89a152 100644 --- a/mobile/assets/i18n/zh-Hans.json +++ b/mobile/assets/i18n/zh-Hans.json @@ -119,7 +119,7 @@ "cache_settings_tile_title": "本地存储", "cache_settings_title": "缓存设置", "change_password_form_confirm_password": "确认密码", - "change_password_form_description": "{firstName} {lastName} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", + "change_password_form_description": "{name} 您好,\n\n这是您首次登录系统,或被管理员要求更改密码。\n请在下方输入新密码。", "change_password_form_new_password": "新密码", "change_password_form_password_mismatch": "密码不匹配", "change_password_form_reenter_new_password": "重新输入新的密码", diff --git a/mobile/integration_test/test_utils/general_helper.dart b/mobile/integration_test/test_utils/general_helper.dart index e3b28e1f3d..8daa08d70d 100644 --- a/mobile/integration_test/test_utils/general_helper.dart +++ b/mobile/integration_test/test_utils/general_helper.dart @@ -45,7 +45,7 @@ class ImmichTestHelper { await tester.pumpWidget( ProviderScope( overrides: [dbProvider.overrideWithValue(db)], - child: app.getMainWidget(), + child: const app.MainWidget(), ), ); // Post run tasks diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index c6c23d942a..75168ce1c9 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -169,4 +169,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 599d8aeb73728400c15364e734525722250a5382 -COCOAPODS: 1.12.1 +COCOAPODS: 1.11.3 diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index e3f6013581..598f956619 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -Color immichBackgroundColor = const Color(0xFFf6f8fe); -Color immichDarkBackgroundColor = const Color.fromARGB(255, 0, 0, 0); -Color immichDarkThemePrimaryColor = const Color.fromARGB(255, 173, 203, 250); +const Color immichBackgroundColor = Color(0xFFf6f8fe); +const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0); +const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250); diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart index 2c4725de8b..036881f3c2 100644 --- a/mobile/lib/extensions/asyncvalue_extensions.dart +++ b/mobile/lib/extensions/asyncvalue_extensions.dart @@ -4,22 +4,32 @@ import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; import 'package:logging/logging.dart'; -extension ScaffoldBody on AsyncValue { - static final Logger _scaffoldBodyLog = Logger("ScaffoldBody"); +extension LogOnError on AsyncValue { + static final Logger _asyncErrorLogger = Logger("AsyncValue"); - Widget scaffoldBodyWhen({ + Widget widgetWhen({ + bool skipLoadingOnRefresh = true, + Widget Function()? onLoading, + Widget Function(Object? error, StackTrace? stack)? onError, required Widget Function(T data) onData, - Widget? onError, }) { if (isLoading) { - return const Center( - child: ImmichLoadingIndicator(), - ); + bool skip = false; + if (isRefreshing) { + skip = skipLoadingOnRefresh; + } + + if (!skip) { + return onLoading?.call() ?? + const Center( + child: ImmichLoadingIndicator(), + ); + } } if (hasError && !hasValue) { - _scaffoldBodyLog.severe("Error occured in AsyncValue", error, stackTrace); - return onError ?? const ScaffoldErrorBody(); + _asyncErrorLogger.severe("Error occured", error, stackTrace); + return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody(); } return onData(requireValue); diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index b2e6ed4720..6151bd1a5c 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -45,7 +45,7 @@ extension ContextHelper on BuildContext { ) => AutoRouter.of(this).navigate(route); -// Auto-Push replace route from the current context + // Auto-Push replace route from the current context Future autoReplace(PageRouteInfo route) => AutoRouter.of(this).replace(route); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 6d4a812b61..a12c43b6ca 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; @@ -7,6 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:timezone/data/latest.dart'; import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/modules/backup/background_service/background.service.dart'; @@ -28,7 +30,6 @@ import 'package:immich_mobile/shared/providers/app_state.provider.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:immich_mobile/shared/services/local_notification.service.dart'; -import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/migration.dart'; @@ -43,10 +44,11 @@ void main() async { await initApp(); await migrateDatabaseIfNeeded(db); HttpOverrides.global = HttpSSLCertOverride(); + runApp( ProviderScope( overrides: [dbProvider.overrideWithValue(db)], - child: getMainWidget(), + child: const MainWidget(), ), ); } @@ -108,16 +110,6 @@ Future loadDb() async { return db; } -Widget getMainWidget() { - return EasyLocalization( - supportedLocales: locales, - path: translationsPath, - useFallbackTranslations: true, - fallbackLocale: locales.first, - child: const ImmichApp(), - ); -} - class ImmichApp extends ConsumerStatefulWidget { const ImmichApp({super.key}); @@ -167,10 +159,9 @@ class ImmichAppState extends ConsumerState // Android 8 does not support transparent app bars final info = await DeviceInfoPlugin().androidInfo; if (info.version.sdkInt <= 26) { - overlayStyle = - MediaQuery.of(context).platformBrightness == Brightness.light - ? SystemUiOverlayStyle.light - : SystemUiOverlayStyle.dark; + overlayStyle = context.isDarkTheme + ? SystemUiOverlayStyle.dark + : SystemUiOverlayStyle.light; } } SystemChrome.setSystemUIOverlayStyle(overlayStyle); @@ -202,22 +193,33 @@ class ImmichAppState extends ConsumerState supportedLocales: context.supportedLocales, locale: context.locale, debugShowCheckedModeBanner: false, - home: Stack( - children: [ - MaterialApp.router( - title: 'Immich', - debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeProvider), - darkTheme: immichDarkTheme, - theme: immichLightTheme, - routeInformationParser: router.defaultRouteParser(), - routerDelegate: router.delegate( - navigatorObservers: () => [TabNavigationObserver(ref: ref)], - ), - ), - const ImmichLoadingOverlay(), - ], + home: MaterialApp.router( + title: 'Immich', + debugShowCheckedModeBanner: false, + themeMode: ref.watch(immichThemeProvider), + darkTheme: immichDarkTheme, + theme: immichLightTheme, + routeInformationParser: router.defaultRouteParser(), + routerDelegate: router.delegate( + navigatorObservers: () => [TabNavigationObserver(ref: ref)], + ), ), ); } } + +// ignore: prefer-single-widget-per-file +class MainWidget extends StatelessWidget { + const MainWidget({super.key}); + + @override + Widget build(BuildContext context) { + return EasyLocalization( + supportedLocales: locales, + path: translationsPath, + useFallbackTranslations: true, + fallbackLocale: locales.first, + child: const ImmichApp(), + ); + } +} diff --git a/mobile/lib/modules/activities/views/activities_page.dart b/mobile/lib/modules/activities/views/activities_page.dart index 1cfd48b5bc..f0c68a3491 100644 --- a/mobile/lib/modules/activities/views/activities_page.dart +++ b/mobile/lib/modules/activities/views/activities_page.dart @@ -4,12 +4,12 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/activities/models/activity.model.dart'; import 'package:immich_mobile/modules/activities/providers/activity.provider.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; import 'package:immich_mobile/extensions/datetime_extensions.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -88,7 +88,7 @@ class ActivitiesPage extends HookConsumerWidget { width: 40, height: 30, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), + borderRadius: const BorderRadius.all(Radius.circular(4)), image: DecorationImage( image: CachedNetworkImageProvider( getThumbnailUrlForRemoteId( @@ -231,11 +231,8 @@ class ActivitiesPage extends HookConsumerWidget { return Scaffold( appBar: AppBar(title: Text(appBarTitle)), - body: activities.maybeWhen( - orElse: () { - return const Center(child: ImmichLoadingIndicator()); - }, - data: (data) { + body: activities.widgetWhen( + onData: (data) { final liked = data.firstWhereOrNull( (a) => a.type == ActivityType.like && diff --git a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart index 650d4da0d4..25747177a5 100644 --- a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart +++ b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart @@ -65,7 +65,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget { } ref.invalidate(albumDetailProvider(album.id)); - Navigator.pop(context); + context.pop(); } return Card( diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart index fbab831f7b..0e2fc74fb3 100644 --- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart +++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart @@ -43,6 +43,7 @@ class AlbumViewerAppbar extends HookConsumerWidget Widget build(BuildContext context, WidgetRef ref) { final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText; final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum; + final isProcessing = useProcessingOverlay(); final comments = album.shared ? ref.watch( activityStatisticsStateProvider( @@ -52,7 +53,7 @@ class AlbumViewerAppbar extends HookConsumerWidget : 0; deleteAlbum() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; final bool success; if (album.shared) { @@ -74,7 +75,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } Future showConfirmationDialog() async { @@ -89,7 +90,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ), actions: [ TextButton( - onPressed: () => Navigator.pop(context, 'Cancel'), + onPressed: () => context.pop('Cancel'), child: Text( 'Cancel', style: TextStyle( @@ -100,7 +101,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ), TextButton( onPressed: () { - Navigator.pop(context, 'Confirm'); + context.pop('Confirm'); deleteAlbum(); }, child: Text( @@ -122,7 +123,7 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onLeaveAlbumPressed() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).leaveAlbum(album); @@ -131,7 +132,7 @@ class AlbumViewerAppbar extends HookConsumerWidget context .autoNavigate(const TabControllerRoute(children: [SharingRoute()])); } else { - Navigator.pop(context); + context.pop(); ImmichToast.show( context: context, msg: "album_viewer_appbar_share_err_leave".tr(), @@ -140,11 +141,11 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void onRemoveFromAlbumPressed() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; bool isSuccess = await ref.watch(sharedAlbumProvider.notifier).removeAssetFromAlbum( @@ -153,12 +154,12 @@ class AlbumViewerAppbar extends HookConsumerWidget ); if (isSuccess) { - Navigator.pop(context); + context.pop(); selectionDisabled(); ref.watch(albumProvider.notifier).getAllAlbums(); ref.invalidate(albumDetailProvider(album.id)); } else { - Navigator.pop(context); + context.pop(); ImmichToast.show( context: context, msg: "album_viewer_appbar_share_err_remove".tr(), @@ -167,7 +168,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void handleShareAssets( @@ -198,9 +199,9 @@ class AlbumViewerAppbar extends HookConsumerWidget } void onShareAssetsTo() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; handleShareAssets(ref, context, selected); - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } buildBottomSheetActions() { @@ -253,7 +254,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ListTile( leading: const Icon(Icons.person_add_alt_rounded), onTap: () { - Navigator.pop(context); + context.pop(); onAddUsers!(album); }, title: const Text( @@ -265,7 +266,7 @@ class AlbumViewerAppbar extends HookConsumerWidget leading: const Icon(Icons.share_rounded), onTap: () { context.autoPush(SharedLinkEditRoute(albumId: album.remoteId)); - Navigator.pop(context); + context.pop(); }, title: const Text( "control_bottom_app_bar_share", @@ -286,7 +287,7 @@ class AlbumViewerAppbar extends HookConsumerWidget ListTile( leading: const Icon(Icons.add_photo_alternate_outlined), onTap: () { - Navigator.pop(context); + context.pop(); onAddPhotos!(album); }, title: const Text( diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart index 6974a63907..6ef7733392 100644 --- a/mobile/lib/modules/album/views/album_options_part.dart +++ b/mobile/lib/modules/album/views/album_options_part.dart @@ -24,10 +24,11 @@ class AlbumOptionsPage extends HookConsumerWidget { final owner = album.owner.value; final userId = ref.watch(authenticationProvider).userId; final activityEnabled = useState(album.activityEnabled); + final isProcessing = useProcessingOverlay(); final isOwner = owner?.id == userId; void showErrorMessage() { - Navigator.pop(context); + context.pop(); ImmichToast.show( context: context, msg: "shared_album_section_people_action_error".tr(), @@ -37,7 +38,7 @@ class AlbumOptionsPage extends HookConsumerWidget { } void leaveAlbum() async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; try { final isSuccess = @@ -54,11 +55,11 @@ class AlbumOptionsPage extends HookConsumerWidget { showErrorMessage(); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } void removeUserFromAlbum(User user) async { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; try { await ref @@ -70,8 +71,8 @@ class AlbumOptionsPage extends HookConsumerWidget { showErrorMessage(); } - Navigator.pop(context); - ImmichLoadingOverlayController.appLoader.hide(); + context.pop(); + isProcessing.value = false; } void handleUserClick(User user) { @@ -180,9 +181,7 @@ class AlbumOptionsPage extends HookConsumerWidget { appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded), - onPressed: () { - context.autoPop(null); - }, + onPressed: () => context.autoPop(null), ), centerTitle: true, title: Text("translated_text_options".tr()), diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart index f3b9827822..6d07c3b66a 100644 --- a/mobile/lib/modules/album/views/album_viewer_page.dart +++ b/mobile/lib/modules/album/views/album_viewer_page.dart @@ -4,6 +4,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart'; @@ -17,7 +18,6 @@ import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; @@ -33,6 +33,7 @@ class AlbumViewerPage extends HookConsumerWidget { final userId = ref.watch(authenticationProvider).userId; final selection = useState>({}); final multiSelectEnabled = useState(false); + final isProcessing = useProcessingOverlay(); useEffect( () { @@ -75,24 +76,21 @@ class AlbumViewerPage extends HookConsumerWidget { ), ); - if (returnPayload != null) { + if (returnPayload != null && returnPayload.selectedAssets.isNotEmpty) { // Check if there is new assets add - if (returnPayload.selectedAssets.isNotEmpty) { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; - var addAssetsResult = - await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( - returnPayload.selectedAssets, - albumInfo, - ); + var addAssetsResult = + await ref.watch(albumServiceProvider).addAdditionalAssetToAlbum( + returnPayload.selectedAssets, + albumInfo, + ); - if (addAssetsResult != null && - addAssetsResult.successfullyAdded > 0) { - ref.invalidate(albumDetailProvider(albumId)); - } - - ImmichLoadingOverlayController.appLoader.hide(); + if (addAssetsResult != null && addAssetsResult.successfullyAdded > 0) { + ref.invalidate(albumDetailProvider(albumId)); } + + isProcessing.value = false; } } @@ -102,7 +100,7 @@ class AlbumViewerPage extends HookConsumerWidget { ); if (sharedUserIds != null) { - ImmichLoadingOverlayController.appLoader.show(); + isProcessing.value = true; var isSuccess = await ref .watch(albumServiceProvider) @@ -112,7 +110,7 @@ class AlbumViewerPage extends HookConsumerWidget { ref.invalidate(albumDetailProvider(album.id)); } - ImmichLoadingOverlayController.appLoader.hide(); + isProcessing.value = false; } } @@ -260,13 +258,11 @@ class AlbumViewerPage extends HookConsumerWidget { error: (error, stackTrace) => AppBar(title: const Text("Error")), loading: () => AppBar(), ), - body: album.when( - data: (data) => WillPopScope( + body: album.widgetWhen( + onData: (data) => WillPopScope( onWillPop: onWillPop, child: GestureDetector( - onTap: () { - titleFocusNode.unfocus(); - }, + onTap: () => titleFocusNode.unfocus(), child: ImmichAssetGrid( renderList: data.renderList, listener: selectionListener, @@ -285,10 +281,6 @@ class AlbumViewerPage extends HookConsumerWidget { ), ), ), - error: (e, _) => Center(child: Text("Error loading album info!\n$e")), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/album/views/asset_selection_page.dart b/mobile/lib/modules/album/views/asset_selection_page.dart index c1870fe442..471a74ace5 100644 --- a/mobile/lib/modules/album/views/asset_selection_page.dart +++ b/mobile/lib/modules/album/views/asset_selection_page.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; @@ -85,12 +86,8 @@ class AssetSelectionPage extends HookConsumerWidget { ), ], ), - body: renderList.when( - data: (data) => buildBody(data), - error: (error, stackTrace) => Center( - child: Text(error.toString()), - ), - loading: () => const Center(child: CircularProgressIndicator()), + body: renderList.widgetWhen( + onData: (data) => buildBody(data), ), ); } diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart index b91197d282..2aad67ef56 100644 --- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart @@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/user.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; class SelectAdditionalUserForSharingPage extends HookConsumerWidget { @@ -137,8 +137,8 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { ), ], ), - body: suggestedShareUsers.when( - data: (users) { + body: suggestedShareUsers.widgetWhen( + onData: (users) { for (var sharedUsers in album.sharedUsers) { users.removeWhere( (u) => u.id == sharedUsers.id || u.id == album.ownerId, @@ -147,10 +147,6 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget { return buildUserList(users); }, - error: (e, _) => Text("Error loading suggested users $e"), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart index 61ced47e22..3d6dcf6787 100644 --- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart +++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/album/providers/album_title.provider.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; @@ -9,7 +10,6 @@ import 'package:immich_mobile/modules/album/providers/suggested_shared_users.pro import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/user.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/user_circle_avatar.dart'; class SelectUserForSharingPage extends HookConsumerWidget { @@ -42,7 +42,12 @@ class SelectUserForSharingPage extends HookConsumerWidget { ScaffoldMessenger( child: SnackBar( - content: const Text('select_user_for_sharing_page_err_album').tr(), + content: Text( + 'select_user_for_sharing_page_err_album', + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ).tr(), ), ); } @@ -166,14 +171,10 @@ class SelectUserForSharingPage extends HookConsumerWidget { ), ], ), - body: suggestedShareUsers.when( - data: (users) { + body: suggestedShareUsers.widgetWhen( + onData: (users) { return buildUserList(users); }, - error: (e, _) => Text("Error loading suggested users $e"), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/archive/views/archive_page.dart b/mobile/lib/modules/archive/views/archive_page.dart index 06270afc1a..fb3cecc10f 100644 --- a/mobile/lib/modules/archive/views/archive_page.dart +++ b/mobile/lib/modules/archive/views/archive_page.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; @@ -48,37 +49,33 @@ class ArchivePage extends HookConsumerWidget { child: SizedBox( height: 64, child: Card( - child: Column( - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - leading: const Icon( - Icons.unarchive_rounded, - ), - title: Text( - 'control_bottom_app_bar_unarchive'.tr(), - style: const TextStyle(fontSize: 14), - ), - onTap: processing.value - ? null - : () async { - processing.value = true; - try { - await handleArchiveAssets( - ref, - context, - selection.value.toList(), - shouldArchive: false, - ); - } finally { - processing.value = false; - selectionEnabledHook.value = false; - } - }, - ), - ], + child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + leading: const Icon( + Icons.unarchive_rounded, + ), + title: Text( + 'control_bottom_app_bar_unarchive'.tr(), + style: const TextStyle(fontSize: 14), + ), + onTap: processing.value + ? null + : () async { + processing.value = true; + try { + await handleArchiveAssets( + ref, + context, + selection.value.toList(), + shouldArchive: false, + ); + } finally { + processing.value = false; + selectionEnabledHook.value = false; + } + }, ), ), ), @@ -86,18 +83,13 @@ class ArchivePage extends HookConsumerWidget { ); } - return archivedAssets.when( - loading: () => Scaffold( - appBar: buildAppBar("?"), - body: const Center(child: CircularProgressIndicator()), + return Scaffold( + appBar: archivedAssets.maybeWhen( + data: (data) => buildAppBar(data.totalAssets.toString()), + orElse: () => buildAppBar("?"), ), - error: (error, stackTrace) => Scaffold( - appBar: buildAppBar("Error"), - body: Center(child: Text(error.toString())), - ), - data: (data) => Scaffold( - appBar: buildAppBar(data.totalAssets.toString()), - body: data.isEmpty + body: archivedAssets.widgetWhen( + onData: (data) => data.isEmpty ? Center( child: Text('archive_page_no_archived_assets'.tr()), ) diff --git a/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart index 97b955b5f3..c265346b00 100644 --- a/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/advanced_bottom_sheet.dart @@ -62,8 +62,14 @@ class AdvancedBottomSheet extends HookConsumerWidget { ClipboardData(text: assetDetail.toString()), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Copied to clipboard"), + SnackBar( + content: Text( + "Copied to clipboard", + style: context.textTheme.bodyLarge + ?.copyWith( + color: context.primaryColor, + ), + ), ), ); }); diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index bdb2bb50ca..9948ab4cc9 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -514,7 +514,7 @@ class GalleryViewerPage extends HookConsumerWidget { currentAsset, stackElements.elementAt(stackIndex.value), ); - Navigator.pop(ctx); + ctx.pop(); context.autoPop(); }, title: const Text( @@ -541,7 +541,7 @@ class GalleryViewerPage extends HookConsumerWidget { stackElements.elementAt(1), childrenToRemove: [currentAsset], ); - Navigator.pop(ctx); + ctx.pop(); context.autoPop(); } else { await ref.read(assetStackServiceProvider).updateStack( @@ -551,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget { ], ); removeAssetFromStack(); - Navigator.pop(ctx); + ctx.pop(); } }, title: const Text( @@ -569,7 +569,7 @@ class GalleryViewerPage extends HookConsumerWidget { currentAsset, childrenToRemove: stack, ); - Navigator.pop(ctx); + ctx.pop(); context.autoPop(); }, title: const Text( diff --git a/mobile/lib/modules/backup/services/backup.service.dart b/mobile/lib/modules/backup/services/backup.service.dart index 15cc0c3493..f4ca5932a1 100644 --- a/mobile/lib/modules/backup/services/backup.service.dart +++ b/mobile/lib/modules/backup/services/backup.service.dart @@ -42,6 +42,9 @@ class BackupService { try { return await _apiService.assetApi.getUserAssetsByDeviceId(deviceId); + + // TODO! Start using this in 1.92.0 + // return await _apiService.assetApi.getAllUserAssetsByDeviceId(deviceId); } catch (e) { debugPrint('Error [getDeviceBackupAsset] ${e.toString()}'); return null; @@ -275,13 +278,6 @@ class BackupService { req.files.add(assetRawUploadData); - if (entity.isLivePhoto) { - var livePhotoRawUploadData = await _getLivePhotoFile(entity); - if (livePhotoRawUploadData != null) { - req.files.add(livePhotoRawUploadData); - } - } - setCurrentUploadAssetCb( CurrentUploadAsset( id: entity.id, @@ -296,6 +292,29 @@ class BackupService { var response = await httpClient.send(req, cancellationToken: cancelToken); + // Send live photo separately + if (entity.isLivePhoto) { + var livePhotoRawUploadData = await _getLivePhotoFile(entity); + if (livePhotoRawUploadData != null) { + var livePhotoReq = MultipartRequest( + req.method, + req.url, + onProgress: req.onProgress, + ) + ..headers.addAll(req.headers) + ..fields.addAll(req.fields); + + livePhotoReq.files.add(livePhotoRawUploadData); + // Send live photo only if the non-motion part is successful + if (response.statusCode == 200 || response.statusCode == 201) { + response = await httpClient.send( + livePhotoReq, + cancellationToken: cancelToken, + ); + } + } + } + if (response.statusCode == 200) { // asset is a duplicate (already exists on the server) duplicatedAssetIds.add(entity.id); @@ -353,7 +372,7 @@ class BackupService { var fileStream = motionFile.openRead(); String fileName = p.basename(motionFile.path); return http.MultipartFile( - "livePhotoData", + "assetData", fileStream, motionFile.lengthSync(), filename: fileName, diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart index 9ffdd7aa5a..2bdb3a5dde 100644 --- a/mobile/lib/modules/backup/views/backup_controller_page.dart +++ b/mobile/lib/modules/backup/views/backup_controller_page.dart @@ -229,6 +229,9 @@ class BackupControllerPage extends HookConsumerWidget { final snackBar = SnackBar( content: Text( msg.tr(), + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), ), backgroundColor: Colors.red, ); diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart index 1636113968..095297507b 100644 --- a/mobile/lib/modules/favorite/views/favorites_page.dart +++ b/mobile/lib/modules/favorite/views/favorites_page.dart @@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; @@ -62,22 +63,18 @@ class FavoritesPage extends HookConsumerWidget { child: SizedBox( height: 64, child: Card( - child: Column( - children: [ - ListTile( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - leading: const Icon( - Icons.star_border, - ), - title: const Text( - "Unfavorite", - style: TextStyle(fontSize: 14), - ), - onTap: processing.value ? null : unfavorite, - ), - ], + child: ListTile( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + leading: const Icon( + Icons.star_border, + ), + title: const Text( + "Unfavorite", + style: TextStyle(fontSize: 14), + ), + onTap: processing.value ? null : unfavorite, ), ), ), @@ -87,10 +84,8 @@ class FavoritesPage extends HookConsumerWidget { return Scaffold( appBar: buildAppBar(), - body: ref.watch(favoriteAssetsProvider).when( - loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stackTrace) => Center(child: Text(error.toString())), - data: (data) => data.isEmpty + body: ref.watch(favoriteAssetsProvider).widgetWhen( + onData: (data) => data.isEmpty ? Center( child: Text('favorites_page_no_favorites'.tr()), ) diff --git a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart index 2c0f63394b..562b7892c3 100644 --- a/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart +++ b/mobile/lib/modules/home/ui/asset_grid/immich_asset_grid.dart @@ -5,13 +5,13 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/render_list.provider.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid_view.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class ImmichAssetGrid extends HookConsumerWidget { @@ -130,12 +130,8 @@ class ImmichAssetGrid extends HookConsumerWidget { if (renderList != null) return buildAssetGridView(renderList!); final renderListFuture = ref.watch(renderListProvider(assets!)); - return renderListFuture.when( - data: (renderList) => buildAssetGridView(renderList), - error: (err, stack) => Center(child: Text("$err")), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), + return renderListFuture.widgetWhen( + onData: (renderList) => buildAssetGridView(renderList), ); } } diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart index c99e08fb3b..694279c0d6 100644 --- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart +++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart @@ -197,7 +197,9 @@ class ThumbnailImage extends StatelessWidget { }, child: Stack( children: [ - Container( + AnimatedContainer( + duration: const Duration(milliseconds: 300), + curve: Curves.decelerate, decoration: BoxDecoration( border: multiselectEnabled && isSelected ? Border.all( diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index 4383ed21b0..58770ed5ca 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -28,6 +28,7 @@ import 'package:immich_mobile/shared/providers/websocket.provider.dart'; import 'package:immich_mobile/shared/ui/immich_app_bar.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; class HomePage extends HookConsumerWidget { @@ -50,7 +51,7 @@ class HomePage extends HookConsumerWidget { final tipOneOpacity = useState(0.0); final refreshCount = useState(0); - final processing = useState(false); + final processing = useProcessingOverlay(); useEffect( () { @@ -212,10 +213,10 @@ class HomePage extends HookConsumerWidget { processing.value = true; selectionEnabledHook.value = false; try { - ref.read(manualUploadProvider.notifier).uploadAssets( - context, - selection.value.where((a) => a.storage == AssetState.local), - ); + ref.read(manualUploadProvider.notifier).uploadAssets( + context, + selection.value.where((a) => a.storage == AssetState.local), + ); } finally { processing.value = false; } @@ -323,16 +324,12 @@ class HomePage extends HookConsumerWidget { } else { refreshCount.value++; // set counter back to 0 if user does not request refresh again - Timer(const Duration(seconds: 4), () { - refreshCount.value = 0; - }); + Timer(const Duration(seconds: 4), () => refreshCount.value = 0); } } buildLoadingIndicator() { - Timer(const Duration(seconds: 2), () { - tipOneOpacity.value = 1; - }); + Timer(const Duration(seconds: 2), () => tipOneOpacity.value = 1); return Center( child: Column( @@ -415,7 +412,6 @@ class HomePage extends HookConsumerWidget { selectionAssetState: selectionAssetState.value, onStack: onStack, ), - if (processing.value) const Center(child: ImmichLoadingIndicator()), ], ), ); diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart index 835907d052..6c12742e4c 100644 --- a/mobile/lib/modules/login/ui/login_form.dart +++ b/mobile/lib/modules/login/ui/login_form.dart @@ -48,7 +48,7 @@ class LoginForm extends HookConsumerWidget { /// Fetch the server login credential and enables oAuth login if necessary /// Returns true if successful, false otherwise Future getServerLoginCredential() async { - final serverUrl = serverEndpointController.text.trim(); + final serverUrl = sanitizeUrl(serverEndpointController.text); // Guard empty URL if (serverUrl.isEmpty) { @@ -127,6 +127,12 @@ class LoginForm extends HookConsumerWidget { ); populateTestLoginInfo() { + usernameController.text = 'demo@immich.app'; + passwordController.text = 'demo'; + serverEndpointController.text = 'https://demo.immich.app'; + } + + populateTestLoginInfo1() { usernameController.text = 'testuser@email.com'; passwordController.text = 'password'; serverEndpointController.text = 'http://10.1.15.216:2283/api'; @@ -144,7 +150,7 @@ class LoginForm extends HookConsumerWidget { await ref.read(authenticationProvider.notifier).login( usernameController.text, passwordController.text, - serverEndpointController.text.trim(), + sanitizeUrl(serverEndpointController.text), ); if (isAuthenticated) { // Resume backup (if enable) then navigate @@ -181,7 +187,7 @@ class LoginForm extends HookConsumerWidget { try { oAuthServerConfig = await oAuthService - .getOAuthServerConfig(serverEndpointController.text); + .getOAuthServerConfig(sanitizeUrl(serverEndpointController.text)); isLoading.value = true; } catch (e) { @@ -203,7 +209,7 @@ class LoginForm extends HookConsumerWidget { .watch(authenticationProvider.notifier) .setSuccessLoginInfo( accessToken: loginResponseDto.accessToken, - serverUrl: serverEndpointController.text, + serverUrl: sanitizeUrl(serverEndpointController.text), ); if (isSuccess) { @@ -299,7 +305,7 @@ class LoginForm extends HookConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Text( - serverEndpointController.text, + sanitizeUrl(serverEndpointController.text), style: context.textTheme.displaySmall, textAlign: TextAlign.center, ), @@ -387,6 +393,7 @@ class LoginForm extends HookConsumerWidget { children: [ GestureDetector( onDoubleTap: () => populateTestLoginInfo(), + onLongPress: () => populateTestLoginInfo1(), child: RotationTransition( turns: logoAnimationController, child: const ImmichLogo( diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart index 7377d06181..0c709919b2 100644 --- a/mobile/lib/modules/memories/ui/memory_lane.dart +++ b/mobile/lib/modules/memories/ui/memory_lane.dart @@ -17,7 +17,7 @@ class MemoryLane extends HookConsumerWidget { .whenData( (memories) => memories != null ? Container( - margin: const EdgeInsets.only(top: 10), + margin: const EdgeInsets.only(top: 10, left: 10), height: 200, child: ListView.builder( scrollDirection: Axis.horizontal, diff --git a/mobile/lib/modules/partner/views/partner_detail_page.dart b/mobile/lib/modules/partner/views/partner_detail_page.dart index f7b1580b20..28d53646df 100644 --- a/mobile/lib/modules/partner/views/partner_detail_page.dart +++ b/mobile/lib/modules/partner/views/partner_detail_page.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; class PartnerDetailPage extends HookConsumerWidget { @@ -71,8 +71,8 @@ class PartnerDetailPage extends HookConsumerWidget { ), ], ), - body: assets.when( - data: (renderList) => renderList.isEmpty + body: assets.widgetWhen( + onData: (renderList) => renderList.isEmpty ? Padding( padding: const EdgeInsets.all(16), child: Text( @@ -84,8 +84,6 @@ class PartnerDetailPage extends HookConsumerWidget { onRefresh: () => ref.read(assetProvider.notifier).getPartnerAssets(partner), ), - error: (e, _) => Text("Error loading partners:\n$e"), - loading: () => const Center(child: ImmichLoadingIndicator()), ), ); } diff --git a/mobile/lib/modules/partner/views/partner_page.dart b/mobile/lib/modules/partner/views/partner_page.dart index bb567b62bb..e90250df52 100644 --- a/mobile/lib/modules/partner/views/partner_page.dart +++ b/mobile/lib/modules/partner/views/partner_page.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/modules/partner/services/partner.service.dart'; import 'package:immich_mobile/shared/models/user.dart'; @@ -34,7 +35,7 @@ class PartnerPage extends HookConsumerWidget { children: [ for (User u in users) SimpleDialogOption( - onPressed: () => Navigator.pop(context, u), + onPressed: () => context.pop(u), child: Row( children: [ Padding( @@ -70,8 +71,7 @@ class PartnerPage extends HookConsumerWidget { builder: (BuildContext context) { return ConfirmDialog( title: "partner_page_stop_sharing_title", - content: - "partner_page_stop_sharing_content".tr(args: [u.name]), + content: "partner_page_stop_sharing_content".tr(args: [u.name]), onOk: () => ref.read(partnerServiceProvider).removePartner(u), ); }, @@ -118,6 +118,7 @@ class PartnerPage extends HookConsumerWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.symmetric(vertical: 8), @@ -126,12 +127,15 @@ class PartnerPage extends HookConsumerWidget { style: TextStyle(fontSize: 14), ).tr(), ), - ElevatedButton.icon( - onPressed: availableUsers.whenOrNull( - data: (data) => addNewUsersHandler, + Align( + alignment: Alignment.center, + child: ElevatedButton.icon( + onPressed: availableUsers.whenOrNull( + data: (data) => addNewUsersHandler, + ), + icon: const Icon(Icons.person_add), + label: const Text("partner_page_add_partner").tr(), ), - icon: const Icon(Icons.person_add), - label: const Text("partner_page_add_partner").tr(), ), ], ), diff --git a/mobile/lib/modules/search/views/all_motion_videos_page.dart b/mobile/lib/modules/search/views/all_motion_videos_page.dart index 2ad53ec456..8290f0dd6e 100644 --- a/mobile/lib/modules/search/views/all_motion_videos_page.dart +++ b/mobile/lib/modules/search/views/all_motion_videos_page.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class AllMotionPhotosPage extends HookConsumerWidget { const AllMotionPhotosPage({super.key}); @@ -21,14 +21,10 @@ class AllMotionPhotosPage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: motionPhotos.when( - data: (assets) => ImmichAssetGrid( + body: motionPhotos.widgetWhen( + onData: (assets) => ImmichAssetGrid( assets: assets, ), - error: (e, s) => Text(e.toString()), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart index 3af087e4e7..7a81831482 100644 --- a/mobile/lib/modules/search/views/all_people_page.dart +++ b/mobile/lib/modules/search/views/all_people_page.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class AllPeoplePage extends HookConsumerWidget { const AllPeoplePage({super.key}); @@ -23,12 +23,8 @@ class AllPeoplePage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: curatedPeople.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center( - child: Text('Error: $err'), - ), - data: (people) => ExploreGrid( + body: curatedPeople.widgetWhen( + onData: (people) => ExploreGrid( isPeople: true, curatedContent: people, ), diff --git a/mobile/lib/modules/search/views/all_videos_page.dart b/mobile/lib/modules/search/views/all_videos_page.dart index beb604fd0d..6835398801 100644 --- a/mobile/lib/modules/search/views/all_videos_page.dart +++ b/mobile/lib/modules/search/views/all_videos_page.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class AllVideosPage extends HookConsumerWidget { const AllVideosPage({super.key}); @@ -21,14 +21,10 @@ class AllVideosPage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: videos.when( - data: (assets) => ImmichAssetGrid( + body: videos.widgetWhen( + onData: (assets) => ImmichAssetGrid( assets: assets, ), - error: (e, s) => Text(e.toString()), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/search/views/curated_location_page.dart b/mobile/lib/modules/search/views/curated_location_page.dart index cb6f8f9ae8..6675e0826f 100644 --- a/mobile/lib/modules/search/views/curated_location_page.dart +++ b/mobile/lib/modules/search/views/curated_location_page.dart @@ -1,11 +1,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart'; import 'package:immich_mobile/modules/search/ui/explore_grid.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:openapi/api.dart'; class CuratedLocationPage extends HookConsumerWidget { @@ -26,12 +26,8 @@ class CuratedLocationPage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: curatedLocation.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center( - child: Text('Error: $err'), - ), - data: (curatedLocations) => ExploreGrid( + body: curatedLocation.widgetWhen( + onData: (curatedLocations) => ExploreGrid( curatedContent: curatedLocations .map( (l) => CuratedContent( diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart index 004e3c6578..40a2d1b14b 100644 --- a/mobile/lib/modules/search/views/person_result_page.dart +++ b/mobile/lib/modules/search/views/person_result_page.dart @@ -8,7 +8,6 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart' import 'package:immich_mobile/modules/search/providers/people.provider.dart'; import 'package:immich_mobile/modules/search/ui/person_name_edit_form.dart'; import 'package:immich_mobile/shared/models/store.dart' as isar_store; -import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; class PersonResultPage extends HookConsumerWidget { @@ -112,7 +111,7 @@ class PersonResultPage extends HookConsumerWidget { ), ], ), - body: ref.watch(personAssetsProvider(personId)).scaffoldBodyWhen( + body: ref.watch(personAssetsProvider(personId)).widgetWhen( onData: (renderList) => ImmichAssetGrid( renderList: renderList, topWidget: Padding( @@ -137,7 +136,6 @@ class PersonResultPage extends HookConsumerWidget { ), ), ), - onError: const ScaffoldErrorBody(icon: Icons.person_off_outlined), ), ); } diff --git a/mobile/lib/modules/search/views/recently_added_page.dart b/mobile/lib/modules/search/views/recently_added_page.dart index 55e9206152..538dea3d71 100644 --- a/mobile/lib/modules/search/views/recently_added_page.dart +++ b/mobile/lib/modules/search/views/recently_added_page.dart @@ -1,10 +1,10 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class RecentlyAddedPage extends HookConsumerWidget { const RecentlyAddedPage({super.key}); @@ -21,14 +21,10 @@ class RecentlyAddedPage extends HookConsumerWidget { icon: const Icon(Icons.arrow_back_ios_rounded), ), ), - body: recents.when( - data: (searchResponse) => ImmichAssetGrid( + body: recents.widgetWhen( + onData: (searchResponse) => ImmichAssetGrid( assets: searchResponse, ), - error: (e, s) => Text(e.toString()), - loading: () => const Center( - child: ImmichLoadingIndicator(), - ), ), ); } diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart index 47ee66dc5e..fb4bd49794 100644 --- a/mobile/lib/modules/search/views/search_page.dart +++ b/mobile/lib/modules/search/views/search_page.dart @@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/search/models/curated_content.dart'; import 'package:immich_mobile/modules/search/providers/people.provider.dart'; @@ -15,7 +16,7 @@ import 'package:immich_mobile/modules/search/ui/search_row_title.dart'; import 'package:immich_mobile/modules/search/ui/search_suggestion_list.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/scaffold_error_body.dart'; // ignore: must_be_immutable class SearchPage extends HookConsumerWidget { @@ -73,10 +74,9 @@ class SearchPage extends HookConsumerWidget { buildPeople() { return SizedBox( height: imageSize, - child: curatedPeople.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), - data: (people) => CuratedPeopleRow( + child: curatedPeople.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (people) => CuratedPeopleRow( content: people.take(12).toList(), onTap: (content, index) { context.autoPush( @@ -97,10 +97,9 @@ class SearchPage extends HookConsumerWidget { buildPlaces() { return SizedBox( height: imageSize, - child: curatedLocation.when( - loading: () => const Center(child: ImmichLoadingIndicator()), - error: (err, stack) => Center(child: Text('Error: $err')), - data: (locations) => CuratedPlacesRow( + child: curatedLocation.widgetWhen( + onError: (error, stack) => const ScaffoldErrorBody(withIcon: false), + onData: (locations) => CuratedPlacesRow( isMapEnabled: isMapEnabled, content: locations .map( diff --git a/mobile/lib/modules/settings/views/settings_page.dart b/mobile/lib/modules/settings/views/settings_page.dart index f6f66cb412..0a1651de95 100644 --- a/mobile/lib/modules/settings/views/settings_page.dart +++ b/mobile/lib/modules/settings/views/settings_page.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/settings/ui/advanced_settings/advanced_settings.dart'; import 'package:immich_mobile/modules/settings/ui/asset_list_settings/asset_list_settings.dart'; import 'package:immich_mobile/modules/settings/ui/local_storage_settings/local_storage_settings.dart'; @@ -18,9 +19,7 @@ class SettingsPage extends HookConsumerWidget { leading: IconButton( iconSize: 20, splashRadius: 24, - onPressed: () { - Navigator.pop(context); - }, + onPressed: () => context.pop(), icon: const Icon(Icons.arrow_back_ios_new_rounded), ), automaticallyImplyLeading: false, diff --git a/mobile/lib/modules/shared_link/ui/shared_link_item.dart b/mobile/lib/modules/shared_link/ui/shared_link_item.dart index 85bfa4445f..8605bdeaf8 100644 --- a/mobile/lib/modules/shared_link/ui/shared_link_item.dart +++ b/mobile/lib/modules/shared_link/ui/shared_link_item.dart @@ -1,4 +1,5 @@ import 'dart:math' as math; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -26,13 +27,13 @@ class SharedLinkItem extends ConsumerWidget { } Widget getExpiryDuration(bool isDarkMode) { - var expiresText = "Expires ∞"; + var expiresText = "shared_link_expires_never".tr(); if (sharedLink.expiresAt != null) { if (isExpired()) { return Text( - "Expired", + "shared_link_expired", style: TextStyle(color: Colors.red[300]), - ); + ).tr(); } final difference = sharedLink.expiresAt!.difference(DateTime.now()); debugPrint("Difference: $difference"); @@ -41,13 +42,15 @@ class SharedLinkItem extends ConsumerWidget { if (difference.inHours % 24 > 12) { dayDifference += 1; } - expiresText = "in $dayDifference days"; + expiresText = "shared_link_expires_days".plural(dayDifference); } else if (difference.inHours > 0) { - expiresText = "in ${difference.inHours} hours"; + expiresText = "shared_link_expires_hours".plural(difference.inHours); } else if (difference.inMinutes > 0) { - expiresText = "in ${difference.inMinutes} minutes"; + expiresText = + "shared_link_expires_minutes".plural(difference.inMinutes); } else if (difference.inSeconds > 0) { - expiresText = "in ${difference.inSeconds} seconds"; + expiresText = + "shared_link_expires_seconds".plural(difference.inSeconds); } } return Text( @@ -72,7 +75,7 @@ class SharedLinkItem extends ConsumerWidget { context: context, gravity: ToastGravity.BOTTOM, toastType: ToastType.error, - msg: 'Cannot fetch the server url', + msg: "shared_link_error_server_url_fetch".tr(), ); return; } @@ -83,11 +86,14 @@ class SharedLinkItem extends ConsumerWidget { ), ).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( + SnackBar( content: Text( - "Copied to clipboard", - ), - duration: Duration(seconds: 2), + "shared_link_clipboard_copied_massage", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ).tr(), + duration: const Duration(seconds: 2), ), ); }); @@ -163,9 +169,12 @@ class SharedLinkItem extends ConsumerWidget { Widget buildBottomInfo() { return Row( children: [ - if (sharedLink.allowUpload) buildInfoChip("Upload"), - if (sharedLink.allowDownload) buildInfoChip("Download"), - if (sharedLink.showMetadata) buildInfoChip("EXIF"), + if (sharedLink.allowUpload) + buildInfoChip("shared_link_info_chip_upload".tr()), + if (sharedLink.allowDownload) + buildInfoChip("shared_link_info_chip_download".tr()), + if (sharedLink.showMetadata) + buildInfoChip("shared_link_info_chip_metadata".tr()), ], ); } diff --git a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart index df14ba3d9d..fe2212cc21 100644 --- a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart +++ b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart @@ -275,7 +275,12 @@ class SharedLinkEditPage extends HookConsumerWidget { ).then((_) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: const Text("shared_link_clipboard_copied_massage").tr(), + content: Text( + "shared_link_clipboard_copied_massage", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ).tr(), duration: const Duration(seconds: 2), ), ); diff --git a/mobile/lib/modules/shared_link/views/shared_link_page.dart b/mobile/lib/modules/shared_link/views/shared_link_page.dart index f878f121e3..7638441b17 100644 --- a/mobile/lib/modules/shared_link/views/shared_link_page.dart +++ b/mobile/lib/modules/shared_link/views/shared_link_page.dart @@ -2,11 +2,11 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/shared_link/models/shared_link.dart'; import 'package:immich_mobile/modules/shared_link/providers/shared_link.provider.dart'; import 'package:immich_mobile/modules/shared_link/ui/shared_link_item.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; class SharedLinkPage extends HookConsumerWidget { const SharedLinkPage({Key? key}) : super(key: key); @@ -18,7 +18,10 @@ class SharedLinkPage extends HookConsumerWidget { useEffect( () { ref.read(sharedLinksStateProvider.notifier).fetchLinks(); - return () => ref.invalidate(sharedLinksStateProvider); + return () { + if (!context.mounted) return; + ref.invalidate(sharedLinksStateProvider); + }; }, [], ); @@ -113,11 +116,10 @@ class SharedLinkPage extends HookConsumerWidget { centerTitle: false, ), body: SafeArea( - child: sharedLinks.when( - data: (links) => + child: sharedLinks.widgetWhen( + onError: (error, stackTrace) => buildNoShares(), + onData: (links) => links.isNotEmpty ? buildSharesList(links) : buildNoShares(), - error: (error, stackTrace) => buildNoShares(), - loading: () => const Center(child: ImmichLoadingIndicator()), ), ), ); diff --git a/mobile/lib/modules/trash/views/trash_page.dart b/mobile/lib/modules/trash/views/trash_page.dart index ad365189ea..88fd32d013 100644 --- a/mobile/lib/modules/trash/views/trash_page.dart +++ b/mobile/lib/modules/trash/views/trash_page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/modules/home/ui/delete_dialog.dart'; @@ -11,8 +12,8 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/providers/server_info.provider.dart'; import 'package:immich_mobile/shared/ui/confirm_dialog.dart'; -import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; import 'package:immich_mobile/shared/ui/immich_toast.dart'; +import 'package:immich_mobile/shared/views/immich_loading_overlay.dart'; class TrashPage extends HookConsumerWidget { const TrashPage({super.key}); @@ -24,7 +25,7 @@ class TrashPage extends HookConsumerWidget { ref.watch(serverInfoProvider.select((v) => v.serverConfig.trashDays)); final selectionEnabledHook = useState(false); final selection = useState({}); - final processing = useState(false); + final processing = useProcessingOverlay(); void selectionListener( bool multiselect, @@ -229,18 +230,13 @@ class TrashPage extends HookConsumerWidget { ); } - return trashedAssets.when( - loading: () => Scaffold( - appBar: buildAppBar("?"), - body: const Center(child: CircularProgressIndicator()), + return Scaffold( + appBar: trashedAssets.maybeWhen( + orElse: () => buildAppBar("?"), + data: (data) => buildAppBar(data.totalAssets.toString()), ), - error: (error, stackTrace) => Scaffold( - appBar: buildAppBar("!"), - body: Center(child: Text(error.toString())), - ), - data: (data) => Scaffold( - appBar: buildAppBar(data.totalAssets.toString()), - body: data.isEmpty + body: trashedAssets.widgetWhen( + onData: (data) => data.isEmpty ? Center( child: Text('trash_page_no_assets'.tr()), ) @@ -254,11 +250,9 @@ class TrashPage extends HookConsumerWidget { showMultiSelectIndicator: false, showStack: true, topWidget: Padding( - padding: const EdgeInsets.only( - top: 24, - bottom: 24, - left: 12, - right: 12, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 24, ), child: const Text( "trash_page_info", @@ -267,8 +261,6 @@ class TrashPage extends HookConsumerWidget { ), ), if (selectionEnabledHook.value) buildBottomBar(), - if (processing.value) - const Center(child: ImmichLoadingIndicator()), ], ), ), diff --git a/mobile/lib/shared/ui/immich_loading_indicator.dart b/mobile/lib/shared/ui/immich_loading_indicator.dart index db5dd3c199..24eedcd47e 100644 --- a/mobile/lib/shared/ui/immich_loading_indicator.dart +++ b/mobile/lib/shared/ui/immich_loading_indicator.dart @@ -21,7 +21,7 @@ class ImmichLoadingIndicator extends StatelessWidget { padding: const EdgeInsets.all(15), child: const CircularProgressIndicator( color: Colors.white, - strokeWidth: 2, + strokeWidth: 3, ), ); } diff --git a/mobile/lib/shared/ui/scaffold_error_body.dart b/mobile/lib/shared/ui/scaffold_error_body.dart index 5c29f7c2a9..fef6bef59e 100644 --- a/mobile/lib/shared/ui/scaffold_error_body.dart +++ b/mobile/lib/shared/ui/scaffold_error_body.dart @@ -4,9 +4,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; // Error widget to be used in Scaffold when an AsyncError is received class ScaffoldErrorBody extends StatelessWidget { - final IconData icon; + final bool withIcon; - const ScaffoldErrorBody({this.icon = Icons.error_outline, super.key}); + const ScaffoldErrorBody({super.key, this.withIcon = true}); @override Widget build(BuildContext context) { @@ -14,19 +14,22 @@ class ScaffoldErrorBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ - const Text( + Text( "scaffold_body_error_occured", - style: - TextStyle(fontSize: 14, fontWeight: FontWeight.bold, height: 3), + style: context.textTheme.displayMedium, textAlign: TextAlign.center, ).tr(), - Center( - child: Icon( - icon, - size: 100, - color: context.themeData.iconTheme.color?.withOpacity(0.5), + if (withIcon) + Center( + child: Padding( + padding: const EdgeInsets.only(top: 15), + child: Icon( + Icons.error_outline, + size: 100, + color: context.themeData.iconTheme.color?.withOpacity(0.5), + ), + ), ), - ), ], ); } diff --git a/mobile/lib/shared/views/app_log_detail_page.dart b/mobile/lib/shared/views/app_log_detail_page.dart index f8ddf51918..8b737fb5cc 100644 --- a/mobile/lib/shared/views/app_log_detail_page.dart +++ b/mobile/lib/shared/views/app_log_detail_page.dart @@ -39,7 +39,14 @@ class AppLogDetailPage extends HookConsumerWidget { Clipboard.setData(ClipboardData(text: stackTrace)) .then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Copied to clipboard")), + SnackBar( + content: Text( + "Copied to clipboard", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), ); }); }, @@ -98,7 +105,14 @@ class AppLogDetailPage extends HookConsumerWidget { onPressed: () { Clipboard.setData(ClipboardData(text: message)).then((_) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Copied to clipboard")), + SnackBar( + content: Text( + "Copied to clipboard", + style: context.textTheme.bodyLarge?.copyWith( + color: context.primaryColor, + ), + ), + ), ); }); }, diff --git a/mobile/lib/shared/views/immich_loading_overlay.dart b/mobile/lib/shared/views/immich_loading_overlay.dart index 6e4ef166bc..85f0123ed9 100644 --- a/mobile/lib/shared/views/immich_loading_overlay.dart +++ b/mobile/lib/shared/views/immich_loading_overlay.dart @@ -1,41 +1,64 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; -class ImmichLoadingOverlay extends StatelessWidget { - const ImmichLoadingOverlay({ - Key? key, - }) : super(key: key); +final _loadingEntry = OverlayEntry( + builder: (context) => SizedBox.square( + dimension: double.infinity, + child: DecoratedBox( + decoration: + BoxDecoration(color: context.colorScheme.surface.withAlpha(200)), + child: const Center(child: ImmichLoadingIndicator()), + ), + ), +); + +ValueNotifier useProcessingOverlay() { + return use(const _LoadingOverlay()); +} + +class _LoadingOverlay extends Hook> { + const _LoadingOverlay(); @override - Widget build(BuildContext context) { - return ValueListenableBuilder( - valueListenable: - ImmichLoadingOverlayController.appLoader.loaderShowingNotifier, - builder: (context, shouldShow, child) { - return shouldShow - ? const Scaffold( - backgroundColor: Colors.black54, - body: Center( - child: ImmichLoadingIndicator(), - ), - ) - : const SizedBox(); - }, - ); - } + _LoadingOverlayState createState() => _LoadingOverlayState(); } -class ImmichLoadingOverlayController { - static final ImmichLoadingOverlayController appLoader = - ImmichLoadingOverlayController(); - ValueNotifier loaderShowingNotifier = ValueNotifier(false); - ValueNotifier loaderTextNotifier = ValueNotifier('error message'); +class _LoadingOverlayState + extends HookState, _LoadingOverlay> { + late final _isProcessing = ValueNotifier(false)..addListener(_listener); + OverlayEntry? overlayEntry; - void show() { - loaderShowingNotifier.value = true; + void _listener() { + setState(() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_isProcessing.value) { + overlayEntry?.remove(); + overlayEntry = _loadingEntry; + Overlay.of(context).insert(_loadingEntry); + } else { + overlayEntry?.remove(); + overlayEntry = null; + } + }); + }); } - void hide() { - loaderShowingNotifier.value = false; + @override + ValueNotifier build(BuildContext context) { + return _isProcessing; } + + @override + void dispose() { + _isProcessing.dispose(); + super.dispose(); + } + + @override + Object? get debugValue => _isProcessing.value; + + @override + String get debugLabel => 'useProcessingOverlay<>'; } diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 76977b4461..4313da60f9 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -48,8 +48,8 @@ ThemeData immichLightTheme = ThemeData( ), backgroundColor: Colors.white, ), - appBarTheme: AppBarTheme( - titleTextStyle: const TextStyle( + appBarTheme: const AppBarTheme( + titleTextStyle: TextStyle( fontFamily: 'Overpass', color: Colors.indigo, fontWeight: FontWeight.bold, @@ -61,7 +61,7 @@ ThemeData immichLightTheme = ThemeData( scrolledUnderElevation: 0, centerTitle: true, ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, backgroundColor: immichBackgroundColor, selectedItemColor: Colors.indigo, @@ -69,7 +69,7 @@ ThemeData immichLightTheme = ThemeData( cardTheme: const CardTheme( surfaceTintColor: Colors.transparent, ), - drawerTheme: DrawerThemeData( + drawerTheme: const DrawerThemeData( backgroundColor: immichBackgroundColor, ), textTheme: const TextTheme( @@ -162,7 +162,7 @@ ThemeData immichDarkTheme = ThemeData( hintColor: Colors.grey[600], fontFamily: 'Overpass', snackBarTheme: SnackBarThemeData( - contentTextStyle: TextStyle( + contentTextStyle: const TextStyle( fontFamily: 'Overpass', color: immichDarkThemePrimaryColor, fontWeight: FontWeight.bold, @@ -174,35 +174,35 @@ ThemeData immichDarkTheme = ThemeData( foregroundColor: immichDarkThemePrimaryColor, ), ), - appBarTheme: AppBarTheme( + appBarTheme: const AppBarTheme( titleTextStyle: TextStyle( fontFamily: 'Overpass', color: immichDarkThemePrimaryColor, fontWeight: FontWeight.bold, fontSize: 18, ), - backgroundColor: const Color.fromARGB(255, 32, 33, 35), + backgroundColor: Color.fromARGB(255, 32, 33, 35), foregroundColor: immichDarkThemePrimaryColor, elevation: 0, scrolledUnderElevation: 0, centerTitle: true, ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( type: BottomNavigationBarType.fixed, - backgroundColor: const Color.fromARGB(255, 35, 36, 37), + backgroundColor: Color.fromARGB(255, 35, 36, 37), selectedItemColor: immichDarkThemePrimaryColor, ), drawerTheme: DrawerThemeData( backgroundColor: immichDarkBackgroundColor, scrimColor: Colors.white.withOpacity(0.1), ), - textTheme: TextTheme( - displayLarge: const TextStyle( + textTheme: const TextTheme( + displayLarge: TextStyle( fontSize: 26, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 255, 255, 255), ), - displayMedium: const TextStyle( + displayMedium: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Color.fromARGB(255, 255, 255, 255), @@ -212,15 +212,15 @@ ThemeData immichDarkTheme = ThemeData( fontWeight: FontWeight.bold, color: immichDarkThemePrimaryColor, ), - titleSmall: const TextStyle( + titleSmall: TextStyle( fontSize: 16.0, fontWeight: FontWeight.bold, ), - titleMedium: const TextStyle( + titleMedium: TextStyle( fontSize: 18.0, fontWeight: FontWeight.bold, ), - titleLarge: const TextStyle( + titleLarge: TextStyle( fontSize: 26.0, fontWeight: FontWeight.bold, ), @@ -258,7 +258,7 @@ ThemeData immichDarkTheme = ThemeData( dialogTheme: const DialogTheme( surfaceTintColor: Colors.transparent, ), - inputDecorationTheme: InputDecorationTheme( + inputDecorationTheme: const InputDecorationTheme( focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: immichDarkThemePrimaryColor, @@ -267,12 +267,12 @@ ThemeData immichDarkTheme = ThemeData( labelStyle: TextStyle( color: immichDarkThemePrimaryColor, ), - hintStyle: const TextStyle( + hintStyle: TextStyle( fontSize: 14.0, fontWeight: FontWeight.normal, ), ), - textSelectionTheme: TextSelectionThemeData( + textSelectionTheme: const TextSelectionThemeData( cursorColor: immichDarkThemePrimaryColor, ), ); diff --git a/mobile/lib/utils/url_helper.dart b/mobile/lib/utils/url_helper.dart index b771a6f705..95fcd80786 100644 --- a/mobile/lib/utils/url_helper.dart +++ b/mobile/lib/utils/url_helper.dart @@ -3,10 +3,10 @@ import 'package:immich_mobile/shared/models/store.dart'; String sanitizeUrl(String url) { // Add schema if none is set final urlWithSchema = - url.startsWith(RegExp(r"https?://")) ? url : "https://$url"; + url.trimLeft().startsWith(RegExp(r"https?://")) ? url : "https://$url"; // Remove trailing slash(es) - return urlWithSchema.replaceFirst(RegExp(r"/+$"), ""); + return urlWithSchema.trimRight().replaceFirst(RegExp(r"/+$"), ""); } String? getServerUrl() { diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 10f10fb01a..f54b788a4e 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -46,7 +46,6 @@ doc/CQMode.md doc/ChangePasswordDto.md doc/CheckExistingAssetsDto.md doc/CheckExistingAssetsResponseDto.md -doc/CitiesFile.md doc/ClassificationConfig.md doc/Colorspace.md doc/CreateAlbumDto.md @@ -231,7 +230,6 @@ lib/model/bulk_ids_dto.dart lib/model/change_password_dto.dart lib/model/check_existing_assets_dto.dart lib/model/check_existing_assets_response_dto.dart -lib/model/cities_file.dart lib/model/classification_config.dart lib/model/clip_config.dart lib/model/clip_mode.dart @@ -388,7 +386,6 @@ test/bulk_ids_dto_test.dart test/change_password_dto_test.dart test/check_existing_assets_dto_test.dart test/check_existing_assets_response_dto_test.dart -test/cities_file_test.dart test/classification_config_test.dart test/clip_config_test.dart test/clip_mode_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1c307eaac4..903919c050 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -98,6 +98,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**downloadFile**](doc//AssetApi.md#downloadfile) | **POST** /asset/download/{id} | *AssetApi* | [**emptyTrash**](doc//AssetApi.md#emptytrash) | **POST** /asset/trash/empty | *AssetApi* | [**getAllAssets**](doc//AssetApi.md#getallassets) | **GET** /asset | +*AssetApi* | [**getAllUserAssetsByDeviceId**](doc//AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | *AssetApi* | [**getAssetById**](doc//AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | *AssetApi* | [**getAssetSearchTerms**](doc//AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | *AssetApi* | [**getAssetStatistics**](doc//AssetApi.md#getassetstatistics) | **GET** /asset/statistics | @@ -110,7 +111,7 @@ Class | Method | HTTP request | Description *AssetApi* | [**getRandom**](doc//AssetApi.md#getrandom) | **GET** /asset/random | *AssetApi* | [**getTimeBucket**](doc//AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | *AssetApi* | [**getTimeBuckets**](doc//AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | +*AssetApi* | [**getUserAssetsByDeviceId**](doc//AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | Use /asset/device/:deviceId instead - Remove in 1.92 release *AssetApi* | [**restoreAssets**](doc//AssetApi.md#restoreassets) | **POST** /asset/restore | *AssetApi* | [**restoreTrash**](doc//AssetApi.md#restoretrash) | **POST** /asset/trash/restore | *AssetApi* | [**runAssetJobs**](doc//AssetApi.md#runassetjobs) | **POST** /asset/jobs | @@ -243,7 +244,6 @@ Class | Method | HTTP request | Description - [ChangePasswordDto](doc//ChangePasswordDto.md) - [CheckExistingAssetsDto](doc//CheckExistingAssetsDto.md) - [CheckExistingAssetsResponseDto](doc//CheckExistingAssetsResponseDto.md) - - [CitiesFile](doc//CitiesFile.md) - [ClassificationConfig](doc//ClassificationConfig.md) - [Colorspace](doc//Colorspace.md) - [CreateAlbumDto](doc//CreateAlbumDto.md) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 16f7ef94d7..b479c08f34 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -16,6 +16,7 @@ Method | HTTP request | Description [**downloadFile**](AssetApi.md#downloadfile) | **POST** /asset/download/{id} | [**emptyTrash**](AssetApi.md#emptytrash) | **POST** /asset/trash/empty | [**getAllAssets**](AssetApi.md#getallassets) | **GET** /asset | +[**getAllUserAssetsByDeviceId**](AssetApi.md#getalluserassetsbydeviceid) | **GET** /asset/device/{deviceId} | [**getAssetById**](AssetApi.md#getassetbyid) | **GET** /asset/assetById/{id} | [**getAssetSearchTerms**](AssetApi.md#getassetsearchterms) | **GET** /asset/search-terms | [**getAssetStatistics**](AssetApi.md#getassetstatistics) | **GET** /asset/statistics | @@ -28,7 +29,7 @@ Method | HTTP request | Description [**getRandom**](AssetApi.md#getrandom) | **GET** /asset/random | [**getTimeBucket**](AssetApi.md#gettimebucket) | **GET** /asset/time-bucket | [**getTimeBuckets**](AssetApi.md#gettimebuckets) | **GET** /asset/time-buckets | -[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | +[**getUserAssetsByDeviceId**](AssetApi.md#getuserassetsbydeviceid) | **GET** /asset/{deviceId} | Use /asset/device/:deviceId instead - Remove in 1.92 release [**restoreAssets**](AssetApi.md#restoreassets) | **POST** /asset/restore | [**restoreTrash**](AssetApi.md#restoretrash) | **POST** /asset/trash/restore | [**runAssetJobs**](AssetApi.md#runassetjobs) | **POST** /asset/jobs | @@ -443,6 +444,63 @@ Name | Type | Description | Notes [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) +# **getAllUserAssetsByDeviceId** +> List getAllUserAssetsByDeviceId(deviceId) + + + +Get all asset of a device that are in the database, ID only. + +### Example +```dart +import 'package:openapi/api.dart'; +// TODO Configure API key authorization: cookie +//defaultApiClient.getAuthentication('cookie').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('cookie').apiKeyPrefix = 'Bearer'; +// TODO Configure API key authorization: api_key +//defaultApiClient.getAuthentication('api_key').apiKey = 'YOUR_API_KEY'; +// uncomment below to setup prefix (e.g. Bearer) for API key, if needed +//defaultApiClient.getAuthentication('api_key').apiKeyPrefix = 'Bearer'; +// TODO Configure HTTP Bearer authorization: bearer +// Case 1. Use String Token +//defaultApiClient.getAuthentication('bearer').setAccessToken('YOUR_ACCESS_TOKEN'); +// Case 2. Use Function which generate token. +// String yourTokenGeneratorFunction() { ... } +//defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); + +final api_instance = AssetApi(); +final deviceId = deviceId_example; // String | + +try { + final result = api_instance.getAllUserAssetsByDeviceId(deviceId); + print(result); +} catch (e) { + print('Exception when calling AssetApi->getAllUserAssetsByDeviceId: $e\n'); +} +``` + +### Parameters + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **deviceId** | **String**| | + +### Return type + +**List** + +### Authorization + +[cookie](../README.md#cookie), [api_key](../README.md#api_key), [bearer](../README.md#bearer) + +### HTTP request headers + + - **Content-Type**: Not defined + - **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md) + # **getAssetById** > AssetResponseDto getAssetById(id, key) @@ -1154,9 +1212,7 @@ Name | Type | Description | Notes # **getUserAssetsByDeviceId** > List getUserAssetsByDeviceId(deviceId) - - -Get all asset of a device that are in the database, ID only. +Use /asset/device/:deviceId instead - Remove in 1.92 release ### Example ```dart @@ -1177,7 +1233,7 @@ import 'package:openapi/api.dart'; //defaultApiClient.getAuthentication('bearer').setAccessToken(yourTokenGeneratorFunction); final api_instance = AssetApi(); -final deviceId = 38400000-8cf0-11bd-b23e-10b96e4ef00d; // String | +final deviceId = deviceId_example; // String | try { final result = api_instance.getUserAssetsByDeviceId(deviceId); diff --git a/mobile/openapi/doc/CitiesFile.md b/mobile/openapi/doc/CitiesFile.md deleted file mode 100644 index 9acca959c7..0000000000 --- a/mobile/openapi/doc/CitiesFile.md +++ /dev/null @@ -1,14 +0,0 @@ -# openapi.model.CitiesFile - -## Load the model package -```dart -import 'package:openapi/api.dart'; -``` - -## Properties -Name | Type | Description | Notes ------------- | ------------- | ------------- | ------------- - -[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) - - diff --git a/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md b/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md index 36eab47477..9fca6c2094 100644 --- a/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md +++ b/mobile/openapi/doc/SystemConfigReverseGeocodingDto.md @@ -8,7 +8,6 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- -**citiesFileOverride** | [**CitiesFile**](CitiesFile.md) | | **enabled** | **bool** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3052d5d8b2..8941626933 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -83,7 +83,6 @@ part 'model/cq_mode.dart'; part 'model/change_password_dto.dart'; part 'model/check_existing_assets_dto.dart'; part 'model/check_existing_assets_response_dto.dart'; -part 'model/cities_file.dart'; part 'model/classification_config.dart'; part 'model/colorspace.dart'; part 'model/create_album_dto.dart'; diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 366c83d57e..45c1e11043 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -414,6 +414,62 @@ class AssetApi { return null; } + /// Get all asset of a device that are in the database, ID only. + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] deviceId (required): + Future getAllUserAssetsByDeviceIdWithHttpInfo(String deviceId,) async { + // ignore: prefer_const_declarations + final path = r'/asset/device/{deviceId}' + .replaceAll('{deviceId}', deviceId); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Get all asset of a device that are in the database, ID only. + /// + /// Parameters: + /// + /// * [String] deviceId (required): + Future?> getAllUserAssetsByDeviceId(String deviceId,) async { + final response = await getAllUserAssetsByDeviceIdWithHttpInfo(deviceId,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(); + + } + return null; + } + /// Get a single asset's information /// /// Note: This method returns the HTTP [Response]. @@ -1211,7 +1267,7 @@ class AssetApi { return null; } - /// Get all asset of a device that are in the database, ID only. + /// Use /asset/device/:deviceId instead - Remove in 1.92 release /// /// Note: This method returns the HTTP [Response]. /// @@ -1244,7 +1300,7 @@ class AssetApi { ); } - /// Get all asset of a device that are in the database, ID only. + /// Use /asset/device/:deviceId instead - Remove in 1.92 release /// /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 77a9997010..42a0e5cbb3 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -255,8 +255,6 @@ class ApiClient { return CheckExistingAssetsDto.fromJson(value); case 'CheckExistingAssetsResponseDto': return CheckExistingAssetsResponseDto.fromJson(value); - case 'CitiesFile': - return CitiesFileTypeTransformer().decode(value); case 'ClassificationConfig': return ClassificationConfig.fromJson(value); case 'Colorspace': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index d3f7971e3e..728a4ed833 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -73,9 +73,6 @@ String parameterToString(dynamic value) { if (value is CQMode) { return CQModeTypeTransformer().encode(value).toString(); } - if (value is CitiesFile) { - return CitiesFileTypeTransformer().encode(value).toString(); - } if (value is Colorspace) { return ColorspaceTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/cities_file.dart b/mobile/openapi/lib/model/cities_file.dart deleted file mode 100644 index 96f5d8e573..0000000000 --- a/mobile/openapi/lib/model/cities_file.dart +++ /dev/null @@ -1,91 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class CitiesFile { - /// Instantiate a new enum with the provided [value]. - const CitiesFile._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const cities15000 = CitiesFile._(r'cities15000'); - static const cities5000 = CitiesFile._(r'cities5000'); - static const cities1000 = CitiesFile._(r'cities1000'); - static const cities500 = CitiesFile._(r'cities500'); - - /// List of all possible values in this [enum][CitiesFile]. - static const values = [ - cities15000, - cities5000, - cities1000, - cities500, - ]; - - static CitiesFile? fromJson(dynamic value) => CitiesFileTypeTransformer().decode(value); - - static List? listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = CitiesFile.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [CitiesFile] to String, -/// and [decode] dynamic data back to [CitiesFile]. -class CitiesFileTypeTransformer { - factory CitiesFileTypeTransformer() => _instance ??= const CitiesFileTypeTransformer._(); - - const CitiesFileTypeTransformer._(); - - String encode(CitiesFile data) => data.value; - - /// Decodes a [dynamic value][data] to a CitiesFile. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - CitiesFile? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'cities15000': return CitiesFile.cities15000; - case r'cities5000': return CitiesFile.cities5000; - case r'cities1000': return CitiesFile.cities1000; - case r'cities500': return CitiesFile.cities500; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [CitiesFileTypeTransformer] instance. - static CitiesFileTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart index 727e5534fd..d995d96673 100644 --- a/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart +++ b/mobile/openapi/lib/model/system_config_reverse_geocoding_dto.dart @@ -13,31 +13,25 @@ part of openapi.api; class SystemConfigReverseGeocodingDto { /// Returns a new [SystemConfigReverseGeocodingDto] instance. SystemConfigReverseGeocodingDto({ - required this.citiesFileOverride, required this.enabled, }); - CitiesFile citiesFileOverride; - bool enabled; @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigReverseGeocodingDto && - other.citiesFileOverride == citiesFileOverride && other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis - (citiesFileOverride.hashCode) + (enabled.hashCode); @override - String toString() => 'SystemConfigReverseGeocodingDto[citiesFileOverride=$citiesFileOverride, enabled=$enabled]'; + String toString() => 'SystemConfigReverseGeocodingDto[enabled=$enabled]'; Map toJson() { final json = {}; - json[r'citiesFileOverride'] = this.citiesFileOverride; json[r'enabled'] = this.enabled; return json; } @@ -50,7 +44,6 @@ class SystemConfigReverseGeocodingDto { final json = value.cast(); return SystemConfigReverseGeocodingDto( - citiesFileOverride: CitiesFile.fromJson(json[r'citiesFileOverride'])!, enabled: mapValueOfType(json, r'enabled')!, ); } @@ -99,7 +92,6 @@ class SystemConfigReverseGeocodingDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'citiesFileOverride', 'enabled', }; } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 50c35d2890..c4f6c85112 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -58,6 +58,13 @@ void main() { // TODO }); + // Get all asset of a device that are in the database, ID only. + // + //Future> getAllUserAssetsByDeviceId(String deviceId) async + test('test getAllUserAssetsByDeviceId', () async { + // TODO + }); + // Get a single asset's information // //Future getAssetById(String id, { String key }) async @@ -120,7 +127,7 @@ void main() { // TODO }); - // Get all asset of a device that are in the database, ID only. + // Use /asset/device/:deviceId instead - Remove in 1.92 release // //Future> getUserAssetsByDeviceId(String deviceId) async test('test getUserAssetsByDeviceId', () async { diff --git a/mobile/openapi/test/cities_file_test.dart b/mobile/openapi/test/cities_file_test.dart deleted file mode 100644 index cfe63b7548..0000000000 --- a/mobile/openapi/test/cities_file_test.dart +++ /dev/null @@ -1,21 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.12 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -import 'package:openapi/api.dart'; -import 'package:test/test.dart'; - -// tests for CitiesFile -void main() { - - group('test CitiesFile', () { - - }); - -} diff --git a/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart b/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart index 12f7655ead..b4aa477df3 100644 --- a/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart +++ b/mobile/openapi/test/system_config_reverse_geocoding_dto_test.dart @@ -16,11 +16,6 @@ void main() { // final instance = SystemConfigReverseGeocodingDto(); group('test SystemConfigReverseGeocodingDto', () { - // CitiesFile citiesFileOverride - test('to test the property `citiesFileOverride`', () async { - // TODO - }); - // bool enabled test('to test the property `enabled`', () async { // TODO diff --git a/renovate.json b/renovate.json index d8f272e687..12124c359c 100644 --- a/renovate.json +++ b/renovate.json @@ -1,40 +1,64 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:base"], + "extends": ["config:base", "docker:pinDigests"], + "minimumReleaseAge": "5 days", "packageRules": [ { "matchFileNames": ["cli/**"], "groupName": "@immich/cli", - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" + }, + { + "matchFileNames": ["docs/**"], + "groupName": "docs", + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" }, { "matchFileNames": ["mobile/**"], "groupName": "mobile", - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" }, { "matchFileNames": ["server/**"], "groupName": "server", "matchUpdateTypes": ["minor", "patch"], - "excludePackagePrefixes": ["exiftool"] + "excludePackagePrefixes": ["exiftool"], + "schedule": "on tuesday" }, { "groupName": "exiftool", - "matchPackagePrefixes": ["exiftool"] + "matchPackagePrefixes": ["exiftool"], + "schedule": "on tuesday" }, { "matchFileNames": ["web/**"], "groupName": "web", - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" }, { "matchFileNames": ["machine-learning/**"], "groupName": "machine-learning", - "matchUpdateTypes": ["minor", "patch"] + "matchUpdateTypes": ["minor", "patch"], + "schedule": "on tuesday" }, { "matchFileNames": [".github/**"], - "groupName": "github-actions" + "groupName": "github-actions", + "schedule": "on tuesday" + }, + { + "groupName": "base-image", + "matchPackagePrefixes": ["ghcr.io/immich-app/base-server"] + }, + { + "matchDatasources": ["docker"], + "matchPackageNames": ["node"], + "versionCompatibility": "^(?[^-]+)(?-.*)?$", + "versioning": "node" } ], "ignoreDeps": [ diff --git a/server/.eslintrc.js b/server/.eslintrc.js index 17a0a2dd6c..2e46281fe6 100644 --- a/server/.eslintrc.js +++ b/server/.eslintrc.js @@ -19,6 +19,7 @@ module.exports = { '@typescript-eslint/explicit-module-boundary-types': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-floating-promises': 'error', + curly: 2, 'prettier/prettier': 0, }, }; diff --git a/server/Dockerfile b/server/Dockerfile index c780a116e0..bd269a378f 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20231109 as dev +FROM ghcr.io/immich-app/base-server-dev:20231125@sha256:f33b6eaf384e76ef3705a6e2cc76d276144ad6d3366b82f9b45b07d6a19285e2 as dev WORKDIR /usr/src/app COPY server/package.json server/package-lock.json ./ @@ -13,7 +13,7 @@ RUN npm run build RUN npm prune --omit=dev --omit=optional # web build -FROM node:20.9-alpine3.18 as web +FROM node:iron-alpine3.18 as web WORKDIR /usr/src/app COPY web/package.json web/package-lock.json ./ @@ -23,7 +23,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20231109 +FROM ghcr.io/immich-app/base-server-prod:20231125@sha256:a0e15f5bf87a97a79a399a5adffb5fe5befc18fb212e8341e744d958fe41e32a WORKDIR /usr/src/app ENV NODE_ENV=production @@ -31,7 +31,7 @@ COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/bin ./bin COPY --from=web /usr/src/app/build ./www -COPY server/assets assets +COPY server/resources resources COPY server/package.json server/package-lock.json ./ COPY server/start*.sh ./ RUN npm link && npm cache clean --force diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 23433d933b..2b2b69bde7 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1219,6 +1219,51 @@ ] } }, + "/asset/device/{deviceId}": { + "get": { + "description": "Get all asset of a device that are in the database, ID only.", + "operationId": "getAllUserAssetsByDeviceId", + "parameters": [ + { + "name": "deviceId", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Asset" + ] + } + }, "/asset/download/archive": { "post": { "operationId": "downloadArchive", @@ -2281,7 +2326,7 @@ }, "/asset/{deviceId}": { "get": { - "description": "Get all asset of a device that are in the database, ID only.", + "deprecated": true, "operationId": "getUserAssetsByDeviceId", "parameters": [ { @@ -2289,7 +2334,6 @@ "required": true, "in": "path", "schema": { - "format": "uuid", "type": "string" } } @@ -2320,6 +2364,7 @@ "api_key": [] } ], + "summary": "Use /asset/device/:deviceId instead - Remove in 1.92 release", "tags": [ "Asset" ] @@ -6953,15 +6998,6 @@ ], "type": "object" }, - "CitiesFile": { - "enum": [ - "cities15000", - "cities5000", - "cities1000", - "cities500" - ], - "type": "string" - }, "ClassificationConfig": { "properties": { "enabled": { @@ -9076,15 +9112,11 @@ }, "SystemConfigReverseGeocodingDto": { "properties": { - "citiesFileOverride": { - "$ref": "#/components/schemas/CitiesFile" - }, "enabled": { "type": "boolean" } }, "required": [ - "citiesFileOverride", "enabled" ], "type": "object" diff --git a/server/package-lock.json b/server/package-lock.json index 6ae9ae2596..c4c977e174 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -38,7 +38,6 @@ "i18n-iso-countries": "^7.6.0", "ioredis": "^5.3.2", "joi": "^17.10.0", - "local-reverse-geocoder": "0.16.5", "lodash": "^4.17.21", "luxon": "^3.4.2", "mv": "^2.1.1", @@ -71,7 +70,7 @@ "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.9", + "@types/jest": "29.5.10", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", @@ -2730,12 +2729,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@testcontainers/postgresql": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.2.2.tgz", - "integrity": "sha512-G1xJKe8omeNzngK0dj4R2cSYxWyOUdTXD/oBA03AqIwdReq/gi4WjT6CJqGbkqQy9opXZV6ug3gHMja+wM5BCA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.3.2.tgz", + "integrity": "sha512-PhbmhWe+M5RF9QZGOzg/Vc+Ve6KlT/j9OLv9G11xubvcdhbuAz9Am3u1GjsXS7C1Vt/rwWx+j0kg+FtYwbJQng==", "dev": true, "dependencies": { - "testcontainers": "^10.2.2" + "testcontainers": "^10.3.2" } }, "node_modules/@tsconfig/node10": { @@ -2909,6 +2908,26 @@ "cron": "*" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "3.3.23", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", + "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", + "dev": true, + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -3027,9 +3046,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.9", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.9.tgz", - "integrity": "sha512-zJeWhqBwVoPm83sP8h1/SVntwWTu5lZbKQGCvBjxQOyEWnKnsaomt2y7SlV4KfwlrHAHHAn00Sh4IAWaIsGOgQ==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3093,9 +3112,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz", - "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==", + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -3237,16 +3256,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", - "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/type-utils": "6.12.0", - "@typescript-eslint/utils": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3272,15 +3291,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", - "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { @@ -3300,13 +3319,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", - "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3317,13 +3336,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", - "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3344,9 +3363,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", - "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3357,13 +3376,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", - "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3384,17 +3403,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", - "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", "semver": "^7.5.4" }, "engines": { @@ -3409,12 +3428,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", - "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -4132,18 +4151,6 @@ "node": ">=0.6" } }, - "node_modules/binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "dependencies": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - }, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4329,14 +4336,6 @@ "node": ">=4" } }, - "node_modules/buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", - "engines": { - "node": ">=0.2.0" - } - }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -4348,9 +4347,9 @@ } }, "node_modules/bullmq": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.0.tgz", - "integrity": "sha512-2IjTzXfTkXQ+WNRy2/CVupnHJtqp6JpxacIvYbru2EvporUALnIcpiSpjJbk4V6kAbsYvrV2wRdUKllb+LfssQ==", + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz", + "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==", "dependencies": { "cron-parser": "^4.6.0", "glob": "^8.0.3", @@ -4500,17 +4499,6 @@ } ] }, - "node_modules/chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "dependencies": { - "traverse": ">=0.3.0 <0.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5161,19 +5149,6 @@ "node": ">= 8" } }, - "node_modules/csv-parse": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", - "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "engines": { - "node": ">= 12" - } - }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -6300,28 +6275,6 @@ "bser": "2.1.1" } }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -6575,17 +6528,6 @@ "node": ">= 6" } }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -8323,11 +8265,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/kdt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz", - "integrity": "sha512-ueX0gyv7tw4zBq9cQjaCr9qIhGTo5XYHUf/8aUUMHwoyb81KeCZHkSOoUwHGg/mgabvhTKCYjDUuYEmdak6Xjg==" - }, "node_modules/keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -8425,41 +8362,6 @@ "node": ">=6.11.5" } }, - "node_modules/local-reverse-geocoder": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.16.5.tgz", - "integrity": "sha512-MgJsyR3s8eeMfRfMvikwIdOG/jh9s78zPPX9kfx1qk5fwQLJnry5Qx5jreclqDPEpjOpNKIqz4aG5BbWGAGLbw==", - "hasInstallScript": true, - "dependencies": { - "async": "^3.2.4", - "csv-parse": "^5.5.0", - "debug": "^4.3.4", - "kdt": "^0.1.0", - "node-fetch": "^3.3.2", - "unzip-stream": "^0.3.1" - }, - "engines": { - "node": ">=11.0.0", - "npm": ">=6.4.1" - } - }, - "node_modules/local-reverse-geocoder/node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -9077,24 +8979,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "engines": { - "node": ">=10.5.0" - } - }, "node_modules/node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -11397,12 +11281,13 @@ } }, "node_modules/testcontainers": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.2.2.tgz", - "integrity": "sha512-5GZ93rtoVXMx/s3xZjydftrKLnv1Yf+ETzGkXYRCm16LB60W48SGodxuiouYvNlVy0y0ogoQhdOw3DqsPActEA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.3.2.tgz", + "integrity": "sha512-IsV6NgS5reHcVF1nJLCDJv1hM9gAWUhLwh9b3ybgzvM3X7T2dcmuLFKt1RAR8qN8k+44tW2Drj7idxW6oeGvvg==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.21", "archiver": "^5.3.2", "async-lock": "^1.4.0", "byline": "^5.0.0", @@ -11717,14 +11602,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "node_modules/traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", - "engines": { - "node": "*" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -12314,15 +12191,6 @@ "node": ">=8" } }, - "node_modules/unzip-stream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", - "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", - "dependencies": { - "binary": "^0.3.0", - "mkdirp": "^0.5.1" - } - }, "node_modules/update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -12480,14 +12348,6 @@ "defaults": "^1.0.3" } }, - "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", - "engines": { - "node": ">= 8" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -14764,12 +14624,12 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@testcontainers/postgresql": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.2.2.tgz", - "integrity": "sha512-G1xJKe8omeNzngK0dj4R2cSYxWyOUdTXD/oBA03AqIwdReq/gi4WjT6CJqGbkqQy9opXZV6ug3gHMja+wM5BCA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.3.2.tgz", + "integrity": "sha512-PhbmhWe+M5RF9QZGOzg/Vc+Ve6KlT/j9OLv9G11xubvcdhbuAz9Am3u1GjsXS7C1Vt/rwWx+j0kg+FtYwbJQng==", "dev": true, "requires": { - "testcontainers": "^10.2.2" + "testcontainers": "^10.3.2" } }, "@tsconfig/node10": { @@ -14933,6 +14793,26 @@ "cron": "*" } }, + "@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "requires": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "@types/dockerode": { + "version": "3.3.23", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.23.tgz", + "integrity": "sha512-Lz5J+NFgZS4cEVhquwjIGH4oQwlVn2h7LXD3boitujBnzOE5o7s9H8hchEjoDK2SlRsJTogdKnQeiJgPPKLIEw==", + "dev": true, + "requires": { + "@types/docker-modem": "*", + "@types/node": "*" + } + }, "@types/eslint": { "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", @@ -15051,9 +14931,9 @@ } }, "@types/jest": { - "version": "29.5.9", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.9.tgz", - "integrity": "sha512-zJeWhqBwVoPm83sP8h1/SVntwWTu5lZbKQGCvBjxQOyEWnKnsaomt2y7SlV4KfwlrHAHHAn00Sh4IAWaIsGOgQ==", + "version": "29.5.10", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", + "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", "dev": true, "requires": { "expect": "^29.0.0", @@ -15117,9 +14997,9 @@ "dev": true }, "@types/node": { - "version": "20.9.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.3.tgz", - "integrity": "sha512-nk5wXLAXGBKfrhLB0cyHGbSqopS+nz0BUgZkUQqSHSSgdee0kssp1IAqlQOu333bW+gMNs2QREx7iynm19Abxw==", + "version": "20.10.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz", + "integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==", "requires": { "undici-types": "~5.26.4" } @@ -15261,16 +15141,16 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", - "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/type-utils": "6.12.0", - "@typescript-eslint/utils": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -15280,54 +15160,54 @@ } }, "@typescript-eslint/parser": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", - "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", - "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" } }, "@typescript-eslint/type-utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", - "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" } }, "@typescript-eslint/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", - "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", - "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -15336,27 +15216,27 @@ } }, "@typescript-eslint/utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", - "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", "semver": "^7.5.4" } }, "@typescript-eslint/visitor-keys": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", - "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "requires": { - "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/types": "6.13.1", "eslint-visitor-keys": "^3.4.1" } }, @@ -15937,15 +15817,6 @@ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "dev": true }, - "binary": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", - "requires": { - "buffers": "~0.1.1", - "chainsaw": "~0.1.0" - } - }, "binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -16077,11 +15948,6 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, - "buffers": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==" - }, "buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -16090,9 +15956,9 @@ "optional": true }, "bullmq": { - "version": "4.14.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.0.tgz", - "integrity": "sha512-2IjTzXfTkXQ+WNRy2/CVupnHJtqp6JpxacIvYbru2EvporUALnIcpiSpjJbk4V6kAbsYvrV2wRdUKllb+LfssQ==", + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.14.4.tgz", + "integrity": "sha512-8tD3Zq4CP+2q+zZ1JSkKov5ra18Jhth8zdr2X1/V2MMOVpa8vfVCsQaRnKgDpiMpaF6AH9sucXUNtk4xamTEKw==", "requires": { "cron-parser": "^4.6.0", "glob": "^8.0.3", @@ -16194,14 +16060,6 @@ "integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==", "dev": true }, - "chainsaw": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", - "requires": { - "traverse": ">=0.3.0 <0.4" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -16681,16 +16539,6 @@ "which": "^2.0.1" } }, - "csv-parse": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.0.tgz", - "integrity": "sha512-RxruSK3M4XgzcD7Trm2wEN+SJ26ChIb903+IWxNOcB5q4jT2Cs+hFr6QP39J05EohshRFEvyzEBoZ/466S2sbw==" - }, - "data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" - }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -17523,15 +17371,6 @@ "bser": "2.1.1" } }, - "fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "requires": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - } - }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -17717,14 +17556,6 @@ "mime-types": "^2.1.12" } }, - "formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "requires": { - "fetch-blob": "^3.1.2" - } - }, "formidable": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz", @@ -19005,11 +18836,6 @@ "universalify": "^2.0.0" } }, - "kdt": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/kdt/-/kdt-0.1.0.tgz", - "integrity": "sha512-ueX0gyv7tw4zBq9cQjaCr9qIhGTo5XYHUf/8aUUMHwoyb81KeCZHkSOoUwHGg/mgabvhTKCYjDUuYEmdak6Xjg==" - }, "keyv": { "version": "4.5.3", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", @@ -19094,31 +18920,6 @@ "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", "dev": true }, - "local-reverse-geocoder": { - "version": "0.16.5", - "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.16.5.tgz", - "integrity": "sha512-MgJsyR3s8eeMfRfMvikwIdOG/jh9s78zPPX9kfx1qk5fwQLJnry5Qx5jreclqDPEpjOpNKIqz4aG5BbWGAGLbw==", - "requires": { - "async": "^3.2.4", - "csv-parse": "^5.5.0", - "debug": "^4.3.4", - "kdt": "^0.1.0", - "node-fetch": "^3.3.2", - "unzip-stream": "^0.3.1" - }, - "dependencies": { - "node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "requires": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - } - } - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -19599,11 +19400,6 @@ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" }, - "node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" - }, "node-emoji": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.11.0.tgz", @@ -21301,12 +21097,13 @@ } }, "testcontainers": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.2.2.tgz", - "integrity": "sha512-5GZ93rtoVXMx/s3xZjydftrKLnv1Yf+ETzGkXYRCm16LB60W48SGodxuiouYvNlVy0y0ogoQhdOw3DqsPActEA==", + "version": "10.3.2", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.3.2.tgz", + "integrity": "sha512-IsV6NgS5reHcVF1nJLCDJv1hM9gAWUhLwh9b3ybgzvM3X7T2dcmuLFKt1RAR8qN8k+44tW2Drj7idxW6oeGvvg==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", + "@types/dockerode": "^3.3.21", "archiver": "^5.3.2", "async-lock": "^1.4.0", "byline": "^5.0.0", @@ -21569,11 +21366,6 @@ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, - "traverse": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==" - }, "tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -21900,15 +21692,6 @@ "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", "dev": true }, - "unzip-stream": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.1.tgz", - "integrity": "sha512-RzaGXLNt+CW+T41h1zl6pGz3EaeVhYlK+rdAap+7DxW5kqsqePO8kRtWPaCiVqdhZc86EctSPVYNix30YOMzmw==", - "requires": { - "binary": "^0.3.0", - "mkdirp": "^0.5.1" - } - }, "update-browserslist-db": { "version": "1.0.13", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", @@ -22028,11 +21811,6 @@ "defaults": "^1.0.3" } }, - "web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" - }, "webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", diff --git a/server/package.json b/server/package.json index 556740764d..c10d3bcd7a 100644 --- a/server/package.json +++ b/server/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@babel/runtime": "^7.22.11", + "@immich/cli": "^2.0.3", "@nestjs/bullmq": "^10.0.1", "@nestjs/common": "^10.2.2", "@nestjs/config": "^3.0.0", @@ -65,10 +66,8 @@ "glob": "^10.3.3", "handlebars": "^4.7.8", "i18n-iso-countries": "^7.6.0", - "@immich/cli": "^2.0.3", "ioredis": "^5.3.2", "joi": "^17.10.0", - "local-reverse-geocoder": "0.16.5", "lodash": "^4.17.21", "luxon": "^3.4.2", "mv": "^2.1.1", @@ -97,7 +96,7 @@ "@types/express": "^4.17.17", "@types/fluent-ffmpeg": "^2.1.21", "@types/imagemin": "^8.0.1", - "@types/jest": "29.5.9", + "@types/jest": "29.5.10", "@types/jest-when": "^3.5.2", "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", diff --git a/server/assets/style-dark.json b/server/resources/style-dark.json similarity index 100% rename from server/assets/style-dark.json rename to server/resources/style-dark.json diff --git a/server/assets/style-light.json b/server/resources/style-light.json similarity index 100% rename from server/assets/style-light.json rename to server/resources/style-light.json diff --git a/server/src/domain/access/access.core.ts b/server/src/domain/access/access.core.ts index 16527de989..c47b2acd24 100644 --- a/server/src/domain/access/access.core.ts +++ b/server/src/domain/access/access.core.ts @@ -1,5 +1,6 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { AuthUserDto } from '../auth'; +import { setDifference, setUnion } from '../domain.util'; import { IAccessRepository } from '../repositories'; export enum Permission { @@ -68,40 +69,66 @@ export class AccessCore { return authUser; } + /** + * Check if user has access to all ids, for the given permission. + * Throws error if user does not have access to any of the ids. + */ async requirePermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) { - const hasAccess = await this.hasPermission(authUser, permission, ids); - if (!hasAccess) { + ids = Array.isArray(ids) ? ids : [ids]; + const allowedIds = await this.checkAccess(authUser, permission, ids); + if (new Set(ids).size !== allowedIds.size) { throw new BadRequestException(`Not found or no ${permission} access`); } } - async hasAny(authUser: AuthUserDto, permissions: Array<{ permission: Permission; id: string }>) { - for (const { permission, id } of permissions) { - const hasAccess = await this.hasPermission(authUser, permission, id); - if (hasAccess) { - return true; - } + /** + * Return ids that user has access to, for the given permission. + * Check is done for each id, and only allowed ids are returned. + * + * @returns Set + */ + async checkAccess(authUser: AuthUserDto, permission: Permission, ids: Set | string[]) { + const idSet = Array.isArray(ids) ? new Set(ids) : ids; + if (idSet.size === 0) { + return new Set(); } - return false; - } - - async hasPermission(authUser: AuthUserDto, permission: Permission, ids: string[] | string) { - ids = Array.isArray(ids) ? ids : [ids]; const isSharedLink = authUser.isPublicUser ?? false; - - for (const id of ids) { - const hasAccess = isSharedLink - ? await this.hasSharedLinkAccess(authUser, permission, id) - : await this.hasOtherAccess(authUser, permission, id); - if (!hasAccess) { - return false; - } - } - - return true; + return isSharedLink + ? await this.checkAccessSharedLink(authUser, permission, idSet) + : await this.checkAccessOther(authUser, permission, idSet); } + private async checkAccessSharedLink(authUser: AuthUserDto, permission: Permission, ids: Set) { + const sharedLinkId = authUser.sharedLinkId; + if (!sharedLinkId) { + return new Set(); + } + + switch (permission) { + case Permission.ASSET_UPLOAD: + return authUser.isAllowUpload ? ids : new Set(); + + case Permission.ALBUM_READ: + return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); + + case Permission.ALBUM_DOWNLOAD: + return !!authUser.isAllowDownload + ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) + : new Set(); + } + + const allowedIds = new Set(); + for (const id of ids) { + const hasAccess = await this.hasSharedLinkAccess(authUser, permission, id); + if (hasAccess) { + allowedIds.add(id); + } + } + return allowedIds; + } + + // TODO: Migrate logic to checkAccessSharedLink to evaluate permissions in bulk. private async hasSharedLinkAccess(authUser: AuthUserDto, permission: Permission, id: string) { const sharedLinkId = authUser.sharedLinkId; if (!sharedLinkId) { @@ -118,24 +145,95 @@ export class AccessCore { case Permission.ASSET_DOWNLOAD: return !!authUser.isAllowDownload && (await this.repository.asset.hasSharedLinkAccess(sharedLinkId, id)); - case Permission.ASSET_UPLOAD: - return authUser.isAllowUpload; - case Permission.ASSET_SHARE: // TODO: fix this to not use authUser.id for shared link access control return this.repository.asset.hasOwnerAccess(authUser.id, id); - case Permission.ALBUM_READ: - return this.repository.album.hasSharedLinkAccess(sharedLinkId, id); - - case Permission.ALBUM_DOWNLOAD: - return !!authUser.isAllowDownload && (await this.repository.album.hasSharedLinkAccess(sharedLinkId, id)); - default: return false; } } + private async checkAccessOther(authUser: AuthUserDto, permission: Permission, ids: Set) { + switch (permission) { + case Permission.ALBUM_READ: { + const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); + const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_UPDATE: + return this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_DELETE: + return this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_SHARE: + return this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ALBUM_DOWNLOAD: { + const isOwner = await this.repository.album.checkOwnerAccess(authUser.id, ids); + const isShared = await this.repository.album.checkSharedAlbumAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_REMOVE_ASSET: + return this.repository.album.checkOwnerAccess(authUser.id, ids); + + case Permission.ASSET_UPLOAD: + return this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.ARCHIVE_READ: + return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + + case Permission.AUTH_DEVICE_DELETE: + return this.repository.authDevice.checkOwnerAccess(authUser.id, ids); + + case Permission.TIMELINE_READ: { + const isOwner = ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + const isPartner = await this.repository.timeline.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.TIMELINE_DOWNLOAD: + return ids.has(authUser.id) ? new Set([authUser.id]) : new Set(); + + case Permission.LIBRARY_READ: { + const isOwner = await this.repository.library.checkOwnerAccess(authUser.id, ids); + const isPartner = await this.repository.library.checkPartnerAccess(authUser.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.LIBRARY_UPDATE: + return this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.LIBRARY_DELETE: + return this.repository.library.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_READ: + return this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_WRITE: + return this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PERSON_MERGE: + return this.repository.person.checkOwnerAccess(authUser.id, ids); + + case Permission.PARTNER_UPDATE: + return this.repository.partner.checkUpdateAccess(authUser.id, ids); + } + + const allowedIds = new Set(); + for (const id of ids) { + const hasAccess = await this.hasOtherAccess(authUser, permission, id); + if (hasAccess) { + allowedIds.add(id); + } + } + return allowedIds; + } + + // TODO: Migrate logic to checkAccessOther to evaluate permissions in bulk. private async hasOtherAccess(authUser: AuthUserDto, permission: Permission, id: string) { switch (permission) { // uses album id @@ -184,69 +282,6 @@ export class AccessCore { (await this.repository.asset.hasPartnerAccess(authUser.id, id)) ); - case Permission.ALBUM_READ: - return ( - (await this.repository.album.hasOwnerAccess(authUser.id, id)) || - (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) - ); - - case Permission.ALBUM_UPDATE: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_DELETE: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_SHARE: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_DOWNLOAD: - return ( - (await this.repository.album.hasOwnerAccess(authUser.id, id)) || - (await this.repository.album.hasSharedAlbumAccess(authUser.id, id)) - ); - - case Permission.ASSET_UPLOAD: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.ALBUM_REMOVE_ASSET: - return this.repository.album.hasOwnerAccess(authUser.id, id); - - case Permission.ARCHIVE_READ: - return authUser.id === id; - - case Permission.AUTH_DEVICE_DELETE: - return this.repository.authDevice.hasOwnerAccess(authUser.id, id); - - case Permission.TIMELINE_READ: - return authUser.id === id || (await this.repository.timeline.hasPartnerAccess(authUser.id, id)); - - case Permission.TIMELINE_DOWNLOAD: - return authUser.id === id; - - case Permission.LIBRARY_READ: - return ( - (await this.repository.library.hasOwnerAccess(authUser.id, id)) || - (await this.repository.library.hasPartnerAccess(authUser.id, id)) - ); - - case Permission.LIBRARY_UPDATE: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.LIBRARY_DELETE: - return this.repository.library.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_READ: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_WRITE: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PERSON_MERGE: - return this.repository.person.hasOwnerAccess(authUser.id, id); - - case Permission.PARTNER_UPDATE: - return this.repository.partner.hasUpdateAccess(authUser.id, id); - default: return false; } diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/domain/activity/activity.spec.ts index 496d8978b7..659718bede 100644 --- a/server/src/domain/activity/activity.spec.ts +++ b/server/src/domain/activity/activity.spec.ts @@ -24,7 +24,7 @@ describe(ActivityService.name, () => { describe('getAll', () => { it('should get all', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); activityMock.search.mockResolvedValue([]); await expect(sut.getAll(authStub.admin, { assetId: 'asset-id', albumId: 'album-id' })).resolves.toEqual([]); @@ -37,7 +37,7 @@ describe(ActivityService.name, () => { }); it('should filter by type=like', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); activityMock.search.mockResolvedValue([]); await expect( @@ -52,7 +52,7 @@ describe(ActivityService.name, () => { }); it('should filter by type=comment', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); activityMock.search.mockResolvedValue([]); await expect( @@ -70,7 +70,7 @@ describe(ActivityService.name, () => { describe('getStatistics', () => { it('should get the comment count', async () => { activityMock.getStatistics.mockResolvedValue(1); - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([activityStub.oneComment.albumId])); await expect( sut.getStatistics(authStub.admin, { assetId: 'asset-id', @@ -82,7 +82,6 @@ describe(ActivityService.name, () => { describe('addComment', () => { it('should require access to the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.create(authStub.admin, { albumId: 'album-id', @@ -114,7 +113,7 @@ describe(ActivityService.name, () => { }); it('should fail because activity is disabled for the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); accessMock.activity.hasCreateAccess.mockResolvedValue(false); activityMock.create.mockResolvedValue(activityStub.oneComment); @@ -148,7 +147,7 @@ describe(ActivityService.name, () => { }); it('should skip if like exists', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); accessMock.activity.hasCreateAccess.mockResolvedValue(true); activityMock.search.mockResolvedValue([activityStub.liked]); diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/domain/album/album.service.spec.ts index a93cb0ad17..e890305381 100644 --- a/server/src/domain/album/album.service.spec.ts +++ b/server/src/domain/album/album.service.spec.ts @@ -58,9 +58,9 @@ describe(AlbumService.name, () => { describe('getAll', () => { it('gets list of albums for auth user', async () => { albumMock.getOwned.mockResolvedValue([albumStub.empty, albumStub.sharedWithUser]); - albumMock.getAssetCountForIds.mockResolvedValue([ - { albumId: albumStub.empty.id, assetCount: 0 }, - { albumId: albumStub.sharedWithUser.id, assetCount: 0 }, + albumMock.getMetadataForIds.mockResolvedValue([ + { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, + { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); @@ -72,7 +72,14 @@ describe(AlbumService.name, () => { it('gets list of albums that have a specific asset', async () => { albumMock.getByAssetId.mockResolvedValue([albumStub.oneAsset]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { assetId: albumStub.oneAsset.id }); @@ -83,7 +90,9 @@ describe(AlbumService.name, () => { it('gets list of albums that are shared', async () => { albumMock.getShared.mockResolvedValue([albumStub.sharedWithUser]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.sharedWithUser.id, assetCount: 0 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { albumId: albumStub.sharedWithUser.id, assetCount: 0, startDate: undefined, endDate: undefined }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { shared: true }); @@ -94,7 +103,9 @@ describe(AlbumService.name, () => { it('gets list of albums that are NOT shared', async () => { albumMock.getNotShared.mockResolvedValue([albumStub.empty]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.empty.id, assetCount: 0 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { albumId: albumStub.empty.id, assetCount: 0, startDate: undefined, endDate: undefined }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, { shared: false }); @@ -106,7 +117,14 @@ describe(AlbumService.name, () => { it('counts assets correctly', async () => { albumMock.getOwned.mockResolvedValue([albumStub.oneAsset]); - albumMock.getAssetCountForIds.mockResolvedValue([{ albumId: albumStub.oneAsset.id, assetCount: 1 }]); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); albumMock.getInvalidThumbnail.mockResolvedValue([]); const result = await sut.getAll(authStub.admin, {}); @@ -118,8 +136,13 @@ describe(AlbumService.name, () => { it('updates the album thumbnail by listing all albums', async () => { albumMock.getOwned.mockResolvedValue([albumStub.oneAssetInvalidThumbnail]); - albumMock.getAssetCountForIds.mockResolvedValue([ - { albumId: albumStub.oneAssetInvalidThumbnail.id, assetCount: 1 }, + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAssetInvalidThumbnail.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, ]); albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.oneAssetInvalidThumbnail.id]); albumMock.update.mockResolvedValue(albumStub.oneAssetValidThumbnail); @@ -134,8 +157,13 @@ describe(AlbumService.name, () => { it('removes the thumbnail for an empty album', async () => { albumMock.getOwned.mockResolvedValue([albumStub.emptyWithInvalidThumbnail]); - albumMock.getAssetCountForIds.mockResolvedValue([ - { albumId: albumStub.emptyWithInvalidThumbnail.id, assetCount: 1 }, + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.emptyWithInvalidThumbnail.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, ]); albumMock.getInvalidThumbnail.mockResolvedValue([albumStub.emptyWithInvalidThumbnail.id]); albumMock.update.mockResolvedValue(albumStub.emptyWithValidThumbnail); @@ -204,7 +232,6 @@ describe(AlbumService.name, () => { }); it('should prevent updating a not owned album (shared with auth user)', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.update(authStub.admin, albumStub.sharedWithAdmin.id, { albumName: 'new album name', @@ -213,7 +240,7 @@ describe(AlbumService.name, () => { }); it('should require a valid thumbnail asset id', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4'])); albumMock.getById.mockResolvedValue(albumStub.oneAsset); albumMock.update.mockResolvedValue(albumStub.oneAsset); albumMock.hasAsset.mockResolvedValue(false); @@ -229,7 +256,7 @@ describe(AlbumService.name, () => { }); it('should allow the owner to update the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-4'])); albumMock.getById.mockResolvedValue(albumStub.oneAsset); albumMock.update.mockResolvedValue(albumStub.oneAsset); @@ -252,7 +279,7 @@ describe(AlbumService.name, () => { describe('delete', () => { it('should throw an error for an album not found', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(null); await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( @@ -263,7 +290,6 @@ describe(AlbumService.name, () => { }); it('should not let a shared user delete the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf( @@ -274,7 +300,7 @@ describe(AlbumService.name, () => { }); it('should let the owner delete an album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.empty.id])); albumMock.getById.mockResolvedValue(albumStub.empty); await sut.delete(authStub.admin, albumStub.empty.id); @@ -286,7 +312,6 @@ describe(AlbumService.name, () => { describe('addUsers', () => { it('should throw an error if the auth user is not the owner', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.addUsers(authStub.admin, albumStub.sharedWithAdmin.id, { sharedUserIds: ['user-1'] }), ).rejects.toBeInstanceOf(BadRequestException); @@ -294,7 +319,7 @@ describe(AlbumService.name, () => { }); it('should throw an error if the userId is already added', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); await expect( sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { sharedUserIds: [authStub.admin.id] }), @@ -303,7 +328,7 @@ describe(AlbumService.name, () => { }); it('should throw an error if the userId does not exist', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(null); await expect( @@ -313,7 +338,7 @@ describe(AlbumService.name, () => { }); it('should add valid shared users', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithAdmin)); albumMock.update.mockResolvedValue(albumStub.sharedWithAdmin); userMock.get.mockResolvedValue(userStub.user2); @@ -328,14 +353,14 @@ describe(AlbumService.name, () => { describe('removeUser', () => { it('should require a valid album id', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); albumMock.getById.mockResolvedValue(null); await expect(sut.removeUser(authStub.admin, 'album-1', 'user-1')).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); }); it('should remove a shared user from an owned album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithUser.id])); albumMock.getById.mockResolvedValue(albumStub.sharedWithUser); await expect( @@ -352,7 +377,6 @@ describe(AlbumService.name, () => { }); it('should prevent removing a shared user from a not-owned album (shared with auth user)', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.sharedWithMultiple); await expect( @@ -360,7 +384,10 @@ describe(AlbumService.name, () => { ).rejects.toBeInstanceOf(BadRequestException); expect(albumMock.update).not.toHaveBeenCalled(); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, albumStub.sharedWithMultiple.id); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( + authStub.user1.id, + new Set([albumStub.sharedWithMultiple.id]), + ); }); it('should allow a shared user to remove themselves', async () => { @@ -413,51 +440,75 @@ describe(AlbumService.name, () => { describe('getAlbumInfo', () => { it('should get a shared album', async () => { albumMock.getById.mockResolvedValue(albumStub.oneAsset); - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); await sut.get(authStub.admin, albumStub.oneAsset.id, {}); expect(albumMock.getById).toHaveBeenCalledWith(albumStub.oneAsset.id, { withAssets: true }); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.id, + new Set([albumStub.oneAsset.id]), + ); }); it('should get a shared album via a shared link', async () => { albumMock.getById.mockResolvedValue(albumStub.oneAsset); - accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); + accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); await sut.get(authStub.adminSharedLink, 'album-123', {}); expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); - expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( + expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, - 'album-123', + new Set(['album-123']), ); }); it('should get a shared album via shared with user', async () => { albumMock.getById.mockResolvedValue(albumStub.oneAsset); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); + accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); + albumMock.getMetadataForIds.mockResolvedValue([ + { + albumId: albumStub.oneAsset.id, + assetCount: 1, + startDate: new Date('1970-01-01'), + endDate: new Date('1970-01-01'), + }, + ]); await sut.get(authStub.user1, 'album-123', {}); expect(albumMock.getById).toHaveBeenCalledWith('album-123', { withAssets: true }); - expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, 'album-123'); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['album-123'])); }); it('should throw an error for no access', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); - await expect(sut.get(authStub.admin, 'album-123', {})).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123'); - expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-123'); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123'])); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-123'])); }); }); describe('addAssets', () => { it('should allow the owner to add assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -482,7 +533,7 @@ describe(AlbumService.name, () => { }); it('should not set the thumbnail if the album has one already', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep({ ...albumStub.empty, albumThumbnailAssetId: 'asset-id' })); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -500,8 +551,7 @@ describe(AlbumService.name, () => { }); it('should allow a shared user to add assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); + accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.sharedWithUser)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -526,9 +576,7 @@ describe(AlbumService.name, () => { }); it('should allow a shared link user to add assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); - accessMock.album.hasSharedLinkAccess.mockResolvedValue(true); + accessMock.album.checkSharedLinkAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -551,14 +599,14 @@ describe(AlbumService.name, () => { assetIds: ['asset-1', 'asset-2', 'asset-3'], }); - expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalledWith( + expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalledWith( authStub.adminSharedLink.sharedLinkId, - 'album-123', + new Set(['album-123']), ); }); it('should allow adding assets shared via partner sharing', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(false); accessMock.asset.hasPartnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); @@ -577,7 +625,7 @@ describe(AlbumService.name, () => { }); it('should skip duplicate assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); @@ -590,7 +638,7 @@ describe(AlbumService.name, () => { }); it('should skip assets not shared with user', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); accessMock.asset.hasOwnerAccess.mockResolvedValue(false); accessMock.asset.hasPartnerAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.oneAsset); @@ -605,33 +653,31 @@ describe(AlbumService.name, () => { }); it('should not allow unauthorized access to the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.oneAsset); await expect( sut.addAssets(authStub.admin, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), ).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalled(); - expect(accessMock.album.hasSharedAlbumAccess).toHaveBeenCalled(); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalled(); + expect(accessMock.album.checkSharedAlbumAccess).toHaveBeenCalled(); }); it('should not allow unauthorized shared link access to the album', async () => { - accessMock.album.hasSharedLinkAccess.mockResolvedValue(false); albumMock.getById.mockResolvedValue(albumStub.oneAsset); await expect( sut.addAssets(authStub.adminSharedLink, 'album-123', { ids: ['asset-1', 'asset-2', 'asset-3'] }), ).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.album.hasSharedLinkAccess).toHaveBeenCalled(); + expect(accessMock.album.checkSharedLinkAccess).toHaveBeenCalled(); }); }); describe('removeAssets', () => { it('should allow the owner to remove assets', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123'])); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); @@ -644,7 +690,7 @@ describe(AlbumService.name, () => { }); it('should skip assets not in the album', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.empty)); albumMock.getAssetIds.mockResolvedValueOnce(new Set()); @@ -656,7 +702,7 @@ describe(AlbumService.name, () => { }); it('should skip assets without user permission to remove', async () => { - accessMock.album.hasSharedAlbumAccess.mockResolvedValue(true); + accessMock.album.checkSharedAlbumAccess.mockResolvedValue(new Set(['album-123'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.oneAsset)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); @@ -672,7 +718,8 @@ describe(AlbumService.name, () => { }); it('should reset the thumbnail if it is removed', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['album-123'])); + accessMock.album.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-id'])); albumMock.getById.mockResolvedValue(_.cloneDeep(albumStub.twoAssets)); albumMock.getAssetIds.mockResolvedValueOnce(new Set(['asset-id'])); diff --git a/server/src/domain/album/album.service.ts b/server/src/domain/album/album.service.ts index 0fb0391ef4..308735d43c 100644 --- a/server/src/domain/album/album.service.ts +++ b/server/src/domain/album/album.service.ts @@ -3,8 +3,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AccessCore, Permission } from '../access'; import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from '../asset'; import { AuthUserDto } from '../auth'; +import { setUnion } from '../domain.util'; import { JobName } from '../job'; import { + AlbumAssetCount, AlbumInfoOptions, IAccessRepository, IAlbumRepository, @@ -68,11 +70,19 @@ export class AlbumService { // Get asset count for each album. Then map the result to an object: // { [albumId]: assetCount } - const albumsAssetCount = await this.albumRepository.getAssetCountForIds(albums.map((album) => album.id)); - const albumsAssetCountObj = albumsAssetCount.reduce((obj: Record, { albumId, assetCount }) => { - obj[albumId] = assetCount; - return obj; - }, {}); + const albumMetadataForIds = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id)); + const albumMetadataForIdsObj: Record = albumMetadataForIds.reduce( + (obj: Record, { albumId, assetCount, startDate, endDate }) => { + obj[albumId] = { + albumId, + assetCount, + startDate, + endDate, + }; + return obj; + }, + {}, + ); return Promise.all( albums.map(async (album) => { @@ -80,7 +90,9 @@ export class AlbumService { return { ...mapAlbumWithoutAssets(album), sharedLinks: undefined, - assetCount: albumsAssetCountObj[album.id], + startDate: albumMetadataForIdsObj[album.id].startDate, + endDate: albumMetadataForIdsObj[album.id].endDate, + assetCount: albumMetadataForIdsObj[album.id].assetCount, lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt, }; }), @@ -90,7 +102,16 @@ export class AlbumService { async get(authUser: AuthUserDto, id: string, dto: AlbumInfoDto): Promise { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); await this.albumRepository.updateThumbnails(); - return mapAlbum(await this.findOrFail(id, { withAssets: true }), !dto.withoutAssets); + const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; + const album = await this.findOrFail(id, { withAssets }); + const [albumMetadataForIds] = await this.albumRepository.getMetadataForIds([album.id]); + + return { + ...mapAlbum(album, withAssets), + startDate: albumMetadataForIds.startDate, + endDate: albumMetadataForIds.endDate, + assetCount: albumMetadataForIds.assetCount, + }; } async create(authUser: AuthUserDto, dto: CreateAlbumDto): Promise { @@ -153,6 +174,8 @@ export class AlbumService { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); + const notPresentAssetIds = dto.ids.filter((id) => !existingAssetIds.has(id)); + const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds); const results: BulkIdResponseDto[] = []; for (const assetId of dto.ids) { @@ -162,7 +185,7 @@ export class AlbumService { continue; } - const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId); + const hasAccess = allowedAssetIds.has(assetId); if (!hasAccess) { results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; @@ -190,6 +213,9 @@ export class AlbumService { await this.access.requirePermission(authUser, Permission.ALBUM_READ, id); const existingAssetIds = await this.albumRepository.getAssetIds(id, dto.ids); + const canRemove = await this.access.checkAccess(authUser, Permission.ALBUM_REMOVE_ASSET, existingAssetIds); + const canShare = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, existingAssetIds); + const allowedAssetIds = setUnion(canRemove, canShare); const results: BulkIdResponseDto[] = []; for (const assetId of dto.ids) { @@ -199,10 +225,7 @@ export class AlbumService { continue; } - const hasAccess = await this.access.hasAny(authUser, [ - { permission: Permission.ALBUM_REMOVE_ASSET, id: assetId }, - { permission: Permission.ASSET_SHARE, id: assetId }, - ]); + const hasAccess = allowedAssetIds.has(assetId); if (!hasAccess) { results.push({ id: assetId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/domain/asset/asset.service.spec.ts index 45687282f8..28a138254c 100644 --- a/server/src/domain/asset/asset.service.spec.ts +++ b/server/src/domain/asset/asset.service.spec.ts @@ -347,14 +347,14 @@ describe(AssetService.name, () => { describe('getTimeBucket', () => { it('should return the assets for a album time bucket if user has album.read', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-id'])); assetMock.getTimeBucket.mockResolvedValue([assetStub.image]); await expect( sut.getTimeBucket(authStub.admin, { size: TimeBucketSize.DAY, timeBucket: 'bucket', albumId: 'album-id' }), ).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: 'asset-id' })])); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-id'); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-id'])); expect(assetMock.getTimeBucket).toBeCalledWith('bucket', { size: TimeBucketSize.DAY, timeBucket: 'bucket', @@ -546,7 +546,7 @@ describe(AssetService.name, () => { }); it('should return a list of archives (albumId)', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set(['album-1'])); assetMock.getByAlbumId.mockResolvedValue({ items: [assetStub.image, assetStub.video], hasNextPage: false, @@ -554,12 +554,12 @@ describe(AssetService.name, () => { await expect(sut.getDownloadInfo(authStub.admin, { albumId: 'album-1' })).resolves.toEqual(downloadResponse); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'album-1'); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['album-1'])); expect(assetMock.getByAlbumId).toHaveBeenCalledWith({ take: 2500, skip: 0 }, 'album-1'); }); it('should return a list of archives (userId)', async () => { - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); assetMock.getByUserId.mockResolvedValue({ items: [assetStub.image, assetStub.video], hasNextPage: false, @@ -575,7 +575,7 @@ describe(AssetService.name, () => { }); it('should split archives by size', async () => { - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); assetMock.getByUserId.mockResolvedValue({ items: [ @@ -1067,4 +1067,18 @@ describe(AssetService.name, () => { ); }); }); + + it('get assets by device id', async () => { + const assets = [assetStub.image, assetStub.image1]; + + assetMock.getAllByDeviceId.mockImplementation(() => + Promise.resolve(Array.from(assets.map((asset) => asset.deviceAssetId))), + ); + + const deviceId = 'device-id'; + const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); + + expect(result.length).toEqual(2); + expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); + }); }); diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index b68911bfbd..c547d6a6db 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -386,6 +386,10 @@ export class AssetService { return assets.map((a) => mapAsset(a)); } + async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) { + return this.assetRepository.getAllByDeviceId(authUser.id, deviceId); + } + async update(authUser: AuthUserDto, id: string, dto: UpdateAssetDto): Promise { await this.access.requirePermission(authUser, Permission.ASSET_UPDATE, id); diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/domain/auth/auth.service.spec.ts index a815e22d1f..7ece7bed85 100644 --- a/server/src/domain/auth/auth.service.spec.ts +++ b/server/src/domain/auth/auth.service.spec.ts @@ -395,11 +395,11 @@ describe('AuthService', () => { describe('logoutDevice', () => { it('should logout the device', async () => { - accessMock.authDevice.hasOwnerAccess.mockResolvedValue(true); + accessMock.authDevice.checkOwnerAccess.mockResolvedValue(new Set(['token-1'])); await sut.logoutDevice(authStub.user1, 'token-1'); - expect(accessMock.authDevice.hasOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, 'token-1'); + expect(accessMock.authDevice.checkOwnerAccess).toHaveBeenCalledWith(authStub.user1.id, new Set(['token-1'])); expect(userTokenMock.delete).toHaveBeenCalledWith('token-1'); }); }); diff --git a/server/src/domain/domain.util.ts b/server/src/domain/domain.util.ts index 53e674bf30..00ad27bc77 100644 --- a/server/src/domain/domain.util.ts +++ b/server/src/domain/domain.util.ts @@ -150,3 +150,23 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = { return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions); } + +// NOTE: The following Set utils have been added here, to easily determine where they are used. +// They should be replaced with native Set operations, when they are added to the language. +// Proposal reference: https://github.com/tc39/proposal-set-methods + +export const setUnion = (setA: Set, setB: Set): Set => { + const union = new Set(setA); + for (const elem of setB) { + union.add(elem); + } + return union; +}; + +export const setDifference = (setA: Set, setB: Set): Set => { + const difference = new Set(setA); + for (const elem of setB) { + difference.delete(elem); + } + return difference; +}; diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/domain/library/library.service.spec.ts index 3d7d68736f..c7e15e9600 100644 --- a/server/src/domain/library/library.service.spec.ts +++ b/server/src/domain/library/library.service.spec.ts @@ -58,7 +58,7 @@ describe(LibraryService.name, () => { ctime: new Date('2023-01-01'), } as Stats); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([authStub.admin.id])); sut = new LibraryService( accessMock, diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/domain/metadata/metadata.service.spec.ts index a854c0152e..0ef5dfd736 100644 --- a/server/src/domain/metadata/metadata.service.spec.ts +++ b/server/src/domain/metadata/metadata.service.spec.ts @@ -1,4 +1,4 @@ -import { AssetType, CitiesFile, ExifEntity, SystemConfigKey } from '@app/infra/entities'; +import { AssetType, ExifEntity, SystemConfigKey } from '@app/infra/entities'; import { assetStub, newAlbumRepositoryMock, @@ -15,7 +15,7 @@ import { randomBytes } from 'crypto'; import { Stats } from 'fs'; import { constants } from 'fs/promises'; import { when } from 'jest-when'; -import { JobName, QueueName } from '../job'; +import { JobName } from '../job'; import { IAlbumRepository, IAssetRepository, @@ -78,10 +78,7 @@ describe(MetadataService.name, () => { describe('init', () => { beforeEach(async () => { - configMock.load.mockResolvedValue([ - { key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }, - { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_500 }, - ]); + configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: true }]); await sut.init(); }); @@ -90,42 +87,10 @@ describe(MetadataService.name, () => { configMock.load.mockResolvedValue([{ key: SystemConfigKey.REVERSE_GEOCODING_ENABLED, value: false }]); await sut.init(); - expect(metadataMock.deleteCache).not.toHaveBeenCalled(); expect(jobMock.pause).toHaveBeenCalledTimes(1); expect(metadataMock.init).toHaveBeenCalledTimes(1); expect(jobMock.resume).toHaveBeenCalledTimes(1); }); - - it('should return if deleteCache is false and the cities precision has not changed', async () => { - await sut.init(); - - expect(metadataMock.deleteCache).not.toHaveBeenCalled(); - expect(jobMock.pause).toHaveBeenCalledTimes(1); - expect(metadataMock.init).toHaveBeenCalledTimes(1); - expect(jobMock.resume).toHaveBeenCalledTimes(1); - }); - - it('should re-init if deleteCache is false but the cities precision has changed', async () => { - configMock.load.mockResolvedValue([ - { key: SystemConfigKey.REVERSE_GEOCODING_CITIES_FILE_OVERRIDE, value: CitiesFile.CITIES_1000 }, - ]); - - await sut.init(); - - expect(metadataMock.deleteCache).not.toHaveBeenCalled(); - expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_1000 }); - expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - }); - - it('should re-init and delete cache if deleteCache is true', async () => { - await sut.init(true); - - expect(metadataMock.deleteCache).toHaveBeenCalled(); - expect(jobMock.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - expect(metadataMock.init).toHaveBeenCalledWith({ citiesFileOverride: CitiesFile.CITIES_500 }); - expect(jobMock.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); - }); }); describe('handleLivePhotoLinking', () => { diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 353be4e19b..9e41455a04 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -98,31 +98,24 @@ export class MetadataService { this.storageCore = StorageCore.create(assetRepository, moveRepository, personRepository, storageRepository); } - async init(deleteCache = false) { + async init() { if (!this.subscription) { this.subscription = this.configCore.config$.subscribe(() => this.init()); } const { reverseGeocoding } = await this.configCore.getConfig(); - const { citiesFileOverride } = reverseGeocoding; + const { enabled } = reverseGeocoding; - if (!reverseGeocoding.enabled) { + if (!enabled) { return; } try { - if (deleteCache) { - await this.repository.deleteCache(); - } else if (this.oldCities && this.oldCities === citiesFileOverride) { - return; - } - await this.jobRepository.pause(QueueName.METADATA_EXTRACTION); - await this.repository.init({ citiesFileOverride }); + await this.repository.init(); await this.jobRepository.resume(QueueName.METADATA_EXTRACTION); - this.logger.log(`Initialized local reverse geocoder with ${citiesFileOverride}`); - this.oldCities = citiesFileOverride; + this.logger.log(`Initialized local reverse geocoder`); } catch (error: Error | any) { this.logger.error(`Unable to initialize reverse geocoding: ${error}`, error?.stack); } @@ -292,8 +285,11 @@ export class MetadataService { } try { - const { city, state, country } = await this.repository.reverseGeocode({ latitude, longitude }); - Object.assign(exifData, { city, state, country }); + const reverseGeocode = await this.repository.reverseGeocode({ latitude, longitude }); + if (!reverseGeocode) { + return; + } + Object.assign(exifData, reverseGeocode); } catch (error: Error | any) { this.logger.warn( `Unable to run reverse geocoding due to ${error} for asset ${asset.id} at ${asset.originalPath}`, diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 3a4ac6b6db..b210a9165e 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -183,105 +183,101 @@ describe(PersonService.name, () => { describe('getById', () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw a bad request when person is not found', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should get a person by id', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getById(authStub.admin, 'person-1')).resolves.toEqual(responseDto); expect(personMock.getById).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('getThumbnail', () => { it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when person has no thumbnail', async () => { personMock.getById.mockResolvedValue(personStub.noThumbnail); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getThumbnail(authStub.admin, 'person-1')).rejects.toBeInstanceOf(NotFoundException); expect(storageMock.createReadStream).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should serve the thumbnail', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getThumbnail(authStub.admin, 'person-1'); expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('getAssets', () => { it('should require person.read permission', async () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getAssets(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); expect(personMock.getAssets).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should return a person's assets", async () => { personMock.getAssets.mockResolvedValue([assetStub.image, assetStub.video]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await sut.getAssets(authStub.admin, 'person-1'); expect(personMock.getAssets).toHaveBeenCalledWith('person-1'); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('update', () => { it('should require person.write permission', async () => { personMock.getById.mockResolvedValue(personStub.noName); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's name", async () => { personMock.getById.mockResolvedValue(personStub.noName); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto); @@ -291,14 +287,14 @@ describe(PersonService.name, () => { name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetStub.image.id] }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's date of birth", async () => { personMock.getById.mockResolvedValue(personStub.noBirthDate); personMock.update.mockResolvedValue(personStub.withBirthDate); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { birthDate: new Date('1976-06-30') })).resolves.toEqual({ id: 'person-1', @@ -311,14 +307,14 @@ describe(PersonService.name, () => { expect(personMock.getById).toHaveBeenCalledWith('person-1'); expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: new Date('1976-06-30') }); expect(jobMock.queue).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should update a person visibility', async () => { personMock.getById.mockResolvedValue(personStub.hidden); personMock.update.mockResolvedValue(personStub.withName); personMock.getAssets.mockResolvedValue([assetStub.image]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto); @@ -328,7 +324,7 @@ describe(PersonService.name, () => { name: JobName.SEARCH_INDEX_ASSET, data: { ids: [assetStub.image.id] }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it("should update a person's thumbnailPath", async () => { @@ -336,7 +332,7 @@ describe(PersonService.name, () => { personMock.update.mockResolvedValue(personStub.withName); personMock.getFacesByIds.mockResolvedValue([faceStub.face1]); accessMock.asset.hasOwnerAccess.mockResolvedValue(true); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }), @@ -351,31 +347,31 @@ describe(PersonService.name, () => { }, ]); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'person-1' } }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when the face feature assetId is invalid', async () => { personMock.getById.mockResolvedValue(personStub.withName); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.update(authStub.admin, 'person-1', { featureFaceAssetId: '-1' })).rejects.toThrow( BadRequestException, ); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); describe('updateAll', () => { it('should throw an error when personId is invalid', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect( sut.updatePeople(authStub.admin, { people: [{ id: 'person-1', name: 'Person 1' }] }), ).resolves.toEqual([{ error: BulkIdErrorReason.UNKNOWN, id: 'person-1', success: false }]); expect(personMock.update).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); @@ -652,7 +648,6 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValueOnce(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, @@ -663,7 +658,7 @@ describe(PersonService.name, () => { expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should merge two people', async () => { @@ -671,7 +666,8 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValueOnce(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([]); personMock.delete.mockResolvedValue(personStub.mergePerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: true }, @@ -691,14 +687,15 @@ describe(PersonService.name, () => { name: JobName.PERSON_DELETE, data: { id: personStub.mergePerson.id }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should delete conflicting faces before merging', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getById.mockResolvedValue(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: true }, @@ -713,25 +710,26 @@ describe(PersonService.name, () => { name: JobName.SEARCH_REMOVE_FACE, data: { assetId: assetStub.image.id, personId: personStub.mergePerson.id }, }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should throw an error when the primary person is not found', async () => { personMock.getById.mockResolvedValue(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).rejects.toBeInstanceOf( BadRequestException, ); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should handle invalid merge ids', async () => { personMock.getById.mockResolvedValueOnce(personStub.primaryPerson); personMock.getById.mockResolvedValueOnce(null); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, @@ -740,7 +738,7 @@ describe(PersonService.name, () => { expect(personMock.prepareReassignFaces).not.toHaveBeenCalled(); expect(personMock.reassignFaces).not.toHaveBeenCalled(); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should handle an error reassigning faces', async () => { @@ -748,14 +746,15 @@ describe(PersonService.name, () => { personMock.getById.mockResolvedValue(personStub.mergePerson); personMock.prepareReassignFaces.mockResolvedValue([assetStub.image.id]); personMock.reassignFaces.mockRejectedValue(new Error('update failed')); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1'])); + accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-2'])); await expect(sut.mergePerson(authStub.admin, 'person-1', { ids: ['person-2'] })).resolves.toEqual([ { id: 'person-2', success: false, error: BulkIdErrorReason.UNKNOWN }, ]); expect(personMock.delete).not.toHaveBeenCalled(); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); @@ -763,16 +762,15 @@ describe(PersonService.name, () => { it('should get correct number of person', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); personMock.getStatistics.mockResolvedValue(statistics); - accessMock.person.hasOwnerAccess.mockResolvedValue(true); + accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1'])); await expect(sut.getStatistics(authStub.admin, 'person-1')).resolves.toEqual({ assets: 3 }); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); it('should require person.read permission', async () => { personMock.getById.mockResolvedValue(personStub.primaryPerson); - accessMock.person.hasOwnerAccess.mockResolvedValue(false); await expect(sut.getStatistics(authStub.admin, 'person-1')).rejects.toBeInstanceOf(BadRequestException); - expect(accessMock.person.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, 'person-1'); + expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, new Set(['person-1'])); }); }); }); diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 49329a452a..3452807f66 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -375,10 +375,11 @@ export class PersonService { const results: BulkIdResponseDto[] = []; - for (const mergeId of mergeIds) { - const hasPermission = await this.access.hasPermission(authUser, Permission.PERSON_MERGE, mergeId); + const allowedIds = await this.access.checkAccess(authUser, Permission.PERSON_MERGE, mergeIds); - if (!hasPermission) { + for (const mergeId of mergeIds) { + const hasAccess = allowedIds.has(mergeId); + if (!hasAccess) { results.push({ id: mergeId, success: false, error: BulkIdErrorReason.NO_PERMISSION }); continue; } diff --git a/server/src/domain/repositories/access.repository.ts b/server/src/domain/repositories/access.repository.ts index 9c009719d3..7736fd890f 100644 --- a/server/src/domain/repositories/access.repository.ts +++ b/server/src/domain/repositories/access.repository.ts @@ -14,29 +14,29 @@ export interface IAccessRepository { }; authDevice: { - hasOwnerAccess(userId: string, deviceId: string): Promise; + checkOwnerAccess(userId: string, deviceIds: Set): Promise>; }; album: { - hasOwnerAccess(userId: string, albumId: string): Promise; - hasSharedAlbumAccess(userId: string, albumId: string): Promise; - hasSharedLinkAccess(sharedLinkId: string, albumId: string): Promise; + checkOwnerAccess(userId: string, albumIds: Set): Promise>; + checkSharedAlbumAccess(userId: string, albumIds: Set): Promise>; + checkSharedLinkAccess(sharedLinkId: string, albumIds: Set): Promise>; }; library: { - hasOwnerAccess(userId: string, libraryId: string): Promise; - hasPartnerAccess(userId: string, partnerId: string): Promise; + checkOwnerAccess(userId: string, libraryIds: Set): Promise>; + checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; timeline: { - hasPartnerAccess(userId: string, partnerId: string): Promise; + checkPartnerAccess(userId: string, partnerIds: Set): Promise>; }; person: { - hasOwnerAccess(userId: string, personId: string): Promise; + checkOwnerAccess(userId: string, personIds: Set): Promise>; }; partner: { - hasUpdateAccess(userId: string, partnerId: string): Promise; + checkUpdateAccess(userId: string, partnerIds: Set): Promise>; }; } diff --git a/server/src/domain/repositories/album.repository.ts b/server/src/domain/repositories/album.repository.ts index d3ca62da12..10b789b4b3 100644 --- a/server/src/domain/repositories/album.repository.ts +++ b/server/src/domain/repositories/album.repository.ts @@ -5,6 +5,8 @@ export const IAlbumRepository = 'IAlbumRepository'; export interface AlbumAssetCount { albumId: string; assetCount: number; + startDate: Date | undefined; + endDate: Date | undefined; } export interface AlbumInfoOptions { @@ -30,7 +32,7 @@ export interface IAlbumRepository { hasAsset(asset: AlbumAsset): Promise; removeAsset(assetId: string): Promise; removeAssets(assets: AlbumAssets): Promise; - getAssetCountForIds(ids: string[]): Promise; + getMetadataForIds(ids: string[]): Promise; getInvalidThumbnail(): Promise; getOwned(ownerId: string): Promise; getShared(ownerId: string): Promise; diff --git a/server/src/domain/repositories/asset.repository.ts b/server/src/domain/repositories/asset.repository.ts index 12ae49000e..a42952958c 100644 --- a/server/src/domain/repositories/asset.repository.ts +++ b/server/src/domain/repositories/asset.repository.ts @@ -162,6 +162,7 @@ export interface IAssetRepository { getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise; deleteAll(ownerId: string): Promise; getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated; + getAllByDeviceId(userId: string, deviceId: string): Promise; updateAll(ids: string[], options: Partial): Promise; save(asset: Pick & Partial): Promise; remove(asset: AssetEntity): Promise; diff --git a/server/src/domain/repositories/index.ts b/server/src/domain/repositories/index.ts index ff098d8dbb..f812e6ee59 100644 --- a/server/src/domain/repositories/index.ts +++ b/server/src/domain/repositories/index.ts @@ -20,6 +20,7 @@ export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './storage.repository'; export * from './system-config.repository'; +export * from './system-metadata.repository'; export * from './tag.repository'; export * from './user-token.repository'; export * from './user.repository'; diff --git a/server/src/domain/repositories/metadata.repository.ts b/server/src/domain/repositories/metadata.repository.ts index 03ffe3a354..e8d4d1e4e4 100644 --- a/server/src/domain/repositories/metadata.repository.ts +++ b/server/src/domain/repositories/metadata.repository.ts @@ -1,5 +1,4 @@ import { Tags } from 'exiftool-vendored'; -import { InitOptions } from 'local-reverse-geocoder'; export const IMetadataRepository = 'IMetadataRepository'; @@ -31,10 +30,9 @@ export interface ImmichTags extends Omit { } export interface IMetadataRepository { - init(options: Partial): Promise; + init(): Promise; teardown(): Promise; - reverseGeocode(point: GeoPoint): Promise; - deleteCache(): Promise; + reverseGeocode(point: GeoPoint): Promise; readTags(path: string): Promise; writeTags(path: string, tags: Partial): Promise; } diff --git a/server/src/domain/repositories/system-metadata.repository.ts b/server/src/domain/repositories/system-metadata.repository.ts new file mode 100644 index 0000000000..4d571953bc --- /dev/null +++ b/server/src/domain/repositories/system-metadata.repository.ts @@ -0,0 +1,8 @@ +import { SystemMetadata } from '@app/infra/entities'; + +export const ISystemMetadataRepository = 'ISystemMetadataRepository'; + +export interface ISystemMetadataRepository { + get(key: T): Promise; + set(key: T, value: SystemMetadata[T]): Promise; +} diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/domain/shared-link/shared-link.service.spec.ts index 863e3a3534..abf8128c48 100644 --- a/server/src/domain/shared-link/shared-link.service.spec.ts +++ b/server/src/domain/shared-link/shared-link.service.spec.ts @@ -97,7 +97,6 @@ describe(SharedLinkService.name, () => { }); it('should not allow non-owners to create album shared links', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(false); await expect( sut.create(authStub.admin, { type: SharedLinkType.ALBUM, assetIds: [], albumId: 'album-1' }), ).rejects.toBeInstanceOf(BadRequestException); @@ -117,12 +116,15 @@ describe(SharedLinkService.name, () => { }); it('should create an album shared link', async () => { - accessMock.album.hasOwnerAccess.mockResolvedValue(true); + accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.oneAsset.id])); shareMock.create.mockResolvedValue(sharedLinkStub.valid); await sut.create(authStub.admin, { type: SharedLinkType.ALBUM, albumId: albumStub.oneAsset.id }); - expect(accessMock.album.hasOwnerAccess).toHaveBeenCalledWith(authStub.admin.id, albumStub.oneAsset.id); + expect(accessMock.album.checkOwnerAccess).toHaveBeenCalledWith( + authStub.admin.id, + new Set([albumStub.oneAsset.id]), + ); expect(shareMock.create).toHaveBeenCalledWith({ type: SharedLinkType.ALBUM, userId: authStub.admin.id, diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/domain/shared-link/shared-link.service.ts index d3fd89661b..bd9aaea6a3 100644 --- a/server/src/domain/shared-link/shared-link.service.ts +++ b/server/src/domain/shared-link/shared-link.service.ts @@ -119,15 +119,19 @@ export class SharedLinkService { throw new BadRequestException('Invalid shared link type'); } + const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); + const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); + const allowedAssetIds = await this.access.checkAccess(authUser, Permission.ASSET_SHARE, notPresentAssetIds); + const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { - const hasAsset = sharedLink.assets.find((asset) => asset.id === assetId); + const hasAsset = existingAssetIds.has(assetId); if (hasAsset) { results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); continue; } - const hasAccess = await this.access.hasPermission(authUser, Permission.ASSET_SHARE, assetId); + const hasAccess = allowedAssetIds.has(assetId); if (!hasAccess) { results.push({ assetId, success: false, error: AssetIdErrorReason.NO_PERMISSION }); continue; diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts index be20a02c79..aa224ccc6c 100644 --- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts +++ b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts @@ -1,12 +1,6 @@ -import { CitiesFile } from '@app/infra/entities'; -import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsEnum } from 'class-validator'; +import { IsBoolean } from 'class-validator'; export class SystemConfigReverseGeocodingDto { @IsBoolean() enabled!: boolean; - - @IsEnum(CitiesFile) - @ApiProperty({ enum: CitiesFile, enumName: 'CitiesFile' }) - citiesFileOverride!: CitiesFile; } diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index b3a030487a..bfab4bb4fc 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -1,6 +1,5 @@ import { AudioCodec, - CitiesFile, Colorspace, CQMode, SystemConfig, @@ -85,7 +84,6 @@ export const defaults = Object.freeze({ }, reverseGeocoding: { enabled: true, - citiesFileOverride: CitiesFile.CITIES_500, }, oauth: { enabled: false, diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index cdeb552b09..6ff4ac5c45 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -1,6 +1,5 @@ import { AudioCodec, - CitiesFile, Colorspace, CQMode, SystemConfig, @@ -85,7 +84,6 @@ const updatedConfig = Object.freeze({ }, reverseGeocoding: { enabled: true, - citiesFileOverride: CitiesFile.CITIES_500, }, oauth: { autoLaunch: true, diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/domain/system-config/system-config.service.ts index 5e9743ba5a..c81c462e89 100644 --- a/server/src/domain/system-config/system-config.service.ts +++ b/server/src/domain/system-config/system-config.service.ts @@ -79,7 +79,7 @@ export class SystemConfigService { return this.repository.fetchStyle(styleUrl); } - return JSON.parse(await this.repository.readFile(`./assets/style-${theme}.json`)); + return JSON.parse(await this.repository.readFile(`./resources/style-${theme}.json`)); } async getCustomCss(): Promise { diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/immich/api-v1/asset/asset.controller.ts index e7a04564c0..ad17ccf319 100644 --- a/server/src/immich/api-v1/asset/asset.controller.ts +++ b/server/src/immich/api-v1/asset/asset.controller.ts @@ -14,7 +14,7 @@ import { UseInterceptors, ValidationPipe, } from '@nestjs/common'; -import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiConsumes, ApiHeader, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger'; import { Response as Res } from 'express'; import { AuthUser, Authenticated, SharedLinkRoute } from '../../app.guard'; import { UUIDParamDto } from '../../controllers/dto/uuid-param.dto'; @@ -147,9 +147,10 @@ export class AssetController { } /** - * Get all asset of a device that are in the database, ID only. + * @deprecated Use /asset/device/:deviceId instead - Remove at 1.92 release */ @Get('/:deviceId') + @ApiOperation({ deprecated: true, summary: 'Use /asset/device/:deviceId instead - Remove in 1.92 release' }) getUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { return this.assetService.getUserAssetsByDeviceId(authUser, deviceId); } diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/immich/api-v1/asset/asset.service.spec.ts index cc2102766c..11173b55fa 100644 --- a/server/src/immich/api-v1/asset/asset.service.spec.ts +++ b/server/src/immich/api-v1/asset/asset.service.spec.ts @@ -130,7 +130,7 @@ describe('AssetService', () => { const dto = _getCreateAssetDto(); assetRepositoryMock.create.mockResolvedValue(assetEntity); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' }); @@ -150,7 +150,7 @@ describe('AssetService', () => { assetRepositoryMock.create.mockRejectedValue(error); assetRepositoryMock.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' }); @@ -167,7 +167,7 @@ describe('AssetService', () => { assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset); assetRepositoryMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); - accessMock.library.hasOwnerAccess.mockResolvedValue(true); + accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!])); await expect( sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion), diff --git a/server/src/immich/api-v1/asset/dto/device-id.dto.ts b/server/src/immich/api-v1/asset/dto/device-id.dto.ts index ff2f4163b5..cae5f60c8b 100644 --- a/server/src/immich/api-v1/asset/dto/device-id.dto.ts +++ b/server/src/immich/api-v1/asset/dto/device-id.dto.ts @@ -1,9 +1,7 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsUUID } from 'class-validator'; +import { IsNotEmpty, IsString } from 'class-validator'; export class DeviceIdDto { @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) + @IsString() deviceId!: string; } diff --git a/server/src/immich/controllers/asset.controller.ts b/server/src/immich/controllers/asset.controller.ts index 105760e502..3a652c2e50 100644 --- a/server/src/immich/controllers/asset.controller.ts +++ b/server/src/immich/controllers/asset.controller.ts @@ -38,6 +38,7 @@ import { StreamableFile, } from '@nestjs/common'; import { ApiOkResponse, ApiTags } from '@nestjs/swagger'; +import { DeviceIdDto } from '../api-v1/asset/dto/device-id.dto'; import { AuthUser, Authenticated, SharedLinkRoute } from '../app.guard'; import { UseValidation, asStreamableFile } from '../app.utils'; import { Route } from '../interceptors'; @@ -100,6 +101,14 @@ export class AssetController { return this.service.downloadFile(authUser, id).then(asStreamableFile); } + /** + * Get all asset of a device that are in the database, ID only. + */ + @Get('/device/:deviceId') + getAllUserAssetsByDeviceId(@AuthUser() authUser: AuthUserDto, @Param() { deviceId }: DeviceIdDto) { + return this.service.getUserAssetsByDeviceId(authUser, deviceId); + } + @Get('statistics') getAssetStatistics(@AuthUser() authUser: AuthUserDto, @Query() dto: AssetStatsDto): Promise { return this.service.getStatistics(authUser, dto); diff --git a/server/src/immich/controllers/search.controller.ts b/server/src/immich/controllers/search.controller.ts index ffa454cd50..b3de4b26c5 100644 --- a/server/src/immich/controllers/search.controller.ts +++ b/server/src/immich/controllers/search.controller.ts @@ -19,11 +19,6 @@ import { UseValidation } from '../app.utils'; export class SearchController { constructor(private service: SearchService) {} - @Get('person') - searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise { - return this.service.searchPerson(authUser, dto); - } - @Get() search(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchDto): Promise { return this.service.search(authUser, dto); @@ -33,4 +28,9 @@ export class SearchController { getExploreData(@AuthUser() authUser: AuthUserDto): Promise { return this.service.getExploreData(authUser) as Promise; } + + @Get('person') + searchPerson(@AuthUser() authUser: AuthUserDto, @Query() dto: SearchPeopleDto): Promise { + return this.service.searchPerson(authUser, dto); + } } diff --git a/server/src/infra/entities/asset-face.entity.ts b/server/src/infra/entities/asset-face.entity.ts index 66f5c2fd14..c47074d2ea 100644 --- a/server/src/infra/entities/asset-face.entity.ts +++ b/server/src/infra/entities/asset-face.entity.ts @@ -1,8 +1,9 @@ -import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Column, Entity, Index, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; import { AssetEntity } from './asset.entity'; import { PersonEntity } from './person.entity'; @Entity('asset_faces') +@Index(['personId', 'assetId']) export class AssetFaceEntity { @PrimaryGeneratedColumn('uuid') id!: string; diff --git a/server/src/infra/entities/asset.entity.ts b/server/src/infra/entities/asset.entity.ts index 93050b23cc..b1f254da42 100644 --- a/server/src/infra/entities/asset.entity.ts +++ b/server/src/infra/entities/asset.entity.ts @@ -33,6 +33,7 @@ export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum'; @Index('IDX_day_of_month', { synchronize: false }) @Index('IDX_month', { synchronize: false }) @Index('IDX_originalPath_libraryId', ['originalPath', 'libraryId']) +@Index(['stackParentId']) // For all assets, each originalpath must be unique per user and library export class AssetEntity { @PrimaryGeneratedColumn('uuid') diff --git a/server/src/infra/entities/geodata-admin1.entity.ts b/server/src/infra/entities/geodata-admin1.entity.ts new file mode 100644 index 0000000000..36cf0a805e --- /dev/null +++ b/server/src/infra/entities/geodata-admin1.entity.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('geodata_admin1') +export class GeodataAdmin1Entity { + @PrimaryColumn({ type: 'varchar' }) + key!: string; + + @Column({ type: 'varchar' }) + name!: string; +} diff --git a/server/src/infra/entities/geodata-admin2.entity.ts b/server/src/infra/entities/geodata-admin2.entity.ts new file mode 100644 index 0000000000..bd03e83776 --- /dev/null +++ b/server/src/infra/entities/geodata-admin2.entity.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('geodata_admin2') +export class GeodataAdmin2Entity { + @PrimaryColumn({ type: 'varchar' }) + key!: string; + + @Column({ type: 'varchar' }) + name!: string; +} diff --git a/server/src/infra/entities/geodata-places.entity.ts b/server/src/infra/entities/geodata-places.entity.ts new file mode 100644 index 0000000000..244e4261b0 --- /dev/null +++ b/server/src/infra/entities/geodata-places.entity.ts @@ -0,0 +1,59 @@ +import { GeodataAdmin1Entity } from '@app/infra/entities/geodata-admin1.entity'; +import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity'; +import { Column, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; + +@Entity('geodata_places', { synchronize: false }) +export class GeodataPlacesEntity { + @PrimaryColumn({ type: 'integer' }) + id!: number; + + @Column({ type: 'varchar', length: 200 }) + name!: string; + + @Column({ type: 'float' }) + longitude!: number; + + @Column({ type: 'float' }) + latitude!: number; + + // @Column({ + // generatedType: 'STORED', + // asExpression: 'll_to_earth((latitude)::double precision, (longitude)::double precision)', + // type: 'earth', + // }) + earthCoord!: unknown; + + @Column({ type: 'char', length: 2 }) + countryCode!: string; + + @Column({ type: 'varchar', length: 20, nullable: true }) + admin1Code!: string; + + @Column({ type: 'varchar', length: 80, nullable: true }) + admin2Code!: string; + + @Column({ + type: 'varchar', + generatedType: 'STORED', + asExpression: `"countryCode" || '.' || "admin1Code"`, + nullable: true, + }) + admin1Key!: string; + + @ManyToOne(() => GeodataAdmin1Entity, { eager: true, nullable: true, createForeignKeyConstraints: false }) + admin1!: GeodataAdmin1Entity; + + @Column({ + type: 'varchar', + generatedType: 'STORED', + asExpression: `"countryCode" || '.' || "admin1Code" || '.' || "admin2Code"`, + nullable: true, + }) + admin2Key!: string; + + @ManyToOne(() => GeodataAdmin2Entity, { eager: true, nullable: true, createForeignKeyConstraints: false }) + admin2!: GeodataAdmin2Entity; + + @Column({ type: 'date' }) + modificationDate!: Date; +} diff --git a/server/src/infra/entities/index.ts b/server/src/infra/entities/index.ts index e4b5c38b4d..6c662a20ad 100644 --- a/server/src/infra/entities/index.ts +++ b/server/src/infra/entities/index.ts @@ -1,3 +1,4 @@ +import { GeodataAdmin2Entity } from '@app/infra/entities/geodata-admin2.entity'; import { ActivityEntity } from './activity.entity'; import { AlbumEntity } from './album.entity'; import { APIKeyEntity } from './api-key.entity'; @@ -6,6 +7,8 @@ import { AssetJobStatusEntity } from './asset-job-status.entity'; import { AssetEntity } from './asset.entity'; import { AuditEntity } from './audit.entity'; import { ExifEntity } from './exif.entity'; +import { GeodataAdmin1Entity } from './geodata-admin1.entity'; +import { GeodataPlacesEntity } from './geodata-places.entity'; import { LibraryEntity } from './library.entity'; import { MoveEntity } from './move.entity'; import { PartnerEntity } from './partner.entity'; @@ -13,6 +16,7 @@ import { PersonEntity } from './person.entity'; import { SharedLinkEntity } from './shared-link.entity'; import { SmartInfoEntity } from './smart-info.entity'; import { SystemConfigEntity } from './system-config.entity'; +import { SystemMetadataEntity } from './system-metadata.entity'; import { TagEntity } from './tag.entity'; import { UserTokenEntity } from './user-token.entity'; import { UserEntity } from './user.entity'; @@ -25,6 +29,9 @@ export * from './asset-job-status.entity'; export * from './asset.entity'; export * from './audit.entity'; export * from './exif.entity'; +export * from './geodata-admin1.entity'; +export * from './geodata-admin2.entity'; +export * from './geodata-places.entity'; export * from './library.entity'; export * from './move.entity'; export * from './partner.entity'; @@ -32,6 +39,7 @@ export * from './person.entity'; export * from './shared-link.entity'; export * from './smart-info.entity'; export * from './system-config.entity'; +export * from './system-metadata.entity'; export * from './tag.entity'; export * from './user-token.entity'; export * from './user.entity'; @@ -45,12 +53,16 @@ export const databaseEntities = [ AssetJobStatusEntity, AuditEntity, ExifEntity, + GeodataPlacesEntity, + GeodataAdmin1Entity, + GeodataAdmin2Entity, MoveEntity, PartnerEntity, PersonEntity, SharedLinkEntity, SmartInfoEntity, SystemConfigEntity, + SystemMetadataEntity, TagEntity, UserEntity, UserTokenEntity, diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 84e72e6380..f6c14e1a7d 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -66,7 +66,6 @@ export enum SystemConfigKey { MAP_DARK_STYLE = 'map.darkStyle', REVERSE_GEOCODING_ENABLED = 'reverseGeocoding.enabled', - REVERSE_GEOCODING_CITIES_FILE_OVERRIDE = 'reverseGeocoding.citiesFileOverride', NEW_VERSION_CHECK_ENABLED = 'newVersionCheck.enabled', @@ -145,13 +144,6 @@ export enum Colorspace { P3 = 'p3', } -export enum CitiesFile { - CITIES_15000 = 'cities15000', - CITIES_5000 = 'cities5000', - CITIES_1000 = 'cities1000', - CITIES_500 = 'cities500', -} - export interface SystemConfig { ffmpeg: { crf: number; @@ -200,7 +192,6 @@ export interface SystemConfig { }; reverseGeocoding: { enabled: boolean; - citiesFileOverride: CitiesFile; }; oauth: { enabled: boolean; diff --git a/server/src/infra/entities/system-metadata.entity.ts b/server/src/infra/entities/system-metadata.entity.ts new file mode 100644 index 0000000000..623806db79 --- /dev/null +++ b/server/src/infra/entities/system-metadata.entity.ts @@ -0,0 +1,18 @@ +import { Column, Entity, PrimaryColumn } from 'typeorm'; + +@Entity('system_metadata') +export class SystemMetadataEntity { + @PrimaryColumn() + key!: string; + + @Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } }) + value!: { [key: string]: unknown }; +} + +export enum SystemMetadataKey { + REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', +} + +export interface SystemMetadata extends Record { + [SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string }; +} diff --git a/server/src/infra/infra.config.ts b/server/src/infra/infra.config.ts index 90477d8ca3..7f24230326 100644 --- a/server/src/infra/infra.config.ts +++ b/server/src/infra/infra.config.ts @@ -74,6 +74,3 @@ function parseTypeSenseConfig(): ConfigurationOptions { } export const typesenseConfig: ConfigurationOptions = parseTypeSenseConfig(); - -export const REVERSE_GEOCODING_DUMP_DIRECTORY = - process.env.REVERSE_GEOCODING_DUMP_DIRECTORY || process.cwd() + '/.reverse-geocoding-dump/'; diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts index 276058c0b3..e0d5711d63 100644 --- a/server/src/infra/infra.module.ts +++ b/server/src/infra/infra.module.ts @@ -21,6 +21,7 @@ import { ISmartInfoRepository, IStorageRepository, ISystemConfigRepository, + ISystemMetadataRepository, ITagRepository, IUserRepository, IUserTokenRepository, @@ -56,6 +57,7 @@ import { SharedLinkRepository, SmartInfoRepository, SystemConfigRepository, + SystemMetadataRepository, TagRepository, TypesenseRepository, UserRepository, @@ -84,6 +86,7 @@ const providers: Provider[] = [ { provide: ISmartInfoRepository, useClass: SmartInfoRepository }, { provide: IStorageRepository, useClass: FilesystemProvider }, { provide: ISystemConfigRepository, useClass: SystemConfigRepository }, + { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository }, { provide: ITagRepository, useClass: TagRepository }, { provide: IMediaRepository, useClass: MediaRepository }, { provide: IUserRepository, useClass: UserRepository }, diff --git a/server/src/infra/migrations/1700345818045-SystemMetadata.ts b/server/src/infra/migrations/1700345818045-SystemMetadata.ts new file mode 100644 index 0000000000..0bd9162db7 --- /dev/null +++ b/server/src/infra/migrations/1700345818045-SystemMetadata.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class SystemMetadata1700345818045 implements MigrationInterface { + name = 'SystemMetadata1700345818045' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL DEFAULT '{}', CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key"))`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "system_metadata"`); + } + +} diff --git a/server/src/infra/migrations/1700362016675-Geodata.ts b/server/src/infra/migrations/1700362016675-Geodata.ts new file mode 100644 index 0000000000..1ef562ff7e --- /dev/null +++ b/server/src/infra/migrations/1700362016675-Geodata.ts @@ -0,0 +1,29 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class Geodata1700362016675 implements MigrationInterface { + name = 'Geodata1700362016675' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS cube`) + await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS earthdistance`) + await queryRunner.query(`CREATE TABLE "geodata_admin2" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_1e3886455dbb684d6f6b4756726" PRIMARY KEY ("key"))`); + await queryRunner.query(`CREATE TABLE "geodata_admin1" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_3fe3a89c5aac789d365871cb172" PRIMARY KEY ("key"))`); + await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin1Key","\"countryCode\" || '.' || \"admin1Code\""]); + await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin2Key","\"countryCode\" || '.' || \"admin1Code\" || '.' || \"admin2Code\""]); + await queryRunner.query(`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "admin1Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED, "admin2Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code" || '.' || "admin2Code") STORED, "modificationDate" date NOT NULL, CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "geodata_places" ADD "earthCoord" earth GENERATED ALWAYS AS (ll_to_earth(latitude, longitude)) STORED`) + await queryRunner.query(`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" USING gist ("earthCoord");`) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_geodata_gist_earthcoord"`); + await queryRunner.query(`DROP TABLE "geodata_places"`); + await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin2Key","immich","public","geodata_places"]); + await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin1Key","immich","public","geodata_places"]); + await queryRunner.query(`DROP TABLE "geodata_admin1"`); + await queryRunner.query(`DROP TABLE "geodata_admin2"`); + await queryRunner.query(`DROP EXTENSION cube`); + await queryRunner.query(`DROP EXTENSION earthdistance`); + } + +} diff --git a/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts b/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts new file mode 100644 index 0000000000..723b22b3d1 --- /dev/null +++ b/server/src/infra/migrations/1700752078178-AddAssetFaceIndicies.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAssetFaceIndicies1700752078178 implements MigrationInterface { + name = 'AddAssetFaceIndicies1700752078178' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE INDEX "IDX_bf339a24070dac7e71304ec530" ON "asset_faces" ("personId", "assetId") `); + await queryRunner.query(`CREATE INDEX "IDX_b463c8edb01364bf2beba08ef1" ON "assets" ("stackParentId") `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`); + await queryRunner.query(`DROP INDEX "public"."IDX_bf339a24070dac7e71304ec530"`); + } + +} diff --git a/server/src/infra/repositories/access.repository.ts b/server/src/infra/repositories/access.repository.ts index fa58628851..b23c559a61 100644 --- a/server/src/infra/repositories/access.repository.ts +++ b/server/src/infra/repositories/access.repository.ts @@ -1,6 +1,6 @@ import { IAccessRepository } from '@app/domain'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; +import { In, Repository } from 'typeorm'; import { ActivityEntity, AlbumEntity, @@ -62,33 +62,52 @@ export class AccessRepository implements IAccessRepository { }); }, }; + library = { - hasOwnerAccess: (userId: string, libraryId: string): Promise => { - return this.libraryRepository.exist({ - where: { - id: libraryId, - ownerId: userId, - }, - }); + checkOwnerAccess: async (userId: string, libraryIds: Set): Promise> => { + if (libraryIds.size === 0) { + return new Set(); + } + + return this.libraryRepository + .find({ + select: { id: true }, + where: { + id: In([...libraryIds]), + ownerId: userId, + }, + }) + .then((libraries) => new Set(libraries.map((library) => library.id))); }, - hasPartnerAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWithId: userId, - sharedById: partnerId, - }, - }); + + checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; timeline = { - hasPartnerAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedWithId: userId, - sharedById: partnerId, - }, - }); + checkPartnerAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; @@ -198,66 +217,109 @@ export class AccessRepository implements IAccessRepository { }; authDevice = { - hasOwnerAccess: (userId: string, deviceId: string): Promise => { - return this.tokenRepository.exist({ - where: { - userId, - id: deviceId, - }, - }); + checkOwnerAccess: async (userId: string, deviceIds: Set): Promise> => { + if (deviceIds.size === 0) { + return new Set(); + } + + return this.tokenRepository + .find({ + select: { id: true }, + where: { + userId, + id: In([...deviceIds]), + }, + }) + .then((tokens) => new Set(tokens.map((token) => token.id))); }, }; album = { - hasOwnerAccess: (userId: string, albumId: string): Promise => { - return this.albumRepository.exist({ - where: { - id: albumId, - ownerId: userId, - }, - }); - }, + checkOwnerAccess: async (userId: string, albumIds: Set): Promise> => { + if (albumIds.size === 0) { + return new Set(); + } - hasSharedAlbumAccess: (userId: string, albumId: string): Promise => { - return this.albumRepository.exist({ - where: { - id: albumId, - sharedUsers: { - id: userId, + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + ownerId: userId, }, - }, - }); + }) + .then((albums) => new Set(albums.map((album) => album.id))); }, - hasSharedLinkAccess: (sharedLinkId: string, albumId: string): Promise => { - return this.sharedLinkRepository.exist({ - where: { - id: sharedLinkId, - albumId, - }, - }); + checkSharedAlbumAccess: async (userId: string, albumIds: Set): Promise> => { + if (albumIds.size === 0) { + return new Set(); + } + + return this.albumRepository + .find({ + select: { id: true }, + where: { + id: In([...albumIds]), + sharedUsers: { + id: userId, + }, + }, + }) + .then((albums) => new Set(albums.map((album) => album.id))); + }, + + checkSharedLinkAccess: async (sharedLinkId: string, albumIds: Set): Promise> => { + if (albumIds.size === 0) { + return new Set(); + } + + return this.sharedLinkRepository + .find({ + select: { albumId: true }, + where: { + id: sharedLinkId, + albumId: In([...albumIds]), + }, + }) + .then( + (sharedLinks) => + new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))), + ); }, }; person = { - hasOwnerAccess: (userId: string, personId: string): Promise => { - return this.personRepository.exist({ - where: { - id: personId, - ownerId: userId, - }, - }); + checkOwnerAccess: async (userId: string, personIds: Set): Promise> => { + if (personIds.size === 0) { + return new Set(); + } + + return this.personRepository + .find({ + select: { id: true }, + where: { + id: In([...personIds]), + ownerId: userId, + }, + }) + .then((persons) => new Set(persons.map((person) => person.id))); }, }; partner = { - hasUpdateAccess: (userId: string, partnerId: string): Promise => { - return this.partnerRepository.exist({ - where: { - sharedById: partnerId, - sharedWithId: userId, - }, - }); + checkUpdateAccess: async (userId: string, partnerIds: Set): Promise> => { + if (partnerIds.size === 0) { + return new Set(); + } + + return this.partnerRepository + .createQueryBuilder('partner') + .select('partner.sharedById') + .where('partner.sharedById IN (:...partnerIds)', { partnerIds: [...partnerIds] }) + .andWhere('partner.sharedWithId = :userId', { userId }) + .getMany() + .then((partners) => new Set(partners.map((partner) => partner.sharedById))); }, }; } diff --git a/server/src/infra/repositories/album.repository.ts b/server/src/infra/repositories/album.repository.ts index 69df226859..e6c2797261 100644 --- a/server/src/infra/repositories/album.repository.ts +++ b/server/src/infra/repositories/album.repository.ts @@ -59,25 +59,30 @@ export class AlbumRepository implements IAlbumRepository { }); } - async getAssetCountForIds(ids: string[]): Promise { + async getMetadataForIds(ids: string[]): Promise { // Guard against running invalid query when ids list is empty. if (!ids.length) { return []; } // Only possible with query builder because of GROUP BY. - const countByAlbums = await this.repository + const albumMetadatas = await this.repository .createQueryBuilder('album') .select('album.id') - .addSelect('COUNT(albums_assets.assetsId)', 'asset_count') - .leftJoin('albums_assets_assets', 'albums_assets', 'albums_assets.albumsId = album.id') + .addSelect('MIN(assets.fileCreatedAt)', 'start_date') + .addSelect('MAX(assets.fileCreatedAt)', 'end_date') + .addSelect('COUNT(album_assets.assetsId)', 'asset_count') + .leftJoin('albums_assets_assets', 'album_assets', 'album_assets.albumsId = album.id') + .leftJoin('assets', 'assets', 'assets.id = album_assets.assetsId') .where('album.id IN (:...ids)', { ids }) .groupBy('album.id') .getRawMany(); - return countByAlbums.map((albumCount) => ({ - albumId: albumCount['album_id'], - assetCount: Number(albumCount['asset_count']), + return albumMetadatas.map((metadatas) => ({ + albumId: metadatas['album_id'], + assetCount: Number(metadatas['asset_count']), + startDate: metadatas['end_date'] ? new Date(metadatas['start_date']) : undefined, + endDate: metadatas['end_date'] ? new Date(metadatas['end_date']) : undefined, })); } diff --git a/server/src/infra/repositories/asset.repository.ts b/server/src/infra/repositories/asset.repository.ts index b6fa10c0d7..59c29a9d22 100644 --- a/server/src/infra/repositories/asset.repository.ts +++ b/server/src/infra/repositories/asset.repository.ts @@ -326,6 +326,27 @@ export class AssetRepository implements IAssetRepository { }); } + /** + * Get assets by device's Id on the database + * @param ownerId + * @param deviceId + * + * @returns Promise - Array of assetIds belong to the device + */ + async getAllByDeviceId(ownerId: string, deviceId: string): Promise { + const items = await this.repository.find({ + select: { deviceAssetId: true }, + where: { + ownerId, + deviceId, + isVisible: true, + }, + withDeleted: true, + }); + + return items.map((asset) => asset.deviceAssetId); + } + getById(id: string): Promise { return this.repository.findOne({ where: { id }, diff --git a/server/src/infra/repositories/index.ts b/server/src/infra/repositories/index.ts index 81ea7dd81f..0324fef43c 100644 --- a/server/src/infra/repositories/index.ts +++ b/server/src/infra/repositories/index.ts @@ -19,6 +19,7 @@ export * from './server-info.repository'; export * from './shared-link.repository'; export * from './smart-info.repository'; export * from './system-config.repository'; +export * from './system-metadata.repository'; export * from './tag.repository'; export * from './typesense.repository'; export * from './user-token.repository'; diff --git a/server/src/infra/repositories/metadata.repository.ts b/server/src/infra/repositories/metadata.repository.ts index 1557a10d76..877c0e007d 100644 --- a/server/src/infra/repositories/metadata.repository.ts +++ b/server/src/infra/repositories/metadata.repository.ts @@ -1,77 +1,182 @@ -import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from '@app/domain'; -import { REVERSE_GEOCODING_DUMP_DIRECTORY } from '@app/infra'; -import { Injectable, Logger } from '@nestjs/common'; -import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored'; -import { readdir, rm } from 'fs/promises'; +import { + GeoPoint, + IMetadataRepository, + ImmichTags, + ISystemMetadataRepository, + ReverseGeocodeResult, +} from '@app/domain'; +import { GeodataAdmin1Entity, GeodataAdmin2Entity, GeodataPlacesEntity, SystemMetadataKey } from '@app/infra/entities'; +import { DatabaseLock } from '@app/infra/utils/database-locks'; +import { Inject, Logger } from '@nestjs/common'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored'; +import { createReadStream, existsSync } from 'fs'; +import { readFile } from 'fs/promises'; import * as geotz from 'geo-tz'; import { getName } from 'i18n-iso-countries'; -import geocoder, { AddressObject, InitOptions } from 'local-reverse-geocoder'; -import path from 'path'; -import { promisify } from 'util'; +import * as readLine from 'readline'; +import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm'; -export interface AdminCode { - name: string; - asciiName: string; - geoNameId: string; -} +type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity; +type GeoEntityClass = typeof GeodataPlacesEntity | typeof GeodataAdmin1Entity | typeof GeodataAdmin2Entity; -export type GeoData = AddressObject & { - admin1Code?: AdminCode | string; - admin2Code?: AdminCode | string; -}; +const CITIES_FILE = 'cities500.txt'; -const lookup = promisify(geocoder.lookUp).bind(geocoder); - -@Injectable() export class MetadataRepository implements IMetadataRepository { + constructor( + @InjectRepository(GeodataPlacesEntity) private readonly geodataPlacesRepository: Repository, + @InjectRepository(GeodataAdmin1Entity) private readonly geodataAdmin1Repository: Repository, + @InjectRepository(GeodataAdmin2Entity) private readonly geodataAdmin2Repository: Repository, + @Inject(ISystemMetadataRepository) private readonly systemMetadataRepository: ISystemMetadataRepository, + @InjectDataSource() private dataSource: DataSource, + ) {} + private logger = new Logger(MetadataRepository.name); - async init(options: Partial): Promise { - return new Promise((resolve) => { - geocoder.init( - { - load: { - admin1: true, - admin2: true, - admin3And4: false, - alternateNames: false, - }, - countries: [], - dumpDirectory: REVERSE_GEOCODING_DUMP_DIRECTORY, - ...options, - }, - resolve, - ); + async init(): Promise { + this.logger.log('Initializing metadata repository'); + const geodataDate = await readFile('/usr/src/resources/geodata-date.txt', 'utf8'); + + await this.geodataPlacesRepository.query('SELECT pg_advisory_lock($1)', [DatabaseLock.GeodataImport]); + + const geocodingMetadata = await this.systemMetadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); + + if (geocodingMetadata?.lastUpdate === geodataDate) { + await this.dataSource.query('SELECT pg_advisory_unlock($1)', [DatabaseLock.GeodataImport]); + return; + } + + this.logger.log('Importing geodata to database from file'); + + const queryRunner = this.dataSource.createQueryRunner(); + await queryRunner.connect(); + + try { + await queryRunner.startTransaction(); + + await this.loadCities500(queryRunner); + await this.loadAdmin1(queryRunner); + await this.loadAdmin2(queryRunner); + + await queryRunner.commitTransaction(); + } catch (e) { + this.logger.fatal('Error importing geodata', e); + await queryRunner.rollbackTransaction(); + throw e; + } finally { + await queryRunner.release(); + } + + await this.systemMetadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, { + lastUpdate: geodataDate, + lastImportFileName: CITIES_FILE, }); + + await this.dataSource.query('SELECT pg_advisory_unlock($1)', [DatabaseLock.GeodataImport]); + this.logger.log('Geodata import completed'); + } + + private async loadGeodataToTableFromFile( + queryRunner: QueryRunner, + lineToEntityMapper: (lineSplit: string[]) => T, + filePath: string, + entity: GeoEntityClass, + ) { + if (!existsSync(filePath)) { + this.logger.error(`Geodata file ${filePath} not found`); + throw new Error(`Geodata file ${filePath} not found`); + } + await queryRunner.manager.clear(entity); + + const input = createReadStream(filePath); + let buffer: DeepPartial[] = []; + const lineReader = readLine.createInterface({ input: input }); + + for await (const line of lineReader) { + const lineSplit = line.split('\t'); + buffer.push(lineToEntityMapper(lineSplit)); + if (buffer.length > 1000) { + await queryRunner.manager.save(buffer); + buffer = []; + } + } + await queryRunner.manager.save(buffer); + } + + private async loadCities500(queryRunner: QueryRunner) { + await this.loadGeodataToTableFromFile( + queryRunner, + (lineSplit: string[]) => + this.geodataPlacesRepository.create({ + id: parseInt(lineSplit[0]), + name: lineSplit[1], + latitude: parseFloat(lineSplit[4]), + longitude: parseFloat(lineSplit[5]), + countryCode: lineSplit[8], + admin1Code: lineSplit[10], + admin2Code: lineSplit[11], + modificationDate: lineSplit[18], + }), + `/usr/src/resources/${CITIES_FILE}`, + GeodataPlacesEntity, + ); + } + + private async loadAdmin1(queryRunner: QueryRunner) { + await this.loadGeodataToTableFromFile( + queryRunner, + (lineSplit: string[]) => + this.geodataAdmin1Repository.create({ + key: lineSplit[0], + name: lineSplit[1], + }), + '/usr/src/resources/admin1CodesASCII.txt', + GeodataAdmin1Entity, + ); + } + + private async loadAdmin2(queryRunner: QueryRunner) { + await this.loadGeodataToTableFromFile( + queryRunner, + (lineSplit: string[]) => + this.geodataAdmin2Repository.create({ + key: lineSplit[0], + name: lineSplit[1], + }), + '/usr/src/resources/admin2Codes.txt', + GeodataAdmin2Entity, + ); } async teardown() { await exiftool.end(); } - async deleteCache() { - const dumpDirectory = REVERSE_GEOCODING_DUMP_DIRECTORY; - if (dumpDirectory) { - // delete contents - const items = await readdir(dumpDirectory, { withFileTypes: true }); - const folders = items.filter((item) => item.isDirectory()); - for (const { name } of folders) { - await rm(path.join(dumpDirectory, name), { recursive: true, force: true }); - } - } - } - - async reverseGeocode(point: GeoPoint): Promise { + async reverseGeocode(point: GeoPoint): Promise { this.logger.debug(`Request: ${point.latitude},${point.longitude}`); - const [address] = await lookup([point], 1); - this.logger.verbose(`Raw: ${JSON.stringify(address, null, 2)}`); + const response = await this.geodataPlacesRepository + .createQueryBuilder('geoplaces') + .leftJoinAndSelect('geoplaces.admin1', 'admin1') + .leftJoinAndSelect('geoplaces.admin2', 'admin2') + .where('earth_box(ll_to_earth(:latitude, :longitude), 25000) @> "earthCoord"', point) + .orderBy('earth_distance(ll_to_earth(:latitude, :longitude), "earthCoord")') + .limit(1) + .getOne(); - const { countryCode, name: city, admin1Code, admin2Code } = address[0] as GeoData; + if (!response) { + this.logger.warn( + `Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`, + ); + return null; + } + + this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`); + + const { countryCode, name: city, admin1, admin2 } = response; const country = getName(countryCode, 'en') ?? null; - const stateParts = [(admin2Code as AdminCode)?.name, (admin1Code as AdminCode)?.name].filter((name) => !!name); + const stateParts = [admin2?.name, admin1?.name].filter((name) => !!name); const state = stateParts.length > 0 ? stateParts.join(', ') : null; - this.logger.debug(`Normalized: ${JSON.stringify({ country, state, city })}`); return { country, state, city }; } diff --git a/server/src/infra/repositories/system-metadata.repository.ts b/server/src/infra/repositories/system-metadata.repository.ts new file mode 100644 index 0000000000..d43002b0be --- /dev/null +++ b/server/src/infra/repositories/system-metadata.repository.ts @@ -0,0 +1,22 @@ +import { ISystemMetadataRepository } from '@app/domain/repositories/system-metadata.repository'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { SystemMetadata, SystemMetadataEntity } from '../entities'; + +export class SystemMetadataRepository implements ISystemMetadataRepository { + constructor( + @InjectRepository(SystemMetadataEntity) + private repository: Repository, + ) {} + async get(key: T): Promise { + const metadata = await this.repository.findOne({ where: { key } }); + if (!metadata) { + return null; + } + return metadata.value as SystemMetadata[T]; + } + + async set(key: T, value: SystemMetadata[T]): Promise { + await this.repository.upsert({ key, value }, { conflictPaths: { key: true } }); + } +} diff --git a/server/src/infra/utils/database-locks.ts b/server/src/infra/utils/database-locks.ts new file mode 100644 index 0000000000..756437743b --- /dev/null +++ b/server/src/infra/utils/database-locks.ts @@ -0,0 +1,3 @@ +export enum DatabaseLock { + GeodataImport = 100, +} diff --git a/server/src/microservices/app.service.ts b/server/src/microservices/app.service.ts index 0386d0f078..3f89fa06fa 100644 --- a/server/src/microservices/app.service.ts +++ b/server/src/microservices/app.service.ts @@ -93,16 +93,6 @@ export class AppService { [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(), }); - process.on('uncaughtException', async (error: Error | any) => { - const isCsvError = error.code === 'CSV_RECORD_INCONSISTENT_FIELDS_LENGTH'; - if (!isCsvError) { - throw error; - } - - this.logger.warn('Geocoding csv parse error, trying again without cache...'); - await this.metadataService.init(true); - }); - await this.metadataService.init(); await this.searchService.init(); } diff --git a/server/test/e2e/album.e2e-spec.ts b/server/test/e2e/album.e2e-spec.ts index f5f6bb66ec..8058d9593c 100644 --- a/server/test/e2e/album.e2e-spec.ts +++ b/server/test/e2e/album.e2e-spec.ts @@ -246,7 +246,7 @@ describe(`${AlbumController.name} (e2e)`, () => { it('should return album info for own album', async () => { const { status, body } = await request(server) - .get(`/album/${user1Albums[0].id}`) + .get(`/album/${user1Albums[0].id}?withoutAssets=false`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); @@ -255,12 +255,34 @@ describe(`${AlbumController.name} (e2e)`, () => { it('should return album info for shared album', async () => { const { status, body } = await request(server) - .get(`/album/${user2Albums[0].id}`) + .get(`/album/${user2Albums[0].id}?withoutAssets=false`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toEqual(user2Albums[0]); }); + + it('should return album info with assets when withoutAssets is undefined', async () => { + const { status, body } = await request(server) + .get(`/album/${user1Albums[0].id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual(user1Albums[0]); + }); + + it('should return album info without assets when withoutAssets is true', async () => { + const { status, body } = await request(server) + .get(`/album/${user1Albums[0].id}?withoutAssets=true`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user1Albums[0], + assets: [], + assetCount: 1, + }); + }); }); describe('PUT /album/:id/assets', () => { diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 8f1e9355d8..f495d800e0 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -30,30 +30,30 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => }, album: { - hasOwnerAccess: jest.fn(), - hasSharedAlbumAccess: jest.fn(), - hasSharedLinkAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + checkSharedAlbumAccess: jest.fn().mockResolvedValue(new Set()), + checkSharedLinkAccess: jest.fn().mockResolvedValue(new Set()), }, authDevice: { - hasOwnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, library: { - hasOwnerAccess: jest.fn(), - hasPartnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, timeline: { - hasPartnerAccess: jest.fn(), + checkPartnerAccess: jest.fn().mockResolvedValue(new Set()), }, person: { - hasOwnerAccess: jest.fn(), + checkOwnerAccess: jest.fn().mockResolvedValue(new Set()), }, partner: { - hasUpdateAccess: jest.fn(), + checkUpdateAccess: jest.fn().mockResolvedValue(new Set()), }, }; }; diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts index 7cd0a846b3..36c3afb297 100644 --- a/server/test/repositories/album.repository.mock.ts +++ b/server/test/repositories/album.repository.mock.ts @@ -5,7 +5,7 @@ export const newAlbumRepositoryMock = (): jest.Mocked => { getById: jest.fn(), getByIds: jest.fn(), getByAssetId: jest.fn(), - getAssetCountForIds: jest.fn(), + getMetadataForIds: jest.fn(), getInvalidThumbnail: jest.fn(), getOwned: jest.fn(), getShared: jest.fn(), diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index 566b7733a1..88bbdabcfd 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -18,6 +18,7 @@ export const newAssetRepositoryMock = (): jest.Mocked => { getFirstAssetForAlbumId: jest.fn(), getLastUpdatedAssetForAlbumId: jest.fn(), getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }), + getAllByDeviceId: jest.fn(), updateAll: jest.fn(), getByLibraryId: jest.fn(), getByLibraryIdAndOriginalPath: jest.fn(), diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts index e31ba87a2c..18aeb31ccc 100644 --- a/server/test/repositories/metadata.repository.mock.ts +++ b/server/test/repositories/metadata.repository.mock.ts @@ -3,6 +3,7 @@ import { IMetadataRepository } from '@app/domain'; export const newMetadataRepositoryMock = (): jest.Mocked => { return { deleteCache: jest.fn(), + getExifTags: jest.fn(), init: jest.fn(), teardown: jest.fn(), reverseGeocode: jest.fn(), diff --git a/server/test/test-utils.ts b/server/test/test-utils.ts index 2cbd4f19a6..dc7c1b6988 100644 --- a/server/test/test-utils.ts +++ b/server/test/test-utils.ts @@ -25,7 +25,9 @@ export const db = { const tableNames = entities.length > 0 ? entities.map((entity) => em.getRepository(entity).metadata.tableName) - : dataSource.entityMetadatas.map((entity) => entity.tableName); + : dataSource.entityMetadatas + .map((entity) => entity.tableName) + .filter((tableName) => !tableName.startsWith('geodata')); let deleteUsers = false; for (const tableName of tableNames) { diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs index fdf66b7005..9277676ac5 100644 --- a/web/.eslintrc.cjs +++ b/web/.eslintrc.cjs @@ -35,5 +35,6 @@ module.exports = { varsIgnorePattern: '^_$', }, ], + curly: 2, }, }; diff --git a/web/.prettierrc b/web/.prettierrc index fb0e0c92b4..33325428a2 100644 --- a/web/.prettierrc +++ b/web/.prettierrc @@ -3,7 +3,6 @@ "trailingComma": "all", "printWidth": 120, "semi": true, - "organizeImportsSkipDestructiveCodeActions": true, "plugins": ["prettier-plugin-svelte"], - "pluginSearchDirs": false + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] } diff --git a/web/Dockerfile b/web/Dockerfile index 544bce8fb5..28d18db558 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,5 +1,6 @@ -FROM node:20.9-alpine3.18 +FROM node:iron-alpine3.18 +USER node WORKDIR /usr/src/app COPY --chown=node:node package*.json ./ RUN npm ci diff --git a/web/package-lock.json b/web/package-lock.json index 67359aef7c..0eef10223b 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@egjs/svelte-view360": "^4.0.0-beta.7", "@mdi/js": "^7.3.67", - "@zoom-image/svelte": "^0.1.8", + "@zoom-image/svelte": "^0.2.0", "axios": "^0.27.2", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", @@ -35,13 +35,12 @@ "@sveltejs/kit": "^1.20.4", "@testing-library/jest-dom": "^6.0.0", "@testing-library/svelte": "^4.0.3", - "@types/cookie": "^0.5.1", "@types/dom-to-image": "^2.6.4", "@types/justified-layout": "^4.1.0", "@types/lodash-es": "^4.17.6", "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "autoprefixer": "^10.4.13", "babel-jest": "^29.4.3", "eslint": "^8.34.0", @@ -52,13 +51,12 @@ "jest": "^29.4.3", "jest-environment-jsdom": "^29.4.3", "postcss": "^8.4.21", - "prettier": "^2.8.4", - "prettier-plugin-svelte": "^2.10.1", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.0.5", "svelte-check": "^3.4.3", "svelte-jester": "^3.0.0", "svelte-preprocess": "^5.0.3", - "tailwind-merge": "^1.14.0", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", "typescript": "^5.0.0", @@ -1993,9 +1991,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3044,9 +3042,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "1.27.5", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.27.5.tgz", - "integrity": "sha512-+L1WPs/ZYNjXoBFoFARypD4aZOjkT51vFpRCtQI45+Fmmfi4Y0dH/8VFlmYD6VlGe89ViIPg7lgf/JpGQ2tr7A==", + "version": "1.27.6", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-1.27.6.tgz", + "integrity": "sha512-GsjTkMbKzXdbeRg0tk8S7HNShQ4879ftRr0ZHaZfjbig1xQwG57Bvcm9U9/mpLJtCapLbLWUnygKrgcLISLC8A==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -3075,6 +3073,12 @@ "vite": "^4.0.0" } }, + "node_modules/@sveltejs/kit/node_modules/@types/cookie": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz", + "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==", + "dev": true + }, "node_modules/@sveltejs/kit/node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -3415,12 +3419,6 @@ "@babel/types": "^7.20.7" } }, - "node_modules/@types/cookie": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.5.4.tgz", - "integrity": "sha512-7z/eR6O859gyWIAjuvBWFzNURmf2oPBmJlfVWkwehU5nzIyjwBsTh7WMmEEV4JFnHuQ3ex4oyTvfKzcyJVDBNA==", - "dev": true - }, "node_modules/@types/dom-to-image": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/@types/dom-to-image/-/dom-to-image-2.6.7.tgz", @@ -3532,15 +3530,15 @@ } }, "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, "node_modules/@types/justified-layout": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.3.tgz", - "integrity": "sha512-Ph0kv9PuAIM+rQo8vyqool1ss1Kc894umFREsSM5hrTuPzCppnHBOvMyFtXozLyhelHBiN88QB7XBbYklHVz2g==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/justified-layout/-/justified-layout-4.1.4.tgz", + "integrity": "sha512-q2ybP0u0NVj87oMnGZOGxY2iUN8ddr48zPOBHBdbOLpsMTA/keGj+93ou+OMCnJk0xewzlNIaVEkxM6VBD3E2w==", "dev": true }, "node_modules/@types/lodash": { @@ -3550,18 +3548,18 @@ "dev": true }, "node_modules/@types/lodash-es": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.11.tgz", - "integrity": "sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==", + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "dev": true, "dependencies": { "@types/lodash": "*" } }, "node_modules/@types/luxon": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.4.tgz", - "integrity": "sha512-H9OXxv4EzJwE75aTPKpiGXJq+y4LFxjpsdgKwSmr503P5DkWc3AG7VAFYrFNVvqemT5DfgZJV9itYhqBHSGujA==", + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.5.tgz", + "integrity": "sha512-1cyf6Ge/94zlaWIZA2ei1pE6SZ8xpad2hXaYa5JEFiaUH0YS494CZwyi4MXNpXD9oEuv6ZH0Bmh0e7F9sPhmZA==", "dev": true }, "node_modules/@types/mapbox__point-geometry": { @@ -3597,9 +3595,9 @@ "dev": true }, "node_modules/@types/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-OxepLK9EuNEIPxWNME+C6WwbRAOOI2o2BaQEGzz5Lu2e4Z5eDnEo+/aVEDMIXywoJitJ7xWd641wrGLZdtwRyw==", + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz", + "integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==", "dev": true }, "node_modules/@types/stack-utils": { @@ -3643,32 +3641,33 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", - "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.13.1.tgz", + "integrity": "sha512-5bQDGkXaxD46bPvQt08BUz9YSaO4S0fB1LB5JHQuXTfkGPI3+UUeS387C/e9jRie5GqT8u5kFTrMvAjtX4O5kA==", "dev": true, "dependencies": { - "@eslint-community/regexpp": "^4.4.0", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/type-utils": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/type-utils": "6.13.1", + "@typescript-eslint/utils": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "natural-compare-lite": "^1.4.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3710,25 +3709,26 @@ "dev": true }, "node_modules/@typescript-eslint/parser": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", - "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.13.1.tgz", + "integrity": "sha512-fs2XOhWCzRhqMmQf0eicLa/CWSaYss2feXsy7xBD/pLyWke/jCIVc2s1ikEAtSW7ina1HNhv7kONoEfVNEcdDQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3737,16 +3737,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", + "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -3754,25 +3754,25 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.62.0.tgz", - "integrity": "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.13.1.tgz", + "integrity": "sha512-A2qPlgpxx2v//3meMqQyB1qqTg1h1dJvzca7TugM3Yc2USDY+fsRBiojAEo92HO7f5hW5mjAUF6qobOPzlBCBQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.62.0", - "@typescript-eslint/utils": "5.62.0", + "@typescript-eslint/typescript-estree": "6.13.1", + "@typescript-eslint/utils": "6.13.1", "debug": "^4.3.4", - "tsutils": "^3.21.0" + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "*" + "eslint": "^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -3781,12 +3781,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", + "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -3794,21 +3794,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", + "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/visitor-keys": "6.13.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -3854,29 +3854,28 @@ "dev": true }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", + "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.13.1", + "@typescript-eslint/types": "6.13.1", + "@typescript-eslint/typescript-estree": "6.13.1", + "semver": "^7.5.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "eslint": "^7.0.0 || ^8.0.0" } }, "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { @@ -3913,16 +3912,16 @@ "dev": true }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", + "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" + "@typescript-eslint/types": "6.13.1", + "eslint-visitor-keys": "^3.4.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^16.0.0 || >=18.0.0" }, "funding": { "type": "opencollective", @@ -3936,9 +3935,9 @@ "dev": true }, "node_modules/@zoom-image/core": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.28.0.tgz", - "integrity": "sha512-+tqhet1Ev/1QcJtKSNYgYJLgPIyQ5BZjbcMZu6bW8o2/ak+UQ1L4UeVyc2Q0Ivro1ltLYBe5ywLRgwzqhyAgkQ==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.31.0.tgz", + "integrity": "sha512-lvFVfIe/CSASXVq1E2vWnt/inXqrBMgjW96lW/l1JdM9EaCj5yis6YXPL5z+Rz2WHmMg5bb7Ps6w1Gzs/bC8LQ==", "dependencies": { "@namnode/store": "^0.1.0" }, @@ -3948,11 +3947,11 @@ } }, "node_modules/@zoom-image/svelte": { - "version": "0.1.21", - "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.1.21.tgz", - "integrity": "sha512-a7Spta7WD1e94InjnWxyqAURBnq24fkEDiLN6sz0BuedxDYaT+kmdhHueXsrNEl7z8RSwXsdC7xABAmV+pEb0w==", + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@zoom-image/svelte/-/svelte-0.2.1.tgz", + "integrity": "sha512-UGOFsXJN5Sk/uJxp7ZMajedXusmdmQ23nTNgphR4T9Q0Aef4qJJZI5dpGZtMCbGH2kdLbpIm30Sbht9kIe1L1Q==", "dependencies": { - "@zoom-image/core": "0.28.0" + "@zoom-image/core": "0.31.0" }, "funding": { "type": "github", @@ -4767,14 +4766,6 @@ "periscopic": "^3.1.0" } }, - "node_modules/code-red/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, "node_modules/collect-v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", @@ -5471,15 +5462,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/js": "8.54.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -5550,9 +5541,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.35.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.0.tgz", - "integrity": "sha512-3WDFxNrkXaMlpqoNo3M1ZOQuoFLMO9+bdnN6oVVXaydXC7nzCJuGy9a0zqoNDHMSRPYt0Rqo6hIdHMEaI5sQnw==", + "version": "2.35.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.35.1.tgz", + "integrity": "sha512-IF8TpLnROSGy98Z3NrsKXWDSCbNY2ReHDcrYTuXZMbfX7VmESISR78TWgO9zdg4Dht1X8coub5jKwHzP0ExRug==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", @@ -5617,19 +5608,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", @@ -5918,13 +5896,12 @@ "node": ">=4.0" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" } }, "node_modules/esutils": { @@ -6934,6 +6911,14 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -9363,9 +9348,9 @@ } }, "node_modules/luxon": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.3.tgz", - "integrity": "sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", "engines": { "node": ">=12" } @@ -9449,9 +9434,9 @@ } }, "node_modules/maplibre-gl": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.6.0.tgz", - "integrity": "sha512-l+jBu+bMy96FOV4em7FgjMH77ewlOtLPXLAem/Q44y4+0vTGsJvPksJSoLoedmikcSff2QN20VZFo3+Zg0UJPQ==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.6.2.tgz", + "integrity": "sha512-krg2KFIdOpLPngONDhP6ixCoWl5kbdMINP0moMSJFVX7wX1Clm2M9hlNKXS8vBGlVWwR5R3ZfI6IPrYz7c+aCQ==", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -9461,11 +9446,11 @@ "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "@maplibre/maplibre-gl-style-spec": "^19.3.3", - "@types/geojson": "^7946.0.12", - "@types/mapbox__point-geometry": "^0.1.3", - "@types/mapbox__vector-tile": "^1.3.3", - "@types/pbf": "^3.0.4", - "@types/supercluster": "^7.1.2", + "@types/geojson": "^7946.0.13", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", "earcut": "^2.2.4", "geojson-vt": "^3.2.1", "gl-matrix": "^3.4.3", @@ -9652,12 +9637,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/natural-compare-lite": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", - "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", - "dev": true - }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -9979,22 +9958,6 @@ "is-reference": "^3.0.0" } }, - "node_modules/periscopic/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/periscopic/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10239,28 +10202,28 @@ } }, "node_modules/prettier": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", - "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", + "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", "dev": true, "bin": { - "prettier": "bin-prettier.js" + "prettier": "bin/prettier.cjs" }, "engines": { - "node": ">=10.13.0" + "node": ">=14" }, "funding": { "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/prettier-plugin-svelte": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-2.10.1.tgz", - "integrity": "sha512-Wlq7Z5v2ueCubWo0TZzKc9XHcm7TDxqcuzRuGd0gcENfzfT4JZ9yDlCbEgxWgiPmLHkBjfOtpAWkcT28MCDpUQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.1.2.tgz", + "integrity": "sha512-7xfMZtwgAWHMT0iZc8jN4o65zgbAQ3+O32V6W7pXrqNvKnHnkoyQCGCbKeUyXKZLbYE0YhFRnamfxfkEGxm8qA==", "dev": true, "peerDependencies": { - "prettier": "^1.16.4 || ^2.0.0", - "svelte": "^3.2.0 || ^4.0.0-next.0" + "prettier": "^3.0.0", + "svelte": "^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0" } }, "node_modules/pretty-format": { @@ -11192,9 +11155,9 @@ } }, "node_modules/svelte": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.3.tgz", - "integrity": "sha512-sqmG9KC6uUc7fb3ZuWoxXvqk6MI9Uu4ABA1M0fYDgTlFYu1k02xp96u6U9+yJZiVm84m9zge7rrA/BNZdFpOKw==", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.7.tgz", + "integrity": "sha512-UExR1KS7raTdycsUrKLtStayu4hpdV3VZQgM0akX8XbXgLBlosdE/Sf3crOgyh9xIjqSYB3UEBuUlIQKRQX2hg==", "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", @@ -11215,9 +11178,9 @@ } }, "node_modules/svelte-check": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.0.tgz", - "integrity": "sha512-8VfqhfuRJ1sKW+o8isH2kPi0RhjXH1nNsIbCFGyoUHG+ZxVxHYRKcb+S8eaL/1tyj3VGvWYx3Y5+oCUsJgnzcw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.6.2.tgz", + "integrity": "sha512-E6iFh4aUCGJLRz6QZXH3gcN/VFfkzwtruWSRmlKrLWQTiO6VzLsivR6q02WYLGNAGecV3EocqZuCDrC2uttZ0g==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", @@ -11331,9 +11294,9 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.7.0.tgz", - "integrity": "sha512-8Mm3MEr0mCq4en5ZmuemCxnv82ljd4mNzTt/pC+X3CTKEcfoVyJgr2PaDu8Znu3DOxUR378XBlG1z5Dw3amnvA==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.7.3.tgz", + "integrity": "sha512-mF/wAHQKqrutC6NxnEBDWfszfcQiYusyyE5ulbRVuwWayC0ZTm9lkm376nmNfgruAJOe0QzPx4Mdxa7c2JlGLA==", "dependencies": { "d3-geo": "^3.1.0", "just-compare": "^2.3.0", @@ -11360,9 +11323,9 @@ } }, "node_modules/svelte-preprocess": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.0.tgz", - "integrity": "sha512-EkErPiDzHAc0k2MF5m6vBNmRUh338h2myhinUw/xaqsLs7/ZvsgREiLGj03VrSzbY/TB5ZXgBOsKraFee5yceA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.1.tgz", + "integrity": "sha512-p/Dp4hmrBW5mrCCq29lEMFpIJT2FZsRlouxEc5qpbOmXRbaFs7clLs8oKPwD3xCFyZfv1bIhvOzpQkhMEVQdMw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -11421,22 +11384,6 @@ } } }, - "node_modules/svelte/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/svelte/node_modules/is-reference": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", - "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", - "dependencies": { - "@types/estree": "*" - } - }, "node_modules/svelte/node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -11454,16 +11401,6 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, - "node_modules/tailwind-merge": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-1.14.0.tgz", - "integrity": "sha512-3mFKyCo/MBcgyOTlrY8T7odzZFx+w+qKSMAmdFzRvqBfLlSigU6TZnlFHK0lkMwj9Bj8OYU+9yW9lmGuS0QEnQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.5.tgz", @@ -11683,6 +11620,18 @@ "node": ">=12" } }, + "node_modules/ts-api-utils": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", + "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -11695,27 +11644,6 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11750,9 +11678,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/web/package.json b/web/package.json index 79fb3d1eeb..db7b2e8cf1 100644 --- a/web/package.json +++ b/web/package.json @@ -28,13 +28,12 @@ "@sveltejs/kit": "^1.20.4", "@testing-library/jest-dom": "^6.0.0", "@testing-library/svelte": "^4.0.3", - "@types/cookie": "^0.5.1", "@types/dom-to-image": "^2.6.4", "@types/justified-layout": "^4.1.0", "@types/lodash-es": "^4.17.6", "@types/luxon": "^3.2.0", - "@typescript-eslint/eslint-plugin": "^5.53.0", - "@typescript-eslint/parser": "^5.53.0", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", "autoprefixer": "^10.4.13", "babel-jest": "^29.4.3", "eslint": "^8.34.0", @@ -45,13 +44,12 @@ "jest": "^29.4.3", "jest-environment-jsdom": "^29.4.3", "postcss": "^8.4.21", - "prettier": "^2.8.4", - "prettier-plugin-svelte": "^2.10.1", + "prettier": "^3.1.0", + "prettier-plugin-svelte": "^3.1.2", "svelte": "^4.0.5", "svelte-check": "^3.4.3", "svelte-jester": "^3.0.0", "svelte-preprocess": "^5.0.3", - "tailwind-merge": "^1.14.0", "tailwindcss": "^3.2.7", "tslib": "^2.5.0", "typescript": "^5.0.0", @@ -61,7 +59,7 @@ "dependencies": { "@egjs/svelte-view360": "^4.0.0-beta.7", "@mdi/js": "^7.3.67", - "@zoom-image/svelte": "^0.1.8", + "@zoom-image/svelte": "^0.2.0", "axios": "^0.27.2", "buffer": "^6.0.3", "copy-image-clipboard": "^2.1.2", diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 6bbe321aa0..fa714fbf75 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -1182,22 +1182,6 @@ export interface CheckExistingAssetsResponseDto { */ 'existingIds': Array; } -/** - * - * @export - * @enum {string} - */ - -export const CitiesFile = { - Cities15000: 'cities15000', - Cities5000: 'cities5000', - Cities1000: 'cities1000', - Cities500: 'cities500' -} as const; - -export type CitiesFile = typeof CitiesFile[keyof typeof CitiesFile]; - - /** * * @export @@ -3850,12 +3834,6 @@ export interface SystemConfigPasswordLoginDto { * @interface SystemConfigReverseGeocodingDto */ export interface SystemConfigReverseGeocodingDto { - /** - * - * @type {CitiesFile} - * @memberof SystemConfigReverseGeocodingDto - */ - 'citiesFileOverride': CitiesFile; /** * * @type {boolean} @@ -3863,8 +3841,6 @@ export interface SystemConfigReverseGeocodingDto { */ 'enabled': boolean; } - - /** * * @export @@ -6844,6 +6820,48 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'deviceId' is not null or undefined + assertParamExists('getAllUserAssetsByDeviceId', 'deviceId', deviceId) + const localVarPath = `/asset/device/{deviceId}` + .replace(`{${"deviceId"}}`, encodeURIComponent(String(deviceId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication cookie required + + // authentication api_key required + await setApiKeyToObject(localVarHeaderParameter, "x-api-key", configuration) + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + setSearchParams(localVarUrlObj, localVarQueryParameter); let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; @@ -7513,9 +7531,11 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration }; }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId: async (deviceId: string, options: AxiosRequestConfig = {}): Promise => { @@ -8347,6 +8367,16 @@ export const AssetApiFp = function(configuration?: Configuration) { const localVarAxiosArgs = await localVarAxiosParamCreator.getAllAssets(skip, take, userId, isFavorite, isArchived, updatedAfter, updatedBefore, ifNoneMatch, options); return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {string} deviceId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllUserAssetsByDeviceId(deviceId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, /** * Get a single asset\'s information * @param {string} id @@ -8494,9 +8524,11 @@ export const AssetApiFp = function(configuration?: Configuration) { return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {string} deviceId * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ async getUserAssetsByDeviceId(deviceId: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { @@ -8722,6 +8754,15 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath getAllAssets(requestParameters: AssetApiGetAllAssetsRequest = {}, options?: AxiosRequestConfig): AxiosPromise> { return localVarFp.getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(axios, basePath)); }, + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { + return localVarFp.getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(axios, basePath)); + }, /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -8828,9 +8869,11 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath return localVarFp.getTimeBuckets(requestParameters.size, requestParameters.userId, requestParameters.albumId, requestParameters.personId, requestParameters.isArchived, requestParameters.isFavorite, requestParameters.isTrashed, requestParameters.withStacked, requestParameters.withPartners, requestParameters.key, options).then((request) => request(axios, basePath)); }, /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} */ getUserAssetsByDeviceId(requestParameters: AssetApiGetUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig): AxiosPromise> { @@ -9066,6 +9109,20 @@ export interface AssetApiGetAllAssetsRequest { readonly ifNoneMatch?: string } +/** + * Request parameters for getAllUserAssetsByDeviceId operation in AssetApi. + * @export + * @interface AssetApiGetAllUserAssetsByDeviceIdRequest + */ +export interface AssetApiGetAllUserAssetsByDeviceIdRequest { + /** + * + * @type {string} + * @memberof AssetApiGetAllUserAssetsByDeviceId + */ + readonly deviceId: string +} + /** * Request parameters for getAssetById operation in AssetApi. * @export @@ -10010,6 +10067,17 @@ export class AssetApi extends BaseAPI { return AssetApiFp(this.configuration).getAllAssets(requestParameters.skip, requestParameters.take, requestParameters.userId, requestParameters.isFavorite, requestParameters.isArchived, requestParameters.updatedAfter, requestParameters.updatedBefore, requestParameters.ifNoneMatch, options).then((request) => request(this.axios, this.basePath)); } + /** + * Get all asset of a device that are in the database, ID only. + * @param {AssetApiGetAllUserAssetsByDeviceIdRequest} requestParameters Request parameters. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof AssetApi + */ + public getAllUserAssetsByDeviceId(requestParameters: AssetApiGetAllUserAssetsByDeviceIdRequest, options?: AxiosRequestConfig) { + return AssetApiFp(this.configuration).getAllUserAssetsByDeviceId(requestParameters.deviceId, options).then((request) => request(this.axios, this.basePath)); + } + /** * Get a single asset\'s information * @param {AssetApiGetAssetByIdRequest} requestParameters Request parameters. @@ -10140,9 +10208,11 @@ export class AssetApi extends BaseAPI { } /** - * Get all asset of a device that are in the database, ID only. + * + * @summary Use /asset/device/:deviceId instead - Remove in 1.92 release * @param {AssetApiGetUserAssetsByDeviceIdRequest} requestParameters Request parameters. * @param {*} [options] Override http request option. + * @deprecated * @throws {RequiredError} * @memberof AssetApi */ diff --git a/web/src/app.html b/web/src/app.html index f35409429e..5f31df3336 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -1,4 +1,4 @@ - + diff --git a/web/src/lib/components/admin-page/restore-dialoge.svelte b/web/src/lib/components/admin-page/restore-dialoge.svelte index 848dedaf65..e96a4a54db 100644 --- a/web/src/lib/components/admin-page/restore-dialoge.svelte +++ b/web/src/lib/components/admin-page/restore-dialoge.svelte @@ -9,8 +9,11 @@ const restoreUser = async () => { const restoredUser = await api.userApi.restoreUser({ id: user.id }); - if (restoredUser.data.deletedAt == null) dispatch('user-restore-success'); - else dispatch('user-restore-fail'); + if (restoredUser.data.deletedAt == null) { + dispatch('user-restore-success'); + } else { + dispatch('user-restore-fail'); + } }; diff --git a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte index a4f5274454..aabb86d88a 100644 --- a/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte +++ b/web/src/lib/components/admin-page/server-stats/server-stats-panel.svelte @@ -21,7 +21,8 @@ return '0'.repeat(zeroLength); }; - $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, 0); + const TiB = 1024 ** 4; + $: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
diff --git a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte index 9098bc33cb..c945d5c9e4 100644 --- a/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte +++ b/web/src/lib/components/admin-page/settings/job-settings/job-settings.svelte @@ -16,8 +16,18 @@ let savedConfig: SystemConfigJobDto; let defaultConfig: SystemConfigJobDto; - const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[]; - const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName)); + const jobNames = [ + JobName.ThumbnailGeneration, + JobName.MetadataExtraction, + JobName.Library, + JobName.Sidecar, + JobName.ObjectTagging, + JobName.ClipEncoding, + JobName.RecognizeFaces, + JobName.VideoConversion, + JobName.StorageTemplateMigration, + JobName.Migration, + ]; async function getConfigs() { [savedConfig, defaultConfig] = await Promise.all([ diff --git a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte index 7093a0eeb0..fe2f879690 100644 --- a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte +++ b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte @@ -4,13 +4,12 @@ NotificationType, } from '$lib/components/shared-components/notification/notification'; import { handleError } from '$lib/utils/handle-error'; - import { api, CitiesFile, SystemConfigDto } from '@api'; + import { api, SystemConfigDto } from '@api'; import { cloneDeep, isEqual } from 'lodash-es'; import { fade } from 'svelte/transition'; import SettingAccordion from '../setting-accordion.svelte'; import SettingButtonsRow from '../setting-buttons-row.svelte'; import SettingSwitch from '../setting-switch.svelte'; - import SettingSelect from '../setting-select.svelte'; import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte'; export let config: SystemConfigDto; // this is the config that is being edited @@ -39,7 +38,6 @@ }, reverseGeocoding: { enabled: config.reverseGeocoding.enabled, - citiesFileOverride: config.reverseGeocoding.citiesFileOverride, }, }, }); @@ -131,24 +129,6 @@ subtitle="Enable reverse geocoding" bind:checked={config.reverseGeocoding.enabled} /> - -
- -
diff --git a/web/src/lib/components/asset-viewer/album-list-item.svelte b/web/src/lib/components/asset-viewer/album-list-item.svelte index ede316ff4b..c632ec3193 100644 --- a/web/src/lib/components/asset-viewer/album-list-item.svelte +++ b/web/src/lib/components/asset-viewer/album-list-item.svelte @@ -44,14 +44,10 @@ > {#if variant === 'simple'} - {#if album.shared}Shared{/if} - + {album.shared ? 'Shared' : ''} {:else} {album.assetCount} items - {#if album.shared} · Shared{/if} - + {album.shared ? ' · Shared' : ''} {/if} diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 2527e61d25..a6bdaf257b 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -17,12 +17,14 @@ export let circle = false; export let hidden = false; export let border = false; + export let preload = true; let complete = false; export let eyeColor: 'black' | 'white' = 'white'; { - if (albumViewSettings === option.sortTitle) { + if (albumViewSettings === option.title) { option.sortDesc = !option.sortDesc; } else { - albumViewSettings = option.sortTitle; + albumViewSettings = option.title; } }; @@ -18,12 +18,12 @@ class="rounded-lg p-2 hover:bg-immich-dark-primary hover:dark:bg-immich-dark-primary/50" on:click={() => handleSort()} > - {#if albumViewSettings === option.sortTitle} + {#if albumViewSettings === option.title} {#if option.sortDesc} ↓ {:else} ↑ {/if} - {/if}{option.table} diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index 9a7bb025e7..1456459a5b 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -12,15 +12,21 @@ import { handleError } from '$lib/utils/handle-error'; import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; - import { mdiCallMerge, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; + import { mdiCallMerge, mdiClose, mdiMagnify, mdiMerge, mdiSwapHorizontal } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; import CircleIconButton from '../elements/buttons/circle-icon-button.svelte'; + import { cloneDeep } from 'lodash-es'; + import LoadingSpinner from '../shared-components/loading-spinner.svelte'; export let person: PersonResponseDto; let people: PersonResponseDto[] = []; + let peopleCopy: PersonResponseDto[] = []; let selectedPeople: PersonResponseDto[] = []; let screenHeight: number; let isShowConfirmation = false; + let name = ''; + let searchWord: string; + let isSearchingPeople = false; let dispatch = createEventDispatcher(); $: hasSelection = selectedPeople.length > 0; @@ -31,12 +37,49 @@ onMount(async () => { const { data } = await api.personApi.getAllPeople({ withHidden: false }); people = data.people; + peopleCopy = cloneDeep(people); }); const onClose = () => { dispatch('go-back'); }; + const resetSearch = () => { + name = ''; + people = peopleCopy; + }; + + const searchPeople = async (force: boolean) => { + if (name === '') { + people = peopleCopy; + return; + } + if (!force) { + if (people.length < 20 && name.startsWith(searchWord)) { + people = peopleCopy + .filter((person: PersonResponseDto) => { + const nameParts = person.name.split(' '); + return nameParts.some((splitName) => splitName.toLowerCase().startsWith(name.toLowerCase())); + }) + .slice(0, 10); + return; + } + } + + const timeout = setTimeout(() => (isSearchingPeople = true), 100); + try { + const { data } = await api.searchApi.searchPerson({ name }); + people = data; + searchWord = name; + } catch (error) { + handleError(error, "Can't search people"); + } finally { + clearTimeout(timeout); + } + + isSearchingPeople = false; + }; + const handleSwapPeople = () => { [person, selectedPeople[0]] = [selectedPeople[0], person]; goto(`${AppRoute.PEOPLE}/${person.id}?action=merge`); @@ -136,9 +179,39 @@ +
+ + + searchPeople(false)} + /> + {#if name} + + {/if} + {#if isSearchingPeople} +
+ +
+ {/if} +
+ +
{#each unselectedPeople as person (person.id)} diff --git a/web/src/lib/components/faces-page/people-card.svelte b/web/src/lib/components/faces-page/people-card.svelte index 289234366e..92d339d02a 100644 --- a/web/src/lib/components/faces-page/people-card.svelte +++ b/web/src/lib/components/faces-page/people-card.svelte @@ -12,6 +12,7 @@ import Icon from '$lib/components/elements/icon.svelte'; export let person: PersonResponseDto; + export let preload = false; type MenuItemEvent = 'change-name' | 'set-birth-date' | 'merge-faces' | 'hide-face'; let dispatch = createEventDispatcher<{ @@ -48,6 +49,7 @@
.main-view { - box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.3), 0 8px 12px 6px rgba(0, 0, 0, 0.15); + box-shadow: + 0 4px 4px 0 rgba(0, 0, 0, 0.3), + 0 8px 12px 6px rgba(0, 0, 0, 0.15); } diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 2424de3201..91f5168f1b 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -91,6 +91,8 @@ diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte index acdf6607a2..b49ced3a0d 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -52,7 +52,7 @@

- Add to {#if shared}Shared {/if} Album + Add to {shared ? 'Shared ' : ''}Album

@@ -88,7 +88,7 @@

- New {#if shared}Shared {/if}Album {#if search.length > 0}{search}{/if} + New {shared ? 'Shared ' : ''}Album {#if search.length > 0}{search}{/if}

{#if filteredAlbums.length > 0} @@ -101,7 +101,8 @@ {#if !shared}

- {#if search.length === 0}ALL {/if}ALBUMS + {#if search.length === 0}ALL + {/if}ALBUMS

{/if} {#each filteredAlbums as album (album.id)} diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 92107693d6..ecfe75258c 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -111,10 +111,20 @@ {#await style then style} - + event.detail.setMaxZoom(14)} + bind:map + > {#if !simplified} - + @@ -134,7 +144,7 @@ }), }} id="geojson" - cluster={{ maxZoom: 14, radius: 500 }} + cluster={{ radius: 500 }} >

Skipped - {#if uploadAsset.message} ({uploadAsset.message}){/if} + {#if uploadAsset.message} + ({uploadAsset.message}) + {/if}

{:else if uploadAsset.state === UploadState.DONE}

Uploaded - {#if uploadAsset.message} ({uploadAsset.message}){/if} + {#if uploadAsset.message} + ({uploadAsset.message}) + {/if}

{/if}
diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 621c981326..f4a21a5408 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -7,13 +7,14 @@ import { createEventDispatcher } from 'svelte'; import { goto } from '$app/navigation'; import { mdiCircleEditOutline, mdiContentCopy, mdiDelete, mdiOpenInNew } from '@mdi/js'; + import noThumbnailUrl from '$lib/assets/no-thumbnail.png'; export let link: SharedLinkResponseDto; let expirationCountdown: luxon.DurationObjectUnits; const dispatch = createEventDispatcher(); - const getAssetInfo = async (): Promise => { + const getThumbnail = async (): Promise => { let assetId = ''; if (link.album?.albumThumbnailAssetId) { @@ -60,18 +61,28 @@ class="flex w-full gap-4 border-b border-gray-200 py-4 transition-all hover:border-immich-primary dark:border-gray-600 dark:text-immich-gray dark:hover:border-immich-dark-primary" >
- {#await getAssetInfo()} - - {:then asset} + {#if link?.album?.albumThumbnailAssetId || link.assets.length > 0} + {#await getThumbnail()} + + {:then asset} + {asset.id} + {/await} + {:else} {asset.id} - {/await} + {/if}
diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 635d6e7aae..add61979d7 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -73,7 +73,10 @@ export class AssetStore { assets: AssetResponseDto[] = []; albumAssets: Set = new Set(); - constructor(options: AssetStoreOptions, private albumId?: string) { + constructor( + options: AssetStoreOptions, + private albumId?: string, + ) { this.options = { ...options, size: TimeBucketSize.Month }; this.store$.set(this); } diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts index 07c99ddb44..19448b944f 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -7,12 +7,26 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number { if (assetCount < 6) { return Math.min(320, Math.floor(viewWidth / assetCount - assetCount)); - } else { - if (viewWidth > 600) return viewWidth / 7 - 7; - else if (viewWidth > 400) return viewWidth / 4 - 6; - else if (viewWidth > 300) return viewWidth / 2 - 6; - else if (viewWidth > 200) return viewWidth / 2 - 6; - else if (viewWidth > 100) return viewWidth / 1 - 6; + } + + if (viewWidth > 600) { + return viewWidth / 7 - 7; + } + + if (viewWidth > 400) { + return viewWidth / 4 - 6; + } + + if (viewWidth > 300) { + return viewWidth / 2 - 6; + } + + if (viewWidth > 200) { + return viewWidth / 2 - 6; + } + + if (viewWidth > 100) { + return viewWidth / 1 - 6; } return 300; diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index 3db0851689..c7506a6158 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -1,9 +1,6 @@